Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .claude/skills/fix-issue/findings/mdl-executor.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -545,3 +545,5 @@
{"area": "mdl/executor", "date": "2026-09-05", "symptom": "`UPDATE SECURITY` was inert on every MPR v2 project: it returned on the first module it could not reconcile, and System always is one \u2014 `Error: failed to reconcile security for module System: load domain model 00000000-...-002: .../mprcontents/00/00/...002.mxunit: no such file or directory`. Reported as \"UPDATE SECURITY does not fix the CE0066 it exists to fix\" (mendixlabs/mxcli#1047). A second defect in the same command: `update security RestLab` (without IN) parsed cleanly, dropped the module name, and ran project-wide.", "cause": "execUpdateSecurity returned mdlerrors.NewBackend on any ReconcileMemberAccesses failure. System's domain model is SYNTHESIZED rather than stored (no unit file behind the id the module carries), so loadDomainModelGen can never read it. Now System is skipped by name \u2014 reconciling it is not merely impossible but wrong, its access rules are the platform's \u2014 and any other unreadable module is reported and stepped over. The grammar's `(IN qualifiedName)?` became `(IN? qualifiedName)?`, so a bare module name scopes instead of reaching ANTLR's error recovery.", "file": "`mdl/executor/cmd_security_write.go` (execUpdateSecurity), `mdl/grammar/domains/MDLSecurity.g4`; tests `mdl/executor/cmd_security_update_test.go`; example `mdl-examples/bug-tests/update-security-runs-at-all.mdl`", "insight": "Two different System failures live in this codebase and conflating them wastes a diagnosis \u2014 I did it once in this same session. GetModuleByName/GetDomainModel answer for System perfectly well (System.FileDocument loads with its 6 attributes), which is why inherited-member resolution works; loadDomainModelGen, which reads the UNIT BY ID out of mprcontents, cannot, because System has no file there. Same module, opposite answers, different call path. The other half is the more interesting bug class: `update security RestLab` was not a parse error but a SILENT SCOPE ESCALATION \u2014 the user asked to touch one module and the command touched all of them, with `mxcli check` reporting Syntax OK. Worth looking for wherever an optional keyword precedes an optional operand. Finally, the report's stated precondition (adding an attribute leaves one CE0066) does NOT reproduce: measured on 11.13.0, `alter entity ... add attribute` on both engines and a whole-entity rewrite each left the project at 0 errors, because every mxcli write path already reconciles. So the command could not be shown repairing a real CE0066 end to end \u2014 the integration example says so explicitly rather than implying coverage it does not have."}
{"area": "mdl/executor", "date": "2026-09-05", "symptom": "A datagrid whose datasource is an association path at PAGE level typed its rows as the entity it navigates AWAY from: `datagrid gA (datasource: $Customer/Bench.Order_Customer)` bound its columns against Customer, giving [CE1613] \"The selected attribute 'Bench.Customer.OrderNo' no longer exists.\" at Columns (1/1). The SAME path inside a data view was correct (mendixlabs/mxcli#1045).", "cause": "resolveAssociationDestination picks the end opposite the context, and the context passed was pb.entityContext \u2014 the ENCLOSING data container's entity. At page level nothing encloses the widget, so it is empty, neither end matches, and the function's last-resort fallback (\"default to the child (TO) side\") returns the entity the grid started from. Fixed by resolving from the NAMED context variable when there is one: `$Customer/\u2026` traverses from whatever $Customer holds, enclosed or not. pb.paramEntityNames already knew it.", "file": "`mdl/executor/cmd_pages_builder_v3.go` (the \"association\" branch of buildDataSourceV3); tests `mdl/executor/cmd_pages_builder_assoc_pagelevel_test.go`; example `mdl-examples/bug-tests/assoc-datasource-page-level.mdl`", "insight": "The report's own diagnosis was the useful part and was right: it noticed the same path worked INSIDE a data view and failed at page level, which localises the bug to the context rather than to the association logic. Worth generalising \u2014 a resolver that takes an ambient context is correct exactly where the ambient context exists, and its fallback is what runs everywhere else; that fallback had a plausible comment (\"matches the common FROM=context pattern\") and was silently wrong for the whole page-level case. Two measurement notes: check-mdl cannot catch this at all, because it only runs `mxcli check` and the defect is in what the WRITER stores \u2014 the regression net had to be exec + mx check over the six examples that use an association datasource (five clean, the sixth failing identically before and after on an unrelated CE0106). And the reverse-direction control initially failed for the wrong reason: traversing a Reference from its FROM end yields ONE object, so a grid over it is [CE8812] \"A grid association path must result in a list\" \u2014 a cardinality complaint, not a resolution one, and the control had to become a data view to test what it claimed to test."}
{"area": "mdl/executor", "date": "2026-09-06", "symptom": "Every mxcli-authored Image widget failed the build. On a project at 0 errors, one `image imgProbe (Image: '...', Responsive: false)` on a new page gives [CE0463] \"The definition of this widget has changed\u2026\" at Image 'imgProbe'. A field-level diff of Atlas' brand image against a describe -> rename -> exec copy of it differs in ONE line of 1480: `maxHeight` = '0' where Atlas stores '250' (mxcli-ledger FINDINGS \u00a7142). A previous fix (ee295467) that named maxHeight explicitly was in the shipped binary and changed nothing.", "cause": "hiddenUnnamedProperties default-values a hidden property MDL cannot name, and maxHeight IS hidden (\"hidden when maxHeightUnit = none\"). Its condition property `maxHeightUnit` is unmapped too, so widgetValueMap does not know it, and the fallback read the DECLARED default \u2014 which Image 1.6.0 states as \"pixels\". Condition false -> maxHeight read as visible -> no reset -> the template's captured 0 stood. But maxHeightUnit being unmapped is exactly why the document gets the template's \"none\", not \"pixels\". Fixed by inserting the template's captured configuration (builder.PrimitiveValues(), read before any mapping is applied) between the script's values and the declared defaults in that fallback chain.", "file": "`mdl/executor/widget_engine.go` (hiddenUnnamedProperties' condition fallback + the Build call site); `mdl/backend/widgetobj/builder.go` (new PrimitiveValues, wrapping the existing primitiveValuesOf); `mdl/backend/mutation.go`, `mdl/backend/mcp/widget.go`; tests `mdl/executor/widget_hidden_reset_unmapped_test.go`; example `mdl-examples/bug-tests/image-widget-hidden-maxheight.mdl`", "insight": "The first fix passed a test built on an input that does not exist: its defaults helper declared maxHeightUnit's default as \"none\", and the real package declares \"pixels\" \u2014 the one value at which the rule fires. A fixture that encodes the value under test is not a fixture. The general shape: a visibility rule must be evaluated against the configuration that will be WRITTEN, never against the one the package declares, and the two diverge precisely on unmapped properties, which is the only place the rule matters. Two measurement traps cost time here. (1) `mxcli docker check` runs `mx update-widgets` first, which reconciles the widget and reports 0 errors while the stored value is still 0 \u2014 the same project reads 0 errors through it and 1 error through scripts/mx-check.sh. Use raw mx check for CE0463. (2) The ground truth was cheap and decisive once asked for: dumping all 69 Image widgets of a real project showed all 65 carrying a maxHeight store 250 at every combination of heightUnit/maxHeightUnit, and mxcli's was the sole outlier. Proven both ways by patching the stored 0 to 250 (1 error -> 0) and back (0 -> 1)."}
{"area": "mdl/executor", "date": "2026-09-05", "symptom": "describe navigation -> exec destroyed a menu item's glyph icon. Before: an item with Forms$GlyphIcon and a '-- not reproducible' comment. After exec of DESCRIBE's own output: no icon, no comment, exit 0, 'Navigation profile updated.'", "cause": "Mendix stores THREE icon elements (Forms$IconCollectionIcon{Image}, Forms$GlyphIcon{Code}, Forms$ImageIcon{Image}) and every layer handled only the first. The reader captured $Type and Image but never the glyph's Code, the AST/spec carried a bare name with no kind, and both writers emitted IconCollectionIcon unconditionally. Since CREATE OR REPLACE NAVIGATION is a full replacement, an icon the writer would not emit was an icon the statement DELETED.", "file": "mdl/executor/cmd_navigation.go", "insight": "The giveaway was already in the output and read as harmless: DESCRIBE printed '-- icon … is not reproducible by CREATE NAVIGATION; set it in Studio Pro'. That comment was written to make the loss VISIBLE, and it did — but only in the describe output, not in the exec that then acted on it. A note saying 'I cannot reproduce this' beside a FULL-REPLACEMENT statement is a note saying 'running this deletes it', and nobody made that connection. When a describer declines to emit something, check what the corresponding writer does with the omission. Fix shape: give each variant its own keyword (`icon glyph N` / `icon image QN`, bare = collection) so replay rebuilds the same ELEMENT — widening the bare form to cover all three would have converted an image icon into a collection icon, a silent variant swap. Also: preserve-when-silent was the right fix only while the construct was INEXPRESSIBLE; once authoring exists, omission becomes a real choice and preserving would make it impossible to remove a glyph icon.", "issue": "ako/mxcli"}
{"area": "mdl/executor", "date": "2026-09-05", "symptom": "A brand-new lint rule (MDL074, 'menu item specifies no icon') would have fired on the majority of real menus, reporting items that plainly HAVE an icon.", "cause": "The rule tested `item.Icon == \"\"`. A Forms$GlyphIcon carries a numeric Code and NO qualified name, so the obvious emptiness test calls a perfectly good icon absent. The fixture's own Home item is glyph 57377.", "file": "mdl/executor/validate_navigation_icons.go", "insight": "Caught by writing the rule and the authoring support in the same session — the round-trip test emitted `icon glyph 57377` and check then warned about it, which is what exposed it. A rule that asks 'is this field empty' about a polymorphic element is asking the wrong question: ask the KIND. Generalisable: when a model element has variants with different payload shapes, any emptiness test on one variant's payload is a latent false positive on the others.", "issue": "ako/mxcli"}
27 changes: 24 additions & 3 deletions .claude/skills/mendix/manage-navigation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,30 @@ create or replace navigation Responsive

