Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/fix-issue/findings/mdl-other.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,4 @@
{"area":"mdl/executor","date":"2026-09-18","symptom":"A page parameter passed as an argument to a nanoflow/microflow BUTTON action is not wired — Studio Pro reports CE1571 \"No argument has been selected for parameter 'X' and no default is available\" on opening the page, while `mx check`, `mxcli check --references` and `mxcli lint` are all clean. Reported as an asymmetry: of two arguments, the one matching the enclosing dataview's DataSource 'works' and the other does not","cause":"Mendix stores a flow argument in one of TWO slots of Forms$MicroflowParameterMapping / Forms$NanoflowParameterMapping: a reference to a page parameter, snippet parameter or page variable goes in `Variable` as a Forms$PageVariable; a literal or expression goes in `Expression`. mxcli only ever wrote `Expression: \"$Name\"`, which binds nothing. The read side was wrong in the mirror image — the three action describers and flowSourceArgs looked for a `Name` key on that sub-document, which Forms$PageVariable does not have","file":"`sdk/pages/pages_widgets_action.go` (VariableKind on both mapping types), `mdl/executor/cmd_pages_flow_args.go` (new: classifyFlowArgValue + pageVariableArgValue), `mdl/executor/cmd_pages_builder_v3.go` (3 of the 4 copies of the $-rule), `mdl/backend/modelsdk/widget_write.go` (bindParameterMappingValue), `mdl/executor/cmd_pages_describe_output.go` + `cmd_pages_describe_datasource.go` (read)","insight":"**The reported asymmetry is a red herring — both arguments were written identically and NEITHER was bound.** Studio Pro supplies a default for the one that is the dataview's object and reports the other; 'and no default is available' in CE1571 says exactly that. Time spent on why $Dto worked is wasted. **mxbuild is not a detector here**: `mx check` on the reported project is 0 errors before AND after the fix, so the usual two-copies-of-a-real-project run proves nothing and the reporter is right that it only shows in Studio Pro. **Get the reference from a Marketplace .mpk — it contains a whole Studio Pro-authored `project.mpr`**: `mxcli marketplace download <id> --output x.mpk && unzip -o x.mpk project.mpr`, then `mxcli bson dump` it. A blank app is useless for this (every mapping list in it is empty); Workflow Commons 4.11.0 gave 101 flow parameter mappings, of which 95 bind through Variable and 6 through Expression — and all 6 of those are Boolean literals, so the $-prefixed Expression mxcli wrote occurs ZERO times. `marketplace install` refuses that package (javasource path guard), so extract rather than install. **The PageVariable slot follows what the name refers to** (PageParameter 20, SnippetParameter 58, Widget 17) — a snippet is the COMMON case, not the corner, and `paramScope` is the right oracle because it holds only entity-typed parameters, which is the same set Mendix binds this way. **Leave $currentObject alone**: no reference for the bare form was measured and show_page already depends on the context object being inferred (MDL-PAGEARG01), so changing it on a guess risks the case that works. **The read bug hid the write bug**: describe printed `Action: microflow M.F` with no arguments for Studio Pro content, so a round-trip looked lossless and the missing binding never showed up as a diff","refs":["mendixlabs/mxcli#1140","mendixlabs/mxcli#835"],"ce":["CE1571"]}
{"area": "mdl/versions", "date": "2026-09-21", "symptom": "A version gate copied from the issue text (\"Workflow Groups are GA from Mendix 11.6\") is wrong by four minors", "cause": "Mendix's release notes date the FEATURE's general availability; the metamodel floor is when the type and its property were introduced, and that is what decides whether the document loads. `Settings$WorkflowGroup` and `WorkflowsProjectSettingsPart.groups` are both `introduced: \"11.2.0\"`", "file": "`sdk/versions/mendix-11.yaml` (`workflows.groups`)", "insight": "The arbiter for a metamodel floor is the Model SDK's own StructureVersionInfo: `npm pack mendixmodelsdk && tar xzf \u2026 && grep -n '<Type>' package/src/gen/<domain>.js`, then read BOTH the class's `versionInfo.introduced` and its `properties.<name>.introduced` \u2014 a property can arrive later than its type. Release notes, proposal text and a number already written down in this repo are all downstream of it (same trap as mendixlabs/mxcli#1121). Corroborate it against two real projects rather than trusting one source: `mxcli new` at a version either side of the floor and diff the document's keys \u2014 an 11.1.0 workflows settings part has no `Groups` key at all, an 11.13.0 one carries `Groups: [2]`, which also proves the refusal is right rather than over-cautious (writing the property below the floor would be inventing a key). mendixlabs/mxcli#272", "refs": ["mendixlabs/mxcli#272"]}
{"area": "mdl/linter/rules", "date": "2026-09-23", "symptom": "CONV010 flagged an ACT_ nanoflow that delegated to a sub-flow \u2014 the very thing the rule demands. A real project patched its own copy of the rule and asked for the fix upstream. An ACT_ nanoflow could satisfy CONV010 in NO way: delegate and be flagged, or inline the logic and be flagged.", "cause": "ALLOWED_ACTIONS held `MicroflowCallAction` but not `NanoflowCallAction`. `microflows()` yields nanoflows too \u2014 the catalog's `microflows` table carries a MicroflowType column \u2014 so CONV010 lints ACT_ nanoflows, and a nanoflow delegates with a nanoflow call.", "file": "`.claude/lint-rules/conv010_act_microflow_content.star` (NanoflowCallAction added to ALLOWED_ACTIONS; cmd/mxcli/lint-rules/ is gitignored and regenerated by `make sync-lint-rules`), test `mdl/catalog/lint_rule_vocabulary_test.go` (added to the `permitted` list)", "insight": "Third time this one allowlist has been short, and the rule's own comments record the previous two: the wrong vocabulary entirely (storage names vs SDK names, matching nothing, 11 false positives of 13 findings) and a missing ExclusiveMerge that a permitted ExclusiveSplit necessarily creates (122 hits on one project). The recurring shape is an UNSATISFIABLE rule, and its cost is asymmetric: a rule that cannot be satisfied does not read as a broken rule, it reads as broken CODE, so users refactor around it or patch the rule locally and the defect never comes back upstream \u2014 which is exactly what happened here until someone wrote 'report upstream' in their findings. A vocabulary pin test (TestCONV010AllowsWhatTheCatalogCallsUIActions) already existed to stop this class and did not, because its `permitted` list is hand-maintained and was itself incomplete: pinning a rule to a hand-written list of what SHOULD be allowed only moves the completeness problem. Worth considering: enumerate the delegation actions from the type system rather than listing them.", "refs": ["ako/mxcli#644"]}
{"area": "mdl/linter/rules", "date": "2026-09-25", "symptom": "`mxcli lint` reported CONV013 \"Java action call in 'M.MF' uses '' error handling instead of Custom\" on calls that DO have `on error { ... }` / `on error without rollback { ... }` -- describe shows the handler, mx check is clean -- and never reported CONV014 for `on error continue` on an action. Every Java/REST/web service call was flagged whatever its handling, so the only way to clear the warning was to ignore it", "cause": "Mendix stores error handling on the action (Microflows$*Action.ErrorHandlingType) and both readers put it there, leaving the activity-level BaseActivity.ErrorHandlingType empty. CONV013 and CONV014 read the activity field. DESCRIBE was right because getActionErrorHandlingType reads the action (the reflection lookup from #1078); the linter never used it", "file": "`sdk/microflows/error_handling.go` (new: ActionActivity.ErrorHandling, ActionErrorHandlingType); `mdl/linter/rules/conv_error_handling.go`; `mdl/executor/cmd_microflows_show_helpers.go` (getActionErrorHandlingType now calls it); example `mdl-examples/bug-tests/lint-action-error-handling.mdl`", "insight": "The rule's unit tests built the activity by hand with the handling on the ACTIVITY, the one place the reader never puts it -- so they passed while the rule was wrong on every real model. A rule test should build its input the way the reader does, or run on a model read back from disk. The accessor now lives in sdk/microflows so the describer and the linter cannot drift apart again; one consumer being right (DESCRIBE) is what hid the other being wrong. Measured on a blank 11.12.1 app with four Java calls: before, CONV013 on all four with '' and no CONV014; after, CONV013 only on the unhandled (Rollback) and the continue call, CONV014 on the continue call", "rules": ["CONV013", "CONV014"], "refs": ["#1202", "#1078"]}
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

- **`@caption` on a `while` loop was dropped without a word** (mendixlabs/mxcli#1187) — `check` passed, `exec` wrote the loop, and `describe` showed it without the caption, while the same caption on a `loop` was reported as **MDL042**. Both build a `Microflows$LoopedActivity`, which has no Caption property; only the for-each case was checked. MDL042 now covers a `while` too, pointing to `@annotation`, which round-trips through `describe`.

- **Lint CONV013 flagged every Java action, REST and web service call, and CONV014 never flagged an action's `on error continue`** (mendixlabs/mxcli#1202) — CONV013 reported `uses '' error handling instead of Custom` on calls that do have `on error { … }` or `on error without rollback { … }`, while `describe` showed the handler and `mx check` was clean. Mendix stores error handling on the action, and that is where the reader puts it; both rules read the activity-level field, which the reader leaves empty. They now read it where `describe` does, through one accessor in `sdk/microflows` shared by the describer and the linter. Measured on a blank 11.12.1 app with four Java calls: before, CONV013 on all four and no CONV014; after, CONV013 on the unhandled (`Rollback`) and `continue` calls only, and CONV014 on the `continue` call.

## [0.24.0] - 2026-09-24

Headline: **An element's storage GUID is the database's identity, and mxcli now treats it as one.** A production report of 28 attributes emptied across 607 rows by a single edit (mendixlabs/mxcli#1119) traced to five write paths that re-minted GUIDs — one of them moving 282 in a single module. They are fixed, and a new guard at the write choke point refuses any write that moves one: a class of data loss that leaves the model valid, `mx check` clean and `DESCRIBE` byte-identical, and surfaces only when the package meets a database that already holds data. Alongside it, `MOVE ENTITY` and `RENAME` stop leaving a project unbuildable, and four more scripts that passed every gate and failed the build are refused.
Expand Down
64 changes: 64 additions & 0 deletions mdl-examples/bug-tests/lint-action-error-handling.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
-- ============================================================================
-- Lint: CONV013 / CONV014 read the error handling of an action activity
-- ============================================================================
--
-- Symptom (before fix):
-- The reader stores an action activity's error handling on the ACTION
-- (Microflows$*Action.ErrorHandlingType), where the model keeps it. CONV013
-- and CONV014 read the activity-level field instead, which the reader
-- leaves empty. So:
-- - CONV013 flags every Java action, REST and web service call, including
-- ones with `on error { ... }`, as "uses '' error handling";
-- - CONV014 never flags an action with `on error continue`.
--
-- After fix:
-- Both rules read the action's handling, the same way DESCRIBE does
-- (getActionErrorHandlingType): MF_Handled and MF_HandledNoRollback
-- raise no CONV013, MF_Unhandled raises one, MF_Continue raises CONV014.
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/lint-action-error-handling.mdl -p app.mpr
-- mxcli lint -p app.mpr (look for LintEh.* under CONV013 / CONV014)
-- ============================================================================

create module LintEh;

create java action LintEh.JA_Echo (
Text: string not null
) returns string
as $$
return Text;
$$;
/

-- Custom handling with rollback: no CONV013.
create microflow LintEh.MF_Handled ($Text: string)
begin
call java action LintEh.JA_Echo (Text = $Text) on error {
log error node 'LintEh' 'echo failed';
};
end;
/

-- Custom handling without rollback: no CONV013.
create microflow LintEh.MF_HandledNoRollback ($Text: string)
begin
call java action LintEh.JA_Echo (Text = $Text) on error without rollback {
log error node 'LintEh' 'echo failed';
};
end;
/

-- No handling (the default, Rollback): CONV013.
create microflow LintEh.MF_Unhandled ($Text: string)
begin
call java action LintEh.JA_Echo (Text = $Text);
end;
/

-- Continue swallows the error: CONV014.
create microflow LintEh.MF_Continue ($Text: string)
begin
call java action LintEh.JA_Echo (Text = $Text) on error continue;
end;
/
38 changes: 2 additions & 36 deletions mdl/executor/cmd_microflows_show_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package executor
import (
"context"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -2284,41 +2283,8 @@ func getActionErrorHandlingType(activity *microflows.ActionActivity) microflows.
return ""
}

if errType := actionErrorHandlingField(activity.Action); errType != "" {
return errType
}
// Fall back to activity level for action types without ErrorHandlingType field.
return activity.ErrorHandlingType
}

// actionErrorHandlingField reads ErrorHandlingType off any action that declares it.
//
// Reflection rather than a case per action type. The hand-maintained switch this
// replaces had drifted to 17 of the 38 action types that carry the field, and
// every one of the 21 it missed — CreateObjectAction and ChangeObjectAction
// among them — dropped that activity's whole error branch from DESCRIBE. The
// list had already been patched twice for individual instances (#863, #1020's
// neighbour) and silently regrew, which is what makes an enumeration the wrong
// shape here: a new action type must not have to be remembered.
//
// Actions embed model.BaseElement, which has no such field, so a promoted field
// cannot be picked up by accident.
func actionErrorHandlingField(action microflows.MicroflowAction) microflows.ErrorHandlingType {
v := reflect.ValueOf(action)
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return ""
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return ""
}
f := v.FieldByName("ErrorHandlingType")
if !f.IsValid() || f.Kind() != reflect.String {
return ""
}
return microflows.ErrorHandlingType(f.String())
// On the action where Mendix stores it; the activity field for action types without one.
return activity.ErrorHandling()
}

// collectErrorHandlerStatements traverses the error handler flow and collects statements.
Expand Down
2 changes: 1 addition & 1 deletion mdl/executor/microflow_error_handler_authoring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func actionErrorHandlingTypes(fb *flowBuilder) []microflows.ErrorHandlingType {
var out []microflows.ErrorHandlingType
for _, o := range fb.objects {
if a, ok := o.(*microflows.ActionActivity); ok {
out = append(out, actionErrorHandlingField(a.Action))
out = append(out, microflows.ActionErrorHandlingType(a.Action))
}
}
return out
Expand Down
27 changes: 0 additions & 27 deletions mdl/executor/microflow_error_handler_roundtrip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,33 +155,6 @@ func TestDescribe_RollbackIsStillNotRendered(t *testing.T) {
}
}

// actionErrorHandlingField is the reflection lookup that replaced the switch.
// Pinning it directly is what makes the fix durable: a NEW action type carrying
// ErrorHandlingType is handled the moment it exists, with nothing to remember.
func TestActionErrorHandlingField(t *testing.T) {
for _, tc := range []struct {
name string
action microflows.MicroflowAction
want microflows.ErrorHandlingType
}{
{"reads the field", &microflows.CreateVariableAction{
ErrorHandlingType: microflows.ErrorHandlingTypeCustom}, microflows.ErrorHandlingTypeCustom},
{"empty when unset", &microflows.CreateVariableAction{}, ""},
{"action without the field", &microflows.ListOperationAction{}, ""},
{"nil action", nil, ""},
} {
if got := actionErrorHandlingField(tc.action); got != tc.want {
t.Errorf("%s: got %q, want %q", tc.name, got, tc.want)
}
}

// A typed-nil pointer must not panic — activity.Action can hold one.
var typedNil *microflows.CreateVariableAction
if got := actionErrorHandlingField(typedNil); got != "" {
t.Errorf("typed nil: got %q, want empty", got)
}
}

// RestOperationCallAction stores the field but Mendix refuses a custom handler on
// it (CE6035), so it is the one action deliberately not reported. A reflection
// lookup would otherwise pick it up — this is the case that stops the generic fix
Expand Down
11 changes: 6 additions & 5 deletions mdl/linter/rules/conv_error_handling.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,15 @@ func findUnhandledCalls(objects []microflows.MicroflowObject, mf linter.Microflo
continue
}

// Check if error handling is not custom
if act.ErrorHandlingType != microflows.ErrorHandlingTypeCustom &&
act.ErrorHandlingType != microflows.ErrorHandlingTypeCustomWithoutRollback {
// Check if error handling is not custom. It is stored on the action, not the activity.
errType := act.ErrorHandling()
if errType != microflows.ErrorHandlingTypeCustom &&
errType != microflows.ErrorHandlingTypeCustomWithoutRollback {
*violations = append(*violations, linter.Violation{
RuleID: r.ID(),
Severity: r.DefaultSeverity(),
Message: fmt.Sprintf("%s in '%s.%s' uses '%s' error handling instead of Custom.",
actionName, mf.ModuleName, mf.Name, act.ErrorHandlingType),
actionName, mf.ModuleName, mf.Name, errType),
Location: linter.Location{
Module: mf.ModuleName,
DocumentType: mf.DocumentNoun(),
Expand Down Expand Up @@ -144,7 +145,7 @@ func findContinueErrorHandling(objects []microflows.MicroflowObject, mf linter.M
for _, obj := range objects {
switch act := obj.(type) {
case *microflows.ActionActivity:
if act.ErrorHandlingType == microflows.ErrorHandlingTypeContinue {
if act.ErrorHandling() == microflows.ErrorHandlingTypeContinue {
caption := act.Caption
if caption == "" {
caption = "(unnamed activity)"
Expand Down
40 changes: 40 additions & 0 deletions mdl/linter/rules/conv_error_handling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package rules

import (
"strings"
"testing"

"github.com/mendixlabs/mxcli/mdl/linter"
Expand Down Expand Up @@ -229,3 +230,42 @@ func TestNoContinueErrorHandlingRule_Metadata(t *testing.T) {
t.Errorf("ID = %q, want CONV014", r.ID())
}
}

// The readers store an action activity's error handling on the ACTION and leave
// the activity field empty. Before the rules read it there, a handled Java call
// was reported as "uses ” error handling" and `on error continue` on an action
// was never reported at all.
func TestFindUnhandledCalls_HandlingOnTheAction(t *testing.T) {
for _, tc := range []struct {
name string
eh microflows.ErrorHandlingType
count int
}{
{"custom", microflows.ErrorHandlingTypeCustom, 0},
{"custom without rollback", microflows.ErrorHandlingTypeCustomWithoutRollback, 0},
{"rollback", microflows.ErrorHandlingTypeRollback, 1},
} {
objects := []microflows.MicroflowObject{
&microflows.ActionActivity{Action: &microflows.JavaActionCallAction{ErrorHandlingType: tc.eh}},
}
var violations []linter.Violation
findUnhandledCalls(objects, testMicroflow(), NewErrorHandlingOnCallsRule(), &violations)
if len(violations) != tc.count {
t.Fatalf("%s: expected %d violation(s), got %d", tc.name, tc.count, len(violations))
}
if tc.count == 1 && !strings.Contains(violations[0].Message, "'Rollback'") {
t.Errorf("%s: message should name the stored handling, got %q", tc.name, violations[0].Message)
}
}
}

func TestFindContinueErrorHandling_ContinueOnTheAction(t *testing.T) {
objects := []microflows.MicroflowObject{
&microflows.ActionActivity{Action: &microflows.MicroflowCallAction{ErrorHandlingType: microflows.ErrorHandlingTypeContinue}},
}
var violations []linter.Violation
findContinueErrorHandling(objects, testMicroflow(), NewNoContinueErrorHandlingRule(), &violations)
if len(violations) != 1 || violations[0].RuleID != "CONV014" {
t.Fatalf("expected one CONV014 for `on error continue` stored on the action, got %v", violations)
}
}
Loading
Loading