Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4d495d5
fix(check): resolve an image reference case-sensitively, as Mendix does
claude Nov 4, 2026
8a4d784
test(navigation): a wrong-case app icon is refused, as Mendix refuses it
claude Nov 4, 2026
e169e96
feat(microflows): include associations on a call external action argu…
claude Nov 5, 2026
8632169
fix(microflows): a spliced rewrite keeps a call's associations to inc…
claude Nov 5, 2026
5b77b8e
Merge pull request #1972 from ako/claude/wonderful-hypatia-x7mmyq
ako Nov 5, 2026
337a345
fix(pages): a snippet call passing a data view's object names the wid…
claude Nov 5, 2026
5f15aa0
fix(pages): a text template bound to an unset data source is null (#1…
claude Nov 5, 2026
f828510
Merge pull request #1973 from ako/claude/wonderful-hypatia-x7mmyq
ako Nov 5, 2026
6a31663
fix(describe): print a decision with an empty true branch as stored (…
claude Nov 5, 2026
2009895
fix(check): accept DateFormat: Custom set together with its pattern (…
claude Nov 5, 2026
1edc161
feat(pages): alter page set CaptionParams / ContentParams (#1966)
claude Nov 5, 2026
b2ffd2e
Merge pull request #1974 from ako/claude/wonderful-hypatia-x7mmyq
ako Nov 5, 2026
fcef211
fix(microflows): an import mapping rooted at a JSON array returns one…
claude Nov 5, 2026
1d8020b
Merge pull request #1975 from ako/claude/wonderful-hypatia-x7mmyq
ako Nov 5, 2026
700677d
fix(odata): read annotation terms written with a vocabulary alias (#1…
claude Nov 5, 2026
f39a9f4
Merge pull request #1978 from ako/claude/wonderful-hypatia-x7mmyq
ako Nov 5, 2026
e21f9d7
Merge pull request #1979 from ako/fix/image-reference-case
ako Nov 5, 2026
5944373
feat(pages): alter page set a Data grid 2 column's attribute (#1644)
claude Nov 5, 2026
2b6fc1a
Merge pull request #1985 from ako/claude/wonderful-hypatia-x7mmyq
ako Nov 5, 2026
e48bd0d
fix(workflow): address boundary events by number, refuse ambiguous ou…
claude Nov 6, 2026
02e4e5a
Merge pull request #1987 from ako/claude/wonderful-hypatia-x7mmyq
ako Nov 6, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/backend", "date": "2026-11-06", "symptom": "alter workflow: `drop X outcome 'V'` / `drop X outcome <enum value>` dropped the first of two outcomes sharing that value; `drop X boundary event` could not say which of several events, so under mdl 1 it was refused with no way to drop the second; check-time guards went silent on a reference the mutator resolved by name, because resolveStoredActivity pooled a jump captioned with its target's name with the target itself", "cause": "Both mutators' drop loops stopped at the first match and never counted; the boundary-event op carried no address at all; the executor's resolveStoredActivity copied findActivityByCaption but not its preferNameMatches narrowing", "file": "mdl/backend/wfmutator/mutator.go (dropOneOutcome), mdl/backend/mcp/workflow.go (dropFromActivityArray), mdl/backend/workflow_members.go (BoundaryEventIndex), mdl/executor/validate_workflow_activity_kind.go (resolveStoredActivity)", "fix": "A value that matches more than one outcome is refused as ambiguous, nothing written, in both mutators; `boundary event N` (1-based, describe order) addresses one event, out of range refused at exec before the mutator; resolveStoredActivity prefers Name matches without @N", "insight": "A check-time resolver that mirrors a mutator must mirror every narrowing step, or the check silently abstains (it returns nil on ambiguity by design) exactly where exec acts; test the resolver on the jump-caption shape directly. A drop loop with `!found` guards is the tell for a first-match guess: count matches, then decide", "refs": "ako/mxcli#808, ako/mxcli#796", "test": "mdl/backend/wfmutator/mutator_test.go (DuplicateValueRefused, DropBoundaryEvent_ByIndex); mdl/executor/alter_workflow_member_address_test.go (DropBoundaryEventByIndex, ResolveStoredActivity_PrefersName); mdl-examples/bug-tests/808-drop-boundary-event-by-number.mdl"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-11-04", "symptom": "`mxcli check --references` passed `Image: 'Atlas_Core.Content.mendix'` for the stored image `Mendix` (and `Atlas_Core.content.Mendix`, `atlas_core.Content.Mendix`); mx then reported CE1613 \"The selected image '…' no longer exists\" for each. The miss hint `it holds: mendix` printed the name lower-cased, so copying it from the message produced the CE1613", "cause": "imageRefErrors / buildImageQualifiedNames (and the script context's created collections) keyed every image and collection lower-cased on the unmeasured assumption, pinned by two tests, that Mendix resolves names case-insensitively", "file": "`mdl/executor/validate_widget_image_ref.go` (imageRefErrors, sameNameOtherCase, imagesInCollection, buildImageQualifiedNames), `mdl/executor/validate.go` (scriptContext image collections)", "insight": "Measure case-sensitivity per part before encoding it: one blank-app run with four pages (exact, wrong image case, wrong collection case, wrong module case) through `docker check --no-update-widgets` settled it — CE1613 on all three, clean on the exact one. Two tests asserted the opposite (\"Mendix resolves names case-insensitively, and so must this\") with no measurement behind them; they were inverted, not deleted. A case-only miss names the stored spelling instead of the generic hint, and the hint lists names as stored. The icon reference check (iconIndex) was already exact. Found while verifying ako/mxcli#1503. The navigation `app icon` goes through the same resolveImageRefs and carried the same assumption (TestAppIconMustResolveAtExec accepted `myfirstmodule.images.logo`): measured, main wrote `app icon Atlas_Core.Content.mendix` and mx reported CE1613 at the navigation profile, so that case is now refused too. A full `make ci-local` needs the testdata/testapp submodule (`git submodule update --init testdata/testapp`); without it TestIssue1640_RegisteredListMarkersAreWhatStudioProStores fails on main as well", "refs": ["ako/mxcli#1503"], "ce": ["CE1613"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-11-05","symptom":"`alter page … { set (CaptionParams: ({1} = VersionNo)) on btn }` and `set (ContentParams: …)` on a dynamic text: parse error (\"no viable alternative at input '({'\"), and the visitor added \"takes a plain value, not an expression\" — a template's parameters could be written by create page and replace but not changed in place (mendixlabs/mxcli#1744)","cause":"alterPageAssignment had no alternative for paramListV3, and the page mutator no setter for a ClientTemplate's Parameters","file":"mdl/grammar/MDLParser.g4 (alterPageAssignment), mdl/visitor/visitor_alter_page.go, mdl/executor/cmd_alter_page.go (setTemplateParameters), mdl/backend/pagemutator/template_params.go; test alter_set_template_params_pedapp_test.go","insight":"Resolve the parameters with the CREATE builder (buildClientTemplateParams) seeded with mutator.EnclosingEntity(widget), not with a resolver of the ALTER path's own: a bare attribute then means in a SET what it means in create page. Prove the written property set against create page's, not only against mx check — dump both pages and diff the Forms$ClientTemplateParameter with $IDs stripped (identical here); merge compares property names (#1373). Measured on fresh 11.14.0: check, exec and mx check (0 errors) all pass.","refs":["ako/mxcli#1966","mendixlabs/mxcli#1744"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-11-05","symptom":"`alter page … { set (Attribute: Note) on dgOrders column(Number) }` refused for every spelling — \"column property \\\"Attribute\\\" holds a value of kind Attribute, which ALTER cannot set from a plain value\" — and `Attribute: Assoc/Attr` was a parse error; replacing the column was the only route, and it drops column properties the replacement does not restate (ako/mxcli#1644)","cause":"setColumnPropertyMut wrote only plain-value kinds, nothing bound the column's attribute against the grid's data source, and alterPageAssignment parsed `A/B` as a division expression","file":"mdl/executor/alter_set_column_attribute.go (resolveColumnAttribute), mdl/executor/cmd_alter_page.go (IsColumn Attribute branch; attribute applied last in applySetPropertyMutator), mdl/backend/pagemutator/mutator.go (setColumnAttributeBinding), mdl/backend/mutation.go (AttributeBinding.Steps), mdl/grammar/MDLParser.g4 + visitor_alter_page.go (ATTRIBUTE: hop/path); tests alter_set_column_attribute_pedapp_test.go","insight":"A SET that changes the property an element is ADDRESSED by breaks every later step that re-finds it by the address as written — here the hidden-property reset after a column SET (#1519), which failed with \"column(Old) not found\" once the attribute moved. Apply the address-changing property last. PedApp's Data grid 2 package ships no editorConfig, so the reset never ran there and the PedApp test passed against the bug; the 11.14 measurement found it, and the regression test installs the expr-checker fixture's package to reach it (control: without the ordering it fails with the measured error). Binding mirrors #1641: the executor resolves against EnclosingEntityForChildren(grid) with misbound/foreign-qualified refusals, the mutator writes AttributeRef + EntityRef steps as CREATE writes a multi-hop column.","refs":["ako/mxcli#1644","ako/mxcli#1641","mendixlabs/mxcli#1519"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/types","date":"2026-11-05","symptom":"`$Root = import from mapping M.IM($Json)` over an array-rooted JSON structure whose mapping maps the array itself to its root entity (`create M.Root { create M.Item_Root/M.Item = JsonObject {…} }`): mx check 11.14 CE0243 \"The mapping used to return a value of type 'List of M.Root', but now returns a value of type 'M.Root'\" on the first create — the reporter saw it after re-executing; Studio Pro's \"open the action and click OK\" cleared it (mendixlabs/mxcli#1734)","cause":"types.JsonMappingRootIsList fell back to the JSON structure's root kind whenever the root path did not cross an array, so ANY array-rooted structure read as list-returning — but a root at \"(Array)\" is the array container itself, one object; only a root at \"(Array)|(Object)\" (the item, Studio Pro's own shape, #248) yields many","file":"mdl/types/mapping_shape.go (JsonMappingRootIsList: a stored root path decides alone); test mdl/types/mapping_shape_test.go","insight":"Cardinality is a property of WHERE the mapping roots, not of the structure: the same array-rooted structure gives one object or many depending on whether the root element is the array or its item. A fallback to a coarser source is only safe when the finer one is absent — here the fallback ran whenever the finer source said 'not a list', which turned its 'no' into 'unknown'. The fix is shared by the lint rules (same function). Measured on fresh 11.14.0: base 1 x CE0243 on create; fix 0 on create and after a re-execute with a renamed variable.","refs":["ako/mxcli#1938","mendixlabs/mxcli#1734","#248","#267"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-11-05","symptom":"`alter page … { set (DateFormat: Custom, CustomDateFormat: 'dd/MM/yyyy') on dp }` on a picker whose stored format is not Custom: check refused it (\"`DateFormat: Custom` needs a pattern … in the same statement\") in either order and as two SETs, so exec's pre-check refused it too; --no-check wrote it and mx check passed (mendixlabs/mxcli#1743)","cause":"setWidgetFormattingMut refused Custom on the STORED blank pattern, relying on exec applying properties in name order; check's checkSetOp dry-runs one property at a time on a fresh probe of the stored widget, where Custom never sees the pattern. The whole-statement post-check (checkDateFormatAfterSet, #977) ran only in exec","file":"mdl/backend/pagemutator/mutator.go (in-setter guard removed), mdl/executor/validate_alter_set.go (checkDateFormatSet: earlier SETs on the widget, then this one whole, then the #977 check); test alter_set_date_format_pedapp_test.go","insight":"A rule between two properties cannot live in a per-property setter: whichever setter runs first sees half the statement, and a per-property dry run sees less. Put the cross-property rule after the whole statement, and give check the SAME judgement on a probe carrying the statement's earlier SETs — exec applies them in sequence on one copy, check probes the stored widget per SET. assertAgree (check then exec, same verdict) is the test shape for every check/exec disagreement. Control: before the change the three reported forms fail assertAgree; Custom with no pattern is refused by both throughout.","refs":["ako/mxcli#1965","mendixlabs/mxcli#1743","ako/mxcli#977"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-11-05","symptom":"describe printed a decision whose true branch is empty as `if not(X) then <false branch> end if;`; executing the description stored not(X) with the true/false outcomes swapped — behaviour unchanged, model changed, so describe -> exec was not a round trip (mendixlabs/mxcli#1742)","cause":"an 'empty-then swap' in traverseFlow / traverseFlowUntilMerge folded the decision for readability; #1930 then had to undo the fold in the condition-edit splice (decisionPrintedNegated), and alter targets carried the stored form as an Alternate","file":"mdl/executor/cmd_microflows_show_helpers.go (swap removed, negateIfCondition / negateExpression / decisionPrintedNegated deleted), mdl/executor/cmd_alter_flow.go (no fold to undo); tests flow_splice_negated_condition_pedapp_test.go (TestDescribeExec_EmptyThenDecisionRoundTrips), cmd_microflows_traverse_test.go, cmd_microflows_handles_test.go","insight":"A describe that prints a NORMALISED form instead of the stored one is a standing round-trip defect: every consumer of the text inherits a compensation (the splice's un-negation, the handles' alternates), and each new consumer is a new place to forget one. Prefer printing what is stored when the grammar can express it — `if X then else … end if;` always parsed and builds exactly Studio Pro's shape. A script written against the old folded output still means the same thing: the splice diffs the branches independently, so `if not(Y) then B` over the stored `if X then else B` sets the condition and moves B, it does not invert. Its alter TARGETS in the folded form no longer resolve — a refusal, never a wrong edit. Control: with the fold restored, the PedApp round-trip test reproduces the reporter's table exactly (not($Sale/Code != empty), outcomes swapped).","refs":["ako/mxcli#1963","mendixlabs/mxcli#1742","ako/mxcli#1930"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-11-05","symptom":"a call external action argument's Associations to include had no MDL: describe did not print the tree, a new call could not have one, and — found while measuring — a `create or modify` that took the splice path rebuilt the call and dropped the stored tree, with no warning, although #1922's carry claimed to keep it","cause":"#1922 wired carryIncludedAssociations into buildMicroflowFromStmt only; a create or modify of a stored flow is applied by modifyFlowInPlace first, whose replace op rebuilds the activity through the alter-flow fragment builder and never reaches the carry","file":"mdl/grammar/domains/MDLMicroflow.g4 (includeAssociationsClause), mdl/executor/cmd_microflows_builder_calls.go (buildIncludedAssociations), external_action_carry.go (stated, carriedIncludes), cmd_alter_flow.go (replace carries, statedIncludes), cmd_flow_modify.go (warning), cmd_microflows_format_action.go (describe), validate_external_action_includes.go (MDL-ODATA10), validate_external_action_calls.go (check resolution); tests external_action_include_clause_test.go, external_action_include_splice_pedapp_test.go, mdl/backend/modelsdk/microflow_external_action_include_clause_test.go","insight":"A carry for unauthored state has as many sites as the statement has write paths: create or modify of a STORED flow is a splice first and a rebuild only on fallback, so a carry in the rebuild alone is skipped in the common case. The mock harness never caught it because its stored flow had no start/end and always fell back to the rebuild — the bug only showed on a real project. Enumerate the write paths (rebuild, splice replace, alter replace) and test the splice against the real backend (PedApp + a local $metadata). IsParent is derived from the side entity being the association's FROM end; the authored tree encodes to Studio Pro 11.16's bytes, $IDs aside, and flipping IsParent breaks that comparison. Controls: stashing the splice change fails the PedApp test with the tree dropped; removing the stated skip makes `include associations ()` keep the tree.","refs":["ako/mxcli#1914","mendixlabs/mxcli#1721","ako/mxcli#1922"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-11-05","symptom":"`snippetcall … Params: (Item = $dvOuter)` — a data view's object passed by the data view's name — passed check and exec and described back, and mx check 11.14.0 reported CE0115 \"The arguments that are passed to snippet … need to be refreshed\" (mendixlabs/mxcli#1746)","cause":"buildSnippetCallParams knew three argument kinds (snippet parameter, page variable, everything else as PageParameter); the `widget` kind classifyFlowArgValue already returns for a document widget (#1560) was dropped, so the data view name went into the PageParameter slot","file":"mdl/executor/cmd_pages_builder_v3_widgets.go (IsWidget), sdk/pages/pages_widgets_advanced.go, mdl/backend/modelsdk/widget_write.go (Widget slot), mdl/executor/cmd_pages_describe_parse.go (flowArgVariableValue); tests snippetcall_widget_argument_test.go, widget_scoped_variable_test.go","insight":"The classifier already said `widget`; the snippet-call consumer mapped only the kinds it knew and let the rest fall into the default slot — when a classifier gains a kind, grep its consumers for a switch without that case. Measure on the reporter's version: 11.12.6 reports nothing for this mapping, base build or fixed, so a measurement there would have proven nothing; 11.14.0 gives the reported 2 x CE0115 on the base build and 0 on the fix.","refs":["ako/mxcli#1968","mendixlabs/mxcli#1746","ako/mxcli#1560"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/backend/widgetobj","date":"2026-11-05","symptom":"a pluggable widget with an optional datasource X and a textTemplate bound to it (dataSource=\"X\"), both unset: check and exec pass, mx check 11.14 reports CE0463 \"The definition of this widget has changed\"; `mx update-widgets` changes only that TextTemplate, empty Forms$ClientTemplate -> null (mendixlabs/mxcli#1745)","cause":"the builder nulls a TextTemplate only when the widget's editorConfig hides it (#574) or an item rule does (#1198); a template bound to an unset data source is a third case, decided by the widget.xml binding, not by editorConfig — and the binding was already loaded as PropertyTypeIDEntry.DataSourceProperty","file":"mdl/backend/widgetobj/unset_datasource_templates.go (NullTemplatesOfUnsetDataSources), mdl/executor/widget_engine.go (step 4.11), mdl/backend/mutation.go; test unset_datasource_templates_test.go","insight":"The reporter's widget is unpublished, and no widget an app ships has a TOP-LEVEL textTemplate bound to an OPTIONAL datasource (Gallery, Timeline and TreeNode bind to required ones; HTMLElement's is hidden by editorConfig outside repeat mode). A probe .mpk is a few lines: package.xml, the widget XML pair from the issue, an empty .mjs — `mxcli widget extract --mpk` then makes it authorable. Measured on fresh 11.14.0: base 3 x CE0463 (one per page), fix 0, and the reporter's control (data source set, template unset) keeps its empty template in both. Only unset templates are nulled; authored text is the statement's.","refs":["ako/mxcli#1967","mendixlabs/mxcli#1745","#574","mendixlabs/mxcli#1198"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/types","date":"2026-11-05","symptom":"`create external entities` from a $metadata that declares <edmx:Include Namespace=\"Org.OData.Capabilities.V1\" Alias=\"Capabilities\"/> and writes Term=\"Capabilities.InsertRestrictions\": the entity imported as not creatable / deletable, and mx check 11.14 reported 4 x CE6630 (\"marked Creatable=True in the OData service, but False in the app\") plus CE0852 / CE9435 on the flows writing it (ako/mxcli#1977)","cause":"ParseEdmx matched every annotation term by its full namespace (`case \"Org.OData.Capabilities.V1.UpdateRestrictions\"`) and never read edmx:Reference/Include aliases, so an aliased term matched nothing and was skipped silently — 12 terms (8 Capabilities, 4 Core) across entity sets, schema-level Annotations blocks, entity types and properties","file":"mdl/types/edmx.go (normalizeAnnotationTerms, xmlReference/xmlInclude, Schema Alias); test mdl/types/edmx_vocabulary_alias_test.go","insight":"Normalise at the boundary, once: rewriting each aliased term to its full namespace right after Unmarshal fixes every consumer, the six switch sites and any added later, where teaching each switch about aliases would leave the next one to forget. An undeclared prefix is left as written (the control), so nothing is guessed. A contract fixture is a local file next to the project (MetadataUrl: 'x.xml'); measured on fresh 11.14.0: base 6 errors, fix 0. Updatable stays No by design — Mendix's own Updatable does not follow UpdateRestrictions (cmd_contract.go), and mx check agrees.","refs":["ako/mxcli#1977","ako/mxcli#1939"]}
6 changes: 3 additions & 3 deletions .claude/skills/mendix/write-workflows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,9 @@ the same function — and the refusal names the fragment that fits the target.
branch cannot write a wrong type; it leaves an ordinary build error (`CE6686`)
rather than an unloadable project. `path n` addresses a **parallel split** only
— on a user task it used to delete the n-th outcome (ako/mxcli#791) and is now
refused; drop a user task's outcome by its value. `drop X boundary event` names
no event, so on an activity with several it is refused under `mdl 1` (under
`mdl 0` it drops the first and warns `MDL-V1-BOUNDARYDROP`).
refused; drop a user task's outcome by its value (refused if two share it).
`drop X boundary event 2` drops the second as `describe` lists them; with no number
and several events it is refused under `mdl 1` (`mdl 0` drops the first, warns `MDL-V1-BOUNDARYDROP`).

The old per-action statements (`alter workflow M.W set display 'X';`, `set
activity X page …`, `insert outcome 'N' on X { }`, `drop path 'Path 2' on X`)
Expand Down
Loading
Loading