Sync mxcli: MDL check↔mx-check parity + pluggable-widget write-path fixes - #800
Merged
Conversation
Ledger findings #17–19: a `set $x = <expr>` value re-serialized from the AST silently corrupted the stored Mendix expression — `mxcli check` passed, `mx check` gave only a generic CE0117: - #17 a division's right operand lost its `$` (`$a / $b` → `$a/b`) - #18 a decimal literal lost its fraction (`2.0` → `2`), breaking Decimal - #19 a small decimal became scientific notation (`0.000001` → `1e-06`) shouldPreserveExpressionSource now keeps the raw source whenever the expression contains a `/` or a decimal literal (a `.` adjacent to a digit), mirroring the existing XPath-where handling — so the exact text survives instead of a lossy AST round-trip. (MDL division is `div`; `/` is the member-access separator, which is what caused the `$`-loss mis-parse — preserving source is the robust fix either way.) Test: TestShouldPreserveExpressionSource_DivisionAndDecimals; repro mdl-examples/bug-tests/ledger-17-19-expression-serialization.mdl. Symptom table updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Ledger finding #16: year() and month() were in the built-in function whitelist, so `mxcli check --references` passed, but Mendix has no such functions and the build then failed with CE0117. Remove both so the error is caught at check time; the correct approach is date formatting/parsing (parseInteger(formatDateTime($d,'yyyy'))). Flagged the sibling extraction names (dayOfYear/hour/minute/…) as unverified-and-likely-invalid in a comment rather than removing them blind — they need a Studio Pro build to confirm, and rejecting a valid function is worse than the current state. Test: TestFuncReturnKind_NoYearMonth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Ledger findings #12 & #13 — the MDL spec docs contradicted the grammar and the (correct) generate-domain-model skill: - #12: `DELETE_CASCADE` is not a grammar keyword (the parser wants CASCADE / DELETE_AND_REFERENCES / DELETE_BUT_KEEP_REFERENCES / DELETE_IF_NO_REFERENCES / PREVENT). Replaced every `DELETE_CASCADE` with `CASCADE` and listed the full valid set. - #13: the association section's property table said "from = Parent (owner/many)" while its examples did `from Customer to Order` — the reverse of reality. An association goes `from` the foreign-key owner (the "many" side) `to` the referenced entity (the "one" side); getting it backwards passes `mxcli check` but fails the build with CE0854. Corrected the table, the syntax template, and all examples to `from Order to Customer`, with a note on the counter-intuitive direction. Files: docs/05-mdl-specification/{03-domain-model,01-language-reference}.md. Verified CASCADE + corrected direction parse via `mxcli check`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Ledger finding #24: on the modelsdk engine, a navigationlist item was stored with an empty name, and the caption's generated DynamicText had no name either — Studio Pro rejected the project with CE7247 "name cannot be empty" and CE0495 "duplicate name ''". navListItemToGen now (1) writes the item Name — the gen NavigationListItem type has no typed Name setter, so it's added as a raw property via addStr, mirroring the legacy writer's `Name` key — and (2) names the caption's DynamicText `text_<item>` (same as legacy) and sets its render mode. Test: TestNavListItemToGen_WritesNames (encode → both names present). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…ecimals Corrects the earlier ledger #17-19 fix, which over-broadly preserved raw expression source whenever it saw a '/'. In MDL '/' is the member-access separator (`$obj/Attr`), not division (that is `div`), so preserving on '/' source-froze every association-navigation path and regressed TestAssociationNavParsing. - shouldPreserveExpressionSource: drop the '/' trigger; keep the decimal-literal trigger (findings #18/#19 — `2.0`->`2`, `0.000001`->`1e-06` are genuine AST-serializer losses that raw-source preservation fixes). - New MDL045: walk the microflow expression tree for a BinaryExpr with operator '/' (`$Dec / 2`, `(...) / $x`) and reject with an actionable "use div" message, instead of silently writing an invalid expression that fails the build (CE0117). The bare `$a / $b` form degrades to a member path and is caught by `check --references`. Tests: TestShouldPreserveExpressionSource_Decimals (renamed), TestValidateMicroflow_SlashDivision. Repros: ledger-17-19 (passes) + ledger-17-slash-division.fail.mdl (MDL045). Skill + symptom-table updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…MDL-WIDGET14) A template-parameter slot is a data binding (an attribute path), not a client-side expression: mxcli stored any unquoted contentparams/captionparams value as an attribute path, so a function call like `formatDateTime($obj/LastImport, 'd MMM yyyy')` became a bogus attribute name and Studio Pro rejected the page with CE1613 "attribute no longer exists". Add MDL-WIDGET14 (validateTemplateParamExpressions, called from validateStaticWidget): for each ContentParams/CaptionParams value, skip quoted string literals, then flag a function call or arithmetic/comparison operator as an expression — an attribute path never matches. The message tells the user to precompute the value onto the bound entity (a calculated attribute) and bind that attribute instead. Test: TestValidateTemplateParamExpressions. Bug-test: contentparam-expression-rejected.fail.mdl. Symptom-table updated. Ledger #26. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…otchas Three check-time hints for constructs that pass `mxcli check` but surprise at build or render time (ledger findings #21/#25/#27): - MDL046: dateTime()/dateTimeUTC() with a non-literal argument (CE0117 — these build from hardcoded constants only). Hint: step off a literal anchor with addDays()/addMonths(), which take variables. - MDL047: an association compared to `empty` in a retrieve constraint (`[Module.Assoc = empty]` → CE0161 — `= empty` tests attributes, not associations). Hint: `[not(Assoc/Target)]`. A bare attribute and an attribute-over-association test are correctly not flagged. - MDL-WIDGET15 (info): two or more adjacent dynamictext siblings, which Mendix renders inline (concatenated, no separator) regardless of RenderMode. Info severity — advisory only, never fails the build. Finding #20 (division operand typing) is covered by the existing MDL041 (div→Decimal) and the new MDL045 (`/`-as-division) rather than a separate rule that would contradict CLAUDE.md's documented "integer div yields Decimal". Tests: TestValidateMicroflow_DateTimeLiterals, _XPathAssociationEmpty, TestValidateConsecutiveDynamicText. Repros in mdl-examples/bug-tests/. Skills + symptom table updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Re-test of PR #52 by the ledger project surfaced three gaps: - #17 (MDL045): the variable/variable division form `$Dec / $Dec2` was not caught — it parses as a member-access path, so the BinaryExpr walk missed it. The visitor now narrowly preserves source when a `/` is immediately followed by `$` (a real association path never has `$` after `/`, so legit navigation is untouched), and MDL045 flags a source-preserved AttributePathExpr whose `/ $` source matches (`exprIsSlashDollarDivision`). The `$` guard on the RHS is the reliable division-vs-navigation discriminator the tester suggested. - #25 (MDL047): the check only saw microflow `retrieve` constraints, so an association `= empty` in a page/widget datasource where-clause slipped through. Now `validateDatasourceXPathAssociationEmpty` inspects `DataSourceV3.Where` on every widget, sharing the detection via `xpathAssociationEmptyMatches`. - #27 (MDL-WIDGET15): the advisory false-positived on heading+subtitle pairs. Only `Text`/unset RenderMode is inline; H1–H6/Paragraph are block-level and do not concatenate. `inlineDynamicText` now excludes block modes, so a heading breaks the run. Tests updated (variable/variable division, page-datasource MDL047, heading exclusion). Repros: ledger-17-slash-division.fail.mdl (+ var/var form), new ledger-25-page-datasource-assoc-empty.fail.mdl. Symptom table refreshed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Verification round found that the #27 narrowing wrongly treated `Paragraph` render mode as block-level. On Mendix 11.12.1 + Atlas a Paragraph-mode dynamictext renders as an inline `<span>`, so two adjacent Paragraph widgets fuse (`PARA-ONEPARA-TWO`) exactly like Text-mode ones — and the advisory was silently missing them. `inlineDynamicText` now treats ONLY H1–H6 as block-level (`headingRenderModeRe`); Text/unset and Paragraph are both inline. The advisory message is corrected to recommend a heading RenderMode (not Paragraph, which does not fix the problem). Tests updated: Paragraph+Paragraph and Paragraph+Text now flagged, heading pairs still excluded. Repro ledger-27 extended to show all three cases. Symptom table notes the lesson: verify Mendix render behavior empirically — a name like "Paragraph" does not imply `display: block`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
- #37: write-oql-queries.md RULE 2 said "NEVER use ORDER BY or LIMIT in a view", but mxcli's MDL030 *requires* ORDER BY to be paired with a LIMIT (ORDER BY alone → CE0174; ORDER BY + LIMIT builds clean). Reworded RULE 2 + the Mistake 8, Step 9, and checklist entries to match the tool, and documented UNION/UNION ALL as a supported construct (it worked end-to-end but was undocumented). - #32: MDL001 (nested-loop) reworded — it now distinguishes a key LOOKUP (use FIND) from intentional aggregation that must visit every element (correct as written), so it stops reading as "your loop is wrong" on report pivots. - #31: documented that a variable first created inside an if/else arm (including by `$Var = call ...`) is scoped to that arm — declare before, assign in each. - #34: run-local.md notes the Dart Sass `rgba()` comma-list trap (a theme compile error fails the build step pre-runtime). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
A pass-through view column (a bare source-attribute ref like `select c.Name as X`) inherits the source attribute's length verbatim, and Mendix requires the declared length to match EXACTLY — declaring an unbounded `string` (or a different `string(N)`) against a `string(100)` source fails the build with CE6770 "View Entity out of sync". MDL031's length check previously covered only DERIVED columns (cast/case → 200); the references-mode validator compared pass-through strings with `typesCompatible`, whose `>=` rule only guards truncation, so the mismatch slipped through. `validateViewEntityTypes` now checks pass-through string columns with `passthroughStringLengthMismatch` (exact match), reporting the source entity/attr and inherited length. `col.SourceAttr` is set only for a bare attribute ref, so aggregates/derived expressions are unaffected. References-mode only (needs the domain model to resolve the source length). Test `TestPassthroughStringLengthMismatch`; example mdl-examples/bug-tests/ledger-36-view-passthrough-length.mdl. Verified on a real Mendix 11.12.1 project: `string`→CE6770 flagged, `string(100)`→clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Two entity-correctness fixes from the ledger project, both verified end-to-end on Mendix 11.12.1. #41 (CE6771 check gap): `create association from/to <ViewEntity>` passed `mxcli check --references` and exec, but `mx check` rejects it — associations to/from view entities are impossible. The reference checker now resolves both endpoints and rejects when either `isViewEntity` (new helper), naming it and pointing at a non-persistent alternative. Both directions caught. #39 (mx check crash): a `create or modify entity` dropping an indexed attribute left the index orphaned (its column referenced a removed GUID), and `mx check` CRASHED loading the project with an unhandled AggregateException. Two parts: - executor `reconcileDroppedIndexes`: when the statement lists no indexes, carry existing ones forward, pruning columns for dropped attributes (attr IDs survive by name) and dropping empty indexes — fixes partial drops (index(A,B)→index(A)). - backend `UpdateEntity`: an untouched empty Indexes PartList is "clean", so the codec passes the raw orphan through; force it dirty (add+remove) so the codec re-emits an empty list, clearing the raw — fixes the all-removed case. Verified: `mx check` → 0 errors (previously crashed). Tests: TestIsViewEntity, TestReconcileDroppedIndexes. Examples + symptom table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Two check gaps confirmed with mx check on Mendix 11.12.1 — both pass mxcli check but fail the build: - MDL048 (#42): `retrieve … where [id = $Var]` → CE0161. Mendix XPath has no id operator reachable from a microflow expression. Regex a word-boundaried `id` before a comparison (a user attribute can't be named `id`). Hint points at a GUID marketplace action OR exposing the id as a String on a view entity via `cast(id as string)` and constraining on that column. - MDL049 (#43/#44): a call argument bound to an association-object path (`B = $Edit/M.Edit_Budget`) → CE0117. An association path isn't a value; it must be retrieved first. Flags an AttributePathExpr whose final segment is module-qualified (an association → object); an attribute value over the same association (`…/Name`) is not flagged. Tests: TestValidateMicroflow_XPathIdConstraint, _AssociationObjectArg. Repros + symptom table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
#45 (alter-page.md): document three ALTER PAGE limitations the ledger project hit, with the recommended "bind at page creation" pattern — SET cannot rewire a button's action; REPLACE cannot reuse a widget name from the subtree being replaced (built before the old is removed → collision); a footer is a marker and is not addressable by its author-given name (serialized as footer1). These are design constraints, so the guidance is to define save/reset microflows before the page and bind buttons at creation rather than altering the page afterward. #46 (theme-styling.md): DataGrid2 renders role=grid/row/gridcell <div>s, not table/tr/td — CSS and Playwright must target the ARIA roles; and column `Size` is a flex weight, not pixels — set a min-width on [role='grid'] with overflow-x:auto. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
… ledger cases Wave 1 (MDL-WF01/02/03, MDL-BUTTON01 — cases 1/2/4/5) is shipped. Record the ~10 ledger-driven checks added on the same pattern (MDL045-049, MDL-WIDGET13/14/15, MDL031 pass-through, view-entity association CE6771) plus the two write-path parity fixes (#24 navlist names, #39 orphaned index), and note the two originally- cataloged gaps that remain: case 3 (CE7412) and the standalone case-6 warn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The CE7412 user-task-page-context check is FP-prone and needs the real trigger + FP boundary verified against `mx`. No current test app uses workflows, so record the deferral rather than implement it blind. Pick up when a workflow app enters the test rotation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Ledger finding #48 reported "association navigation must be the whole RHS or it fails CE0117". Verification against mx check (11.12.1, ~15 isolation cases) disproved that rule — `'x' + $obj/Assoc/Attr`, `toString(..) + $obj/Assoc/Attr`, and a bare `set $x = $obj/Assoc/Attr` all build clean. The real trigger is narrower: a RENDER function (formatDateTime/formatDecimal/…) that takes, or co-occurs with, an association-navigated value: formatDateTime($obj/Mod.Assoc/Date, 'd MMM') -> CE0117 formatDateTime($obj/Date, 'd MMM') + $obj/Mod.Assoc/Name -> CE0117 while `formatDateTime($obj/Date, …)` (plain member access) is fine. MDL050 flags a set/declare/return value that contains BOTH a `format*`-family call AND an AttributePathExpr with a module-qualified (association) segment. The curated function set (formatDateTime/DateTimeUTC/Decimal/Float/Boolean) means no false positives — toString/length + assoc-nav build clean and are not flagged; an untested render fn is a conservative miss. Fix: materialize the associated value into a variable first. `make check-mdl` shows no existing example regressed. Test: TestValidateMicroflow_FormatWithAssociation. Repro: ledger-48-format-with-association.fail.mdl. Symptom table records the corrected rule + the lesson (reproduce the passing neighbours, not just the failing case). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
A `break` nested inside an if/case within a loop serializes a dangling
sequence-flow reference — `mx check` then CRASHES loading the project with an
unhandled System.AggregateException ("key … not present in the dictionary"), an
unrecoverable failure (same severity class as #39). A break placed directly in
the loop body serializes fine; only the useful `if <cond> then break` form is
affected.
The underlying flow-graph serialization bug is still open (a write-path fix).
MDL051 is the interim guard: on a LoopStmt, flag a break nested in an
if/case/inheritance-split (not descending into nested loops, which trap their own
break). Since the pattern currently CRASHES, a check-time rejection is a strict
improvement. Fix for the user: a guard variable (declare $Done Boolean = false; …
set $Done = true) — verified clean on mx check (11.12.1).
Test: TestValidateMicroflow_ConditionalBreak. Repro:
ledger-52-break-in-conditional.fail.mdl. Symptom table notes the real fix is the
write-path serialization; MDL051 is the interim.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`contains` is overloaded — the LIST operation contains(list, object) and the STRING function contains(haystack, needle) — but the visitor unconditionally rewrote `set $x = contains(...)` into a ListOperationStmt, dropping literal arguments entirely. String contains was therefore unusable from MDL: a pre-declared Boolean output collided (CE0111 "Duplicate variable name"), and a String input to a list operation is otherwise rejected (CE0023/CE0097). Two-part fix: 1. Visitor (buildSetStatement): a literal or computed argument means the string function unambiguously — fall through to MfSetStmt (a Change Variable value expression) instead of a lossy list operation. Only the both-plain-variables form stays a ListOperationStmt (kind unknown at parse time). 2. Flow builder (addListOperationAction) + validate pre-pass: when the input variable is a declared String, emit a Change Variable action carrying `contains($In, $Second)` rather than a ContainsOperation, and validate the target as a pre-declared SET (no CE0111 duplicate). The genuine list form (list variable + object variable) is unchanged. Verified with `mx check` on a fresh Mendix 11.12.1 project: 0 errors for the literal-arg, two-String-variable, and genuine-list forms. Tests: TestContainsOverloadParsing (visitor), TestBuildContains_StringVsList (builder). Example: mdl-examples/bug-tests/ledger-53-string-contains.mdl. Symptom row added to fix-issue.md; string-vs-list contains documented in write-microflows.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
A datagrid column with an explicit empty caption (`Caption: ''`) passed
`mxcli check` but `mx check` rejected the page with CE0463 "The definition
of this widget has changed" on the Data grid 2. Omitting the caption, or
giving it a non-empty string, both built cleanly.
Root cause: the pluggable widget engine's column-header fallback
(applyColumnHeaderFallback) treated a present-but-empty header property as
"has header" and skipped the attribute-name default — so an explicit
`Caption: ''` emitted an empty header (which Studio Pro rejects), while an
absent caption got the fallback. The keyword datagrid path already handled
this via `if caption == "" { caption = col.Attribute }`.
Fix: detect an empty header (a texttemplate op with empty TextTemplate and
no Parameters) and treat it like an absent one — fill it in place with the
bound attribute's leaf name rather than leaving it empty or appending a
duplicate `header` property. A header with params (`Caption: '{1}'`) or a
column with no bound attribute is left untouched.
Verified with `mx check` on a fresh Mendix 11.12.1 project: 0 errors
(previously CE0463). Test TestApplyColumnHeaderFallback (cases 5/6);
example mdl-examples/bug-tests/ledger-54-empty-column-caption.mdl.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…ledger #57) A plain `commit $Obj;` updates committed attribute values in a grid, but a grid backed by a database datasource does not re-run its sort — so after a reorder that rewrites a sort key, the row keeps its old position until `commit $Obj refresh;` re-queries the datasource. Added the specific gotcha to the commit best-practice note in write-microflows.md; the generic refresh note didn't capture the re-sort case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
MDL045 flagged `$a / $b` division only when it was the entire set-value expression. When the division was embedded in a larger expression — `$a / $b + 1`, `round($a / $b)`, `$a / $b * 100` — the `$a / $b` part degrades to a member-path AttributePathExpr nested under a BinaryExpr or FunctionCallExpr, and the check silently missed it (mxcli check passed but mx check fails with CE0117). Only the bare form and a chained `$a / $b / $c` were caught. Root cause: exprIsSlashDollarDivision matched only when the source-preserved SourceExpr *directly* wrapped an AttributePathExpr, so a division nested one level down slipped through. Fix: replace it with exprHasSlashDollarDivision, which walks the whole expression tree for any SourceExpr and scans its raw source for a `/ $` (slash before a variable) using a string-literal-aware scanner. A real member/association path never writes `/$`, and the scanner skips `/$` inside quoted literals (e.g. 'path/$x') to avoid false positives. The division is now caught wherever it appears — in additions, multiplications, function arguments, return values, and if-conditions. Tests: TestValidateMicroflow_SlashDivision extended with embedded cases and the string-literal guard; ledger-17-slash-division.fail.mdl exercises the embedded forms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…edger #48 root cause) `$T/Mod.Assoc/Attr` in a microflow expression (Change Variable, if-condition, return) passed mxcli check but mx check rejected it with CE0117. Mendix expression syntax needs the target-entity step in the path (`$T/Mod.Assoc/Mod.Entity/Attr`), which the builder inserts via resolveAssociationPaths — but two bugs skipped that insertion: 1. shouldPreserveExpressionSource treated a `.` preceded by a digit as a decimal literal, so a module/entity name ending in a digit (`L48.`, `Account2.` — very common) froze the whole expression's source and bypassed association resolution, writing `$T/L48.Assoc/Attr` with no entity step. 2. A legitimately source-frozen expression (a real decimal literal or preserved whitespace) also bypassed resolution, so an association nav co-occurring with a decimal (`$T/Mod.Assoc/Bal + 2.0`) broke even for non-digit modules. Fixes: 1. A `.` is a decimal point only when its adjacent digit run is a standalone number — not when the preceding token contains a letter or underscore (a qualified-name segment). New dotIsQualifiedNameSeparator distinguishes `L48.Transaction` (name) from `2.0` (decimal). 2. For a source-preserved expression, rewrite association paths inside the raw source: for each AttributePathExpr, compare its rendered form before and after resolution and ReplaceAll in the source when they differ. This keeps decimal/whitespace fidelity while inserting the entity step. Verified with mx check on Mendix 11.12.1: 0 errors for plain attribute-over-association navigation, an if-then-else enum comparison over an association, and an association nav mixed with a decimal literal — all previously CE0117. Example: mdl-examples/bug-tests/ledger-48-assoc-nav-digit-module.mdl. Test: TestShouldPreserveExpressionSource_Decimals extended with digit-ending qualified-name cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
MDL050 flagged expressions combining a format function with an association navigation (e.g. formatDateTime($obj/Mod.Assoc/Date, …)) as CE0117. Once the real #48 root cause was fixed — association navigation dropped its target-entity step — re-verification against mx check on 11.12.1 showed the rule's premise was wrong on both of its cases: - formatDateTime($obj/Assoc/Date, …) now builds CLEAN (it only failed because the entity step was missing, which affected ALL association navigation, not just inside format calls). MDL050 was rejecting valid code. - formatDecimal($x, 2) fails CE0117 even on a PLAIN local decimal with no association — a formatDecimal-signature bug, unrelated to associations. So the "format-function + association" correlation was an artifact of two independent bugs, neither about format functions. Removed the check, its call sites, its test, and its bug-test. The remaining real issue — formatDecimal($x, precision) failing CE0117 regardless of associations — is a separate open finding (likely a wrong function signature/arity) to investigate and fix in the executor or the function-name checker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`set $At = find($Raw, '"id":"')` — the string function find(haystack, needle) returning a substring index — was serialized as a List operation activity 'Find by expression'. A list op creates its output variable, so a pre-declared `$At` collided: mx check reported CE0111 "Duplicate variable name". String find() was unusable from MDL — the same overload trap as #53 (contains). Two-part fix (mirrors #53): 1. Visitor: a string-literal second argument is unambiguously the string function (you never filter a list by a bare string literal) — fall through to MfSetStmt. A boolean condition or both-plain-variables stays a ListOperationStmt. 2. Flow builder + validate pre-pass: when the input variable is a declared String, emit a Change Variable carrying `find($in, <arg1>)` instead of a list FindOperation, and validate the target as a pre-declared SET (no CE0111). The contains/find String-input handling now shares one branch. The genuine list form find(list, condition) is unchanged. Verified with mx check on Mendix 11.12.1: 0 errors for the string-literal, two-String-variable, and genuine-list-condition forms. Tests: TestFindOverloadParsing (visitor), TestBuildContains_StringVsList find cases (builder). Example: mdl-examples/bug-tests/ledger-63-string-find.mdl. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Two loops in the same microflow reusing the same iterator name (`loop $R in … end loop; loop $R in … end loop`) passed mxcli check but mx check failed with CE0111 "Duplicate variable name 'R'." at Loop. A Mendix loop iterator is scoped to the whole microflow, not to its loop, so the second loop re-creates an existing variable. New MDL052: walk the microflow body (recursing through if/case/while/loop bodies) collecting LoopStmt iterator names and flag any name used by a second loop. Catches sequential and nested reuse. Distinct iterators, a single loop, and the same name across different microflows are fine. Test: TestValidateMicroflow_DuplicateLoopVariable. Repro: mdl-examples/bug-tests/ledger-64-duplicate-loop-variable.fail.mdl. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
An action property on a pluggable widget — e.g. `onClick: microflow …` on a DataGrid2 — passed mxcli check --references and mx check, built and ran, but was silently discarded: DESCRIBE omitted it and the model round-tripped minus the action, with no error or warning. Root cause: the .def.json generator never emitted an `action` operation for `type="action"` properties (across the generated defs, no action operation existed at all). The writer reads only propertyMappings from the def, so with no action mapping it had nothing to write. The .mpk correctly declares the action slot — the gap is purely in generation. Fix: emit an `action` PropertyMapping for the action slots MDL can author — `onClick` → source OnClick (the widget's Action property / onClick: alias) and filter `onChange` → source OnChange (new resolveMapping case). The engine's existing applyOperation "action" → SetAction path (nil-guarded) then serializes the ClientAction with its parameter mapping. Non-authorable action slots (DataGrid2 onSelectionChange/onConfigurationChange) return "" from actionSourceForKey, so no mapping is emitted. WidgetDefGeneratorVersion bumped 12→13 so existing projects regenerate their defs. Verified end-to-end with mx check on Mendix 11.12.1: 0 errors, and the DataGrid2 persists the onClick microflow action with its Thing: $currentObject mapping. (The DESCRIBE read path still omits pluggable-widget actions — a separate read gap like #57/#58; the write bug is fixed.) Test: TestGenerateDefJSON_ActionMapping. Example: mdl-examples/bug-tests/ledger-67-pluggable-widget-action.mdl. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…s#67 read gap) After the mendixlabs#67 write fix, DESCRIBE PAGE still omitted a DataGrid2's onClick action, so a describe round-trip silently lost it. The datagrid2 describe path read datasource + columns + paging but never read the widget-level action, and the param-aware renderClientActionMDL reader (used for button/onchange) was never called for a pluggable widget's action. New customWidgetPropertyActionMap returns the raw Forms$*ClientAction map for a CustomWidget property (NoAction → nil). The datagrid2 parse branch renders it via renderClientActionMDL (param-aware) into rawWidget.OnClick; the output branch emits `onClick: <action>`. Verified end-to-end: describe → re-exec → mx check 0 errors, and the round-trip preserves the full `onClick: microflow L67.OnRowClick(Thing: $currentObject)` including the parameter mapping. Test: TestCustomWidgetPropertyActionMap. (Gallery and other pluggable widgets with an action can be wired the same way — datagrid2 is the verified case.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…xlabs#67 CustomChart) Finding mendixlabs#67 was reported on the Charts CustomChart widget. My earlier describe read-gap fix wired only the datagrid2 branch; CustomChart is described through the generic `pluggablewidget '…'` path (!isKnownCustomWidgetType), so its onClick was still omitted by DESCRIBE even though the write path persisted it. Wire the onClick read into the generic pluggable branch too: read the action map via customWidgetPropertyActionMap, render it with renderClientActionMDL (param-aware) into rawWidget.OnClick, and emit `onClick: <action>` in the generic pluggablewidget output. The output guard also fires on w.OnClick != "" so an action-only widget still gets its pluggablewidget header. Verified end-to-end on the actual CustomChart widget (Mendix 11.12.1): mx check → 0 errors; the CustomChart persists the onClick microflow action with its Data: $currentObject mapping; and describe → re-exec → mx check round-trips the onClick (the param name re-parses as a quoted identifier). Example: mdl-examples/bug-tests/ledger-67-customchart-action.mdl. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
guard) A real widget property the generator doesn't map to a write path could be silently dropped (or, via an alias, slip through) with no diagnostic — the same failure mode as mendixlabs#67 for any unmapped property type (expression, icon, a future type, an action slot with no MDL surface). The WidgetDefinition.KnownProperties field and the MDL-WIDGET06 "recognized but not persisted" check already existed, but the generator never populated KnownProperties, so the warning never fired: an unmapped property either false-errored as MDL-WIDGET01 "no such property" or slipped through silently via an alias. Populate KnownProperties purely from the two artifacts mxcli already has: every .mpk-declared property key with no mapping in the generated def (subtract PropertyMappings + aliases + ChildSlots + ObjectLists + mode mappings from the full key set). No per-widget knowledge. The existing check then warns "recognized but not persisted — the value will be dropped." Three-way discrimination verified on DataGrid2 (11.12.1): a mapped property (onClick) is silent, a real-but-unmapped one (rowClass) warns MDL-WIDGET06, a truly-unknown one (totallyBogusProp) still errors MDL-WIDGET01. WidgetDefGeneratorVersion bumped 13→14 so existing projects regenerate. This is the tester's suggested general check — it catches the whole class without guessing which widgets support what; the action mapping remains the specific fix for onClick. Test: TestGenerateDefJSON_KnownPropertiesUnmapped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
… round 2) The first #54 fix handled attribute-bound columns (fall back to the attribute name), but left a custom-content column — a column with child widgets / an actions button and NO bound attribute — untouched: it had nothing to derive a header from, so an empty `Caption: ''` OR an absent caption still tripped CE0463. A custom-content column requires a non-empty header. Fix: applyColumnHeaderFallback now falls back to the column's own NAME when there's no bound attribute, and the whole fallback is gated on the item template actually having a `header` slot (checked via mapping.ItemProperties) so header-less object-list items (chart series, accordion groups) are never given a spurious header. Verified on Mendix 11.12.1: mx check → 0 errors for a custom-content column with an empty caption AND with no caption (both previously CE0463); attribute columns and chart series are unaffected. Test: TestApplyColumnHeaderFallback (cases 1–8, incl. custom-content). Example: mdl-examples/bug-tests/ledger-54-custom-content-column.mdl. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Fix ledger project findings: expression/XPath/widget checks + doc fixes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges
ako/mxcli:mainintomendixlabs/mxcli:main.Closes a batch of ledger findings where
mxcli checkpassed but realmx check/ MxBuild rejected the model (verified against the bundledmxon Mendix 11.12.1, 0 errors each).Microflow expression / check parity
in a larger expression (MDL045).
.in a qualified name ending in a digit (e.g.L48.Transaction) is no longer mis-parsed as a decimal; associationpaths inside preserved source get the target-entity step inserted.
contains/findoverload resolution: String-typedinput compiles to a ChangeVariable expression instead of a list op,
fixing CE0111 on
set.Pluggable widgets (write path + DESCRIBE read)
header: attribute-bound columns fall back to the attribute, custom-content
columns fall back to the column name (gated on the item template having a
headerslot). Fixes CE0463.actionproperty mappings(onClick/onChange), so actions on pluggable widgets (Datagrid, CustomChart)
survive to the MPR; stale defs regenerate via the generator-version stamp.
DESCRIBE renders these actions back for both datagrid2 and generic
pluggable widgets.
widget definition but has no mapping in the generated def is warned as
"will be dropped."
Docs / tests
mdl-examples/bug-tests/for each finding..claude/skills/fix-issue.mdandwrite-microflows.md.make build && make test && make check-mdlall green.