### Menu Icons

Both `menu item` and `menu 'caption' (...)` take an optional `icon`. It is a
**qualified name** into an **icon collection** — a model reference, written like
every other reference in MDL, not a string:
**Give every menu item an icon.** It is optional in the grammar and `mxcli check`
warns when it is missing (**MDL074**), because the navigation sidebar collapses
to an icon rail and that is the state most users leave it in: a collapsed item
shows its icon, and one without falls back to the first few characters of its
caption — rarely enough to tell `Orders` from `Order lines`. Nothing else catches
it. The model builds, `mx check` passes, and the menu is simply hard to use.

Both `menu item` and `menu 'caption' (...)` take an `icon`, in one of three
forms — Mendix stores three different icon **elements**, not three spellings of
one value:

```sql
menu item 'Home' page M.Home icon Atlas_Core.Atlas.home; -- icon collection
menu item 'Close' page M.Close icon glyph 57377; -- numeric glyph code
menu item 'Logo' page M.Logo icon image M.Images.logo; -- image collection
```

The **bare** form is the icon-collection icon and is what you normally want. Use
`glyph` only to reproduce a legacy icon a project already has — `describe
navigation` emits it for you — and `image` for a picture from an image
collection, which is a different document from an icon collection.

The icon-collection form is a **qualified name** — a model reference, written
like every other reference in MDL, not a string:

```sql
create or replace navigation Responsive
Expand Down
1 change: 1 addition & 0 deletions cmd/mxcli/lsp_completions_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 29 additions & 4 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -775,17 +775,42 @@ create or replace navigation Responsive
);
```

**An item with no icon is reported (MDL074, a warning).** The navigation sidebar
collapses to an icon rail, and that is the state most users leave it in: a
collapsed item shows its icon, and one without falls back to the first few
characters of its caption — rarely enough to tell `Orders` from `Order lines`.
The menu still builds and `mx check` passes, so the only symptom is in a browser.
The rule covers every item at every depth, in both `create navigation`'s `menu`
block and `create menu`, and needs no project.

`icon` is optional and is a **qualified name** into an **icon collection** —
`Atlas_Core.Atlas`, `Atlas_Core.Atlas_Filled`, `Atlas_Core.Atlas_Styling`, or one
of your own — written like any other model reference. Hyphenated Atlas names
(`align-center`) are double-quoted, the same way a keyword-colliding name is:
`Atlas_Core.Atlas."align-center"`. List the available names with `describe icon
collection Atlas_Core.Atlas`.

Studio Pro can also set a *glyph* icon (a numeric character code) or an *image*
icon (pointing into an image collection). Those are different elements; MDL
writes only the icon-collection form, and `describe navigation` marks the other
two with a comment instead of emitting an `icon` clause that would convert them.
Mendix stores **three different icon elements**, and each has its own form,
because they are not spellings of one value — a collection icon and an image icon
each hold a qualified name (into an icon collection and an *image* collection,
different documents), while a glyph icon holds a numeric character code and no
name at all:

| form | element | holds |
|------|---------|-------|
| `icon Atlas_Core.Atlas.home` | `Forms$IconCollectionIcon` | a name in an icon collection |
| `icon glyph 57377` | `Forms$GlyphIcon` | a numeric character code |
| `icon image MyModule.Images.logo` | `Forms$ImageIcon` | a name in an image collection |

The bare form is the icon-collection icon, so every existing script keeps its
meaning. The keyword forms exist because writing a bare name for an image icon
would rebuild it as a collection icon — a silent variant swap.

`describe navigation` emits all three, so describe → exec is lossless. It
previously wrote a comment for the other two, and since `create or replace
navigation` is a **full replacement**, re-running that output DELETED the icon
the comment had just declined to describe. A `$Type` this build does not know is
still flagged rather than guessed at.

## Project Settings

Expand Down
65 changes: 65 additions & 0 deletions mdl-examples/bug-tests/navigation-icon-variants.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
-- ============================================================================
-- Menu icons: all three of Mendix's icon elements round-trip
-- ============================================================================
--
-- Mendix stores THREE different icon elements on a menu item, and they are not
-- spellings of one value:
--
-- Forms$IconCollectionIcon a qualified name in an icon collection
-- Forms$GlyphIcon a numeric character code, and no name at all
-- Forms$ImageIcon a qualified name in an IMAGE collection
--
-- MDL could name only the first, so `describe navigation` emitted a comment for
-- the other two — and since CREATE NAVIGATION is a FULL REPLACEMENT, re-running
-- that output DELETED the icon the comment had just declined to describe.
--
-- Measured on testdata/expr-checker, whose Home item carries a glyph icon:
--
-- describe menu item 'Home' page …;
-- -- icon a numeric glyph code (Forms$GlyphIcon) is not reproducible …
-- exec Navigation profile 'Responsive' updated.
-- describe menu item 'Home' page …; <- the icon was destroyed
--
-- Exit 0, success message, silent loss — the same shape as the pluggable-widget
-- body loss in mendixlabs/mxcli#1036.
--
-- The bare form still means the icon-collection icon, so every script written
-- before this keeps its meaning. The keyword forms exist because writing a bare
-- name for an image icon would rebuild it as a COLLECTION icon: a silent variant
-- swap, and the reason the bare form was not simply widened to cover all three.

create module NavIconVariants;

create page NavIconVariants.Home ( Title: 'Home', Layout: Atlas_Core.Atlas_Default ) {
dynamictext t (Content: 'Home')
}

create page NavIconVariants.Close ( Title: 'Close', Layout: Atlas_Core.Atlas_Default ) {
dynamictext t (Content: 'Close')
}

create page NavIconVariants.Logo ( Title: 'Logo', Layout: Atlas_Core.Atlas_Default ) {
dynamictext t (Content: 'Logo')
}

create or replace navigation Responsive
home page NavIconVariants.Home
menu (
-- Forms$IconCollectionIcon — the bare form, unchanged
menu item 'Home' page NavIconVariants.Home icon Atlas_Core.Atlas.home;
-- Forms$GlyphIcon — the numeric character code IS the glyph's identity
menu item 'Close' page NavIconVariants.Close icon glyph 57377;
-- Forms$ImageIcon — an image collection, a different document
menu item 'Logo' page NavIconVariants.Logo icon image NavIconVariants.Images.logo;
-- A submenu takes any of the three too; it sits on the collapsed icon rail
menu 'Admin' icon glyph 57345 (
menu item 'Users' page NavIconVariants.Home icon Atlas_Core.Atlas.user;
);
);

-- The same item syntax serves a standalone menu document, which is why one AST
-- node backs both and the two cannot diverge.
create or modify menu NavIconVariants.Main_Menu (
menu item 'Home' page NavIconVariants.Home icon Atlas_Core.Atlas.home;
menu item 'Close' page NavIconVariants.Close icon glyph 57377;
);
Loading
Loading