diff --git a/.claude/commands/mxcli-dev/wiki-sync.md b/.claude/commands/mxcli-dev/wiki-sync.md index 4adf46e3fb..80bfaed679 100644 --- a/.claude/commands/mxcli-dev/wiki-sync.md +++ b/.claude/commands/mxcli-dev/wiki-sync.md @@ -19,8 +19,14 @@ This command is the trigger; the skill is the contract. - `--all` — sync every page in `docs-wiki/`. - `--stale` — sync only pages whose `last-synced:` SHA is older than HEAD. +For `bug-patterns/` specifically, run **`make digest-status`** first: it reports +how many findings have landed since the last bug-pattern sync and which areas +no page mentions. That is the scope question for this category — the pages +digest `.claude/skills/fix-issue/findings/*.jsonl`, and an area with many +findings and no page is an undigested failure class, not a missing file. + If invoked with no arguments, ask the user which page(s) to sync. List the -seed table from `maintain-wiki.md` as options. +page list from `.claude/skills/maintain-wiki/pages.md` as options. ## Process @@ -88,10 +94,10 @@ If the user is requesting a page that doesn't yet exist in 2. Is it really a procedure (skill), reference (manual), implementation detail (source), state (proposal frontmatter / GitHub), or decision (ADR)? → route there instead. -3. If it genuinely belongs in the wiki, add it to the seed table in +3. If it genuinely belongs in the wiki, add a row to the page list in `maintain-wiki.md` first, with its category, before creating the file. -The seed table is the wiki's table of contents — new pages outside it +That file is the wiki's table of contents — new pages outside it should be the rare exception, not the default. ## Important reminders diff --git a/.claude/skills/fix-issue.md b/.claude/skills/fix-issue.md index 2643911339..5bab3138ff 100644 --- a/.claude/skills/fix-issue.md +++ b/.claude/skills/fix-issue.md @@ -2,22 +2,20 @@ A fast-path workflow for diagnosing and fixing bugs in mxcli. -Each fix appends one row to the symptom table below. That table is **evidence, not -reading material** — it is ~1 MB and cannot be loaded whole. The entry point for a -diagnosis is [`docs-wiki/bug-patterns/`](../../docs-wiki/bug-patterns/), which digests -these rows into failure classes; the table is where you drill down for the specific -instance. +Each fix appends one finding to `findings/*.jsonl`. Those are **evidence, not +reading material**: 630 findings at ~1.7 KB each. The entry point for a diagnosis +is [`docs-wiki/bug-patterns/`](../../docs-wiki/bug-patterns/), which digests them +into failure classes; the findings are where you drill down for the instance. ## How to Use 1. **Start with the failure class.** Skim - [`docs-wiki/bug-patterns/`](../../docs-wiki/bug-patterns/) — it is small, and it tells - you which layer the bug lives in. Coverage is partial (three pages against 600+ rows), - so a miss there means the finding has not been digested yet, **not** that it is new. -2. **Then find the instance**: `grep -i '' .claude/skills/fix-issue.md`. Go - straight to the file the matching row names and follow its fix pattern. Do not open - this file's table whole — it is ~1 MB, and it is looked up by matching a symptom, not - read in order. + [`docs-wiki/bug-patterns/`](../../docs-wiki/bug-patterns/) — it is small, and it + tells you which layer the bug lives in. Coverage is partial (three pages against + 630 findings), so a miss there means the finding has not been digested yet, + **not** that it is new. +2. **Then find the instance** — see "Finding a prior fix" below. Go straight to the + file the matching record names and follow its insight. 3. Write a failing test first, then implement. 4. **Verify at the layer the symptom lives in.** Parser → unit test. BSON we write → unit test on the encoded document. Files on disk after `mx` runs → integration test @@ -28,269 +26,88 @@ instance. 5. **Prove the test detects the bug**: revert the fix and confirm it fails with the reported symptom. A test that has only ever run against fixed code has not been shown to detect anything. -6. After the fix: **add a new row** to the table if the symptom is not already covered. - **Append it at the END of the table, never at the top.** - -> **Conflicts are handled by git, not by where you insert.** Every bug fix touches -> this one file, so two branches fixing unrelated bugs write to the same place and git -> raises a conflict. That cost five resolution rounds in one week when rows went in -> under the header — and moving them to the end did **not** fix it: both sides still -> append to the same line, so the collision moved with the convention. PRs #76, #77 -> and #78 each hit it again afterwards. -> -> The actual fix is in `.gitattributes`: -> -> ``` -> .claude/skills/fix-issue.md merge=union -> ``` -> -> git's built-in `union` driver keeps **both** sides of a conflicting hunk instead of -> raising a conflict. Two fixes that each append a row now merge with no intervention -> and both rows present — verified by merging a simulated later fix into an open -> branch. Where you insert no longer affects merging at all. +6. After the fix: **append a finding** — see "Recording a fix" below. + +> **Why the findings are not in this file.** They were, as one Markdown table, and +> it reached 1.05 MB / 630 rows — past what fits in a context window and past what +> GitHub's web editor will open. It had also stopped being a table: only 227 of the +> 630 rows were still under the header, the other 403 having accreted in twelve +> separate runs through the document, most rendering as literal text. > -> Append at the end anyway, for a human reason: it keeps a fix's diff readable and the -> table roughly chronological. The table is looked up by matching a symptom, not read -> top to bottom, so position carries no meaning. +> `.gitattributes` gave `merge=union` to the whole file, which kept two concurrent +> appends instead of conflicting. That was the right driver on the wrong unit: union +> applies file-wide, so two branches editing the same *prose* line would silently +> keep both. The note that shipped with it called this exact shot — "if that starts +> happening, split the table into its own file so `union` covers only append-only +> content" — and the union driver now sits on `findings/*.jsonl`, where every line is +> an independent record and keeping both sides is always correct. > -> **One caveat.** `union` applies to the whole file, so two branches editing the same -> *prose* line would silently keep both rather than conflict — a visible duplicated -> line, not corruption. If that starts happening, split the table into its own file so -> `union` covers only append-only content. +> It never solved the conflicts anyway: GitHub's server-side merge does not run merge +> drivers, so every PR still had to merge `main` locally first. Sharding by area is +> what actually reduces them — two fixes now collide only when they touch the same +> area. --- -## Symptom → Layer → File Table - -| Symptom | Root cause layer | First file to open | Fix pattern | -|---------|-----------------|-------------------|-------------| -| Every popup opened by an **mxcli-authored** button or Show Page action shows a **blank caption** — just the `×` — while Studio Pro / marketplace popups in the same project are fine. Also: `show page M.P with title = 'X'` is silently ignored | `Forms$FormSettings` / `Forms$PageSettings` `TitleOverride` was written as an **empty** `Microflows$TextTemplate` instead of `null`. An empty template is not the absence of an override — it overrides the title with the empty string. The same unconditional write also discarded `ShowPageAction.OverridePageTitle`, so an authored override never reached BSON (`grep -rn OverridePageTitle` matched only the struct field and its assignment) | `sdk/mpr/writer_microflow_actions.go` (`titleOverrideValue`), `sdk/mpr/writer_widgets_action.go`, `mdl/backend/modelsdk/microflow_write.go` (`showPageFormSettingsToGen`), `mdl/backend/modelsdk/widget_write.go` (`formSettingsToGen` + the `Forms$FormSettings`/`Forms$PageSettings` defaults) | Emit `null` when there is no override and a populated `TextTemplate` when there is. **Beware two traps.** (1) The old comments claimed TitleOverride "must be non-nil", reasoning by analogy from #295, which was about `Forms$PageVariable` — a different field; the repo's own `debug-bson.md` already documented `TitleOverride: nil` as correct. When a comment cites an issue as justification, check that the issue is about the same field. (2) `codec.RegisterTypeDefaults` **overwrites rather than merges**, so two registrations for one `$Type` resolve silently by init order — a `NullFields` entry added next to the microflow writer was clobbered by the one in `widget_write.go` and the null never appeared. Grep for duplicate registrations before debugging a default that "does not apply". Repro `mdl-examples/bug-tests/812-showpage-title-override.mdl`. Issue #812 | -| A generated loop box is drawn far wider than the activities inside it (e.g. 880px around 440px of content), leaving a large empty area — "the visualization is poor" | `measureStatements` sums each element's full width **and** adds `HorizontalSpacing` between them, but `HorizontalSpacing` is a centre-to-centre *pitch*: the builder centres each activity on `posX` and advances by exactly that. Counting it on top of each width over-measures a run of n simple activities by `(n-1)*ActivityWidth` | `mdl/executor/layout.go` (`measureStatementsSpan`), used by `addLoopStatement`/`addWhileStatement` in `cmd_microflows_builder_control.go` | For a run of only simple activities the true span is `(n-1)*HorizontalSpacing + ActivityWidth`. **Do not guess the advance for a compound element** (IF/split, nested loop) — its `posX` advance comes from merge geometry (`mergeX + MergeSize + HorizontalSpacing/2`), and guessing under-sizes the box so activities land *outside* it, which is worse than a box that is too wide; those runs fall back to the conservative measure. Verify with a containment check: every child of a LoopedActivity must lie within `[0, width] × [0, height]`. Issue #790 | -| A microflow writes and `mxcli` reports success, but Studio Pro cannot open the project: `System.Collections.Generic.KeyNotFoundException: The given key '' was not present in the dictionary`. Triggered by a `break`/`continue` inside an if/case within a loop | `microflowObjectToGen`'s `default:` branch returns nil for an unhandled object type, so the event object was dropped at serialization while the SequenceFlow pointing at it was written — a DestinationPointer to a GUID that exists nowhere. Exactly the shape already fixed once for `Microflows$ErrorEvent` | `mdl/backend/modelsdk/microflow_write.go` (`microflowObjectToGen`) — compare against `sdk/mpr/writer_microflow.go`, which handles the full set | Add the missing `case *microflows.XxxEvent:` returning the gen element with ID, position and size. **Diagnose without Studio Pro**: dump the microflow (`mxcli bson dump --type microflow --object M.F`), collect every `$ID`, and check every key ending in `Pointer` resolves — a dangling pointer is this bug. When a check-time rule exists only as a stopgap for a write-path bug (MDL051 was), remove it with the real fix, and check whether it covered every affected keyword (MDL051 covered `break`, not `continue`, which is how #791 reached users). Repro `mdl-examples/bug-tests/791-loop-continue-dangling-flow.mdl`. Issue #791, ledger #52 | -| `mxcli test` leaves the project mutated: Security Level changed, after-startup pointing at the deleted `MxTest.TestRunner`, and only a `Warning:` line about it. Or: an empty `MxTest` module accumulates after every run | Three defects in one teardown. (a) `getAfterStartup` trimmed quotes *before* trailing punctuation, so a DESCRIBE SETTINGS line ending in `,` yielded `Module.Flow',` and the restore statement was unparseable; (b) cleanup dropped only the microflow, not the module it created; (c) the Security Level was forced OFF and restored to a hardcoded PRODUCTION | `cmd/mxcli/testrunner/runner.go` (`parseSettingValue`, `quoteMDLString`, `projectState`, `setupCommands`, `cleanupCommands`) | Strip trailing `,;` before unquoting; re-emit via `quoteMDLString` (doubling embedded quotes, never backslashes); capture a `projectState` before the first mutation and restore from it; drop the module only when the run created it (a pre-existing `MxTest` is the user's); leave Security Level alone entirely; return cleanup errors instead of printing warnings, and fail the run when they occur. Command lists are pure functions so the restore is testable without a project or Docker. Issues #802/#803/#804 | -| `create or modify external entities` silently resets a per-entity setting the user had changed (e.g. allow-create-change-locally) | `applyExternalEntityFields` stamps every field on both the create and the update path, so anything not derivable from the OData contract was overwritten with a default | `mdl/executor/cmd_contract.go` (`applyExternalEntityFields`) | Separate contract-derived fields (Countable/Creatable/Deletable/Skip/Top — refresh from metadata) from local modelling choices (CreateChangeLocally — leave alone; a new entity arrives zero-valued, which is Mendix's default). Issue #782 | -| An **external (OData) entity** loses its remote settings on any read-modify-write — `describe external entity` says `is not an external entity (source: )`, and `alter entity … set allow_create_change_locally = true` reports success but the flag stays off. Works under `--engine legacy` | `entityFromGen` recognised only `DomainModels$OqlViewEntitySource`, so the three `Rest$OData*` sources read back as no source at all: `Source` empty, every remote field zeroed. The write path was fine — it switches on `e.Source`, which the read never populated | `mdl/backend/modelsdk/domainmodel.go` (`entityFromGen`'s source switch, `odataKeyFromGen`) | Mirror the legacy parser (`sdk/mpr/parser_domainmodel.go`) for all three flavours: RemoteEntitySource (capabilities + CreateChangeLocally + key), EntityTypeSource (type name + IsOpen + key), PrimitiveCollectionEntitySource (service only). `Updatable` has no gen accessor and the writer does not emit it — leaving it zero is symmetric. **Check the read side first when a write-path field "does not stick"**: a switch on a field the read never fills looks like a write bug. Repro `mdl-examples/bug-tests/782-external-entity-create-change-locally.mdl`. Issue #782 | -| A pluggable widget's datasource (or child widget, or client action) is **silently dropped at write time** — `exec` prints `Created page` but the widget lands with the piece missing, and only a `log` line mentions `not yet supported — rerun with MXCLI_ENGINE=legacy` | The converter *does* return an error, but it travels through `widgetobj.ChildSerializer`, whose methods return BSON with no error channel (the `TODO(shared-types)` in `mdl/backend/widgetobj/builder.go`), so the caller logged it and returned nil | `mdl/backend/modelsdk/widget_pluggable_write.go` (`recordChildSerializeErr`, `takeChildSerializeErr`) + the drains in `page_write.go` / `snippet_write.go` | Record the failure in a package-level accumulator and drain it at every page/snippet write entry point, so the statement fails instead of the write succeeding with data missing (ADR-0004: refuse, don't drop). The real fix is the deferred `ChildSerializer` interface change; until then, any **new** write entry point that builds pluggable widgets must drain too. Repro `mdl-examples/bug-tests/795b-flow-datasource-context-entity.mdl` | -| `describe page` reports the wrong context entity under a data container bound to a microflow/nanoflow — `-- Context: $currentObject (Module.GetOrders)` names the *flow* instead of the entity it returns | `widget.EntityContext = widget.DataSource.Reference` is correct for a database source (reference *is* the entity) and wrong for a flow source (reference is the flow's qualified name) | `mdl/executor/cmd_pages_describe_flowcontext.go` (`dataSourceEntityContext`, `flowReturnEntity`) + the five assignment sites in `cmd_pages_describe_parse.go` | Resolve the flow's return type via `ListMicroflows`/`ListNanoflows` + `getHierarchy().GetQualifiedName`, taking the entity from an Object/List return type; fall back to the reference when the flow is unresolvable or returns a scalar, so the result is never worse than before. Note `GetRawUnitByName` is unimplemented on the modelsdk engine — the list+hierarchy path is the one that works | -| `describe page` omits a **pluggable** widget's `DataSource` when it is bound to a microflow (`datagrid g1 {` with no DataSource), while a `database from` source describes fine — re-applying the output recreates the grid unbound | A `Forms$MicroflowSource` stores the name in the nested `Forms$MicroflowSettings` (`MicroflowSettings` → `Microflow`), which is what the write path and Studio Pro emit; the reader looked up a top-level `Microflow` key, got `""`, and returned no datasource. The describe *formatter* was correct all along — read bug only | `mdl/executor/cmd_pages_describe_pluggable.go` (`microflowSourceRef`, `nanoflowSourceRef`, `extractDataGrid2DataSource`, `extractGalleryDataSource`, `parseCustomWidgetDataSource`) | Read the nested settings with a top-level fallback, via one shared helper — there were four divergent copies of this lookup and two were wrong. `Forms$NanoflowSource` was missing entirely from the DataGrid2/Gallery switches; add it alongside. Do **not** touch `CustomWidgets$CustomWidgetNanoflowSource` (a different metamodel type whose `Nanoflow` really is top-level) or the `Forms$MicroflowAction` reads (actions, not datasources). Repro `mdl-examples/bug-tests/795-datagrid-microflow-datasource-describe.mdl`. Issue #795 | -| Any `ALTER SETTINGS` (any section) reports success but the Default configuration's **Custom settings** are gone, **Tracing** is reset, and every **constant override** shows blank in Studio Pro — Integer/Long constants then fail the build | The configuration was re-serialized from `model.ServerConfiguration`, which carries only the modelled fields, so CustomSettings/Tracing/OpenAdminPort/OpenHttpPort were dropped, the list version markers downgraded 3→2, and overrides were written with a flat `Value` instead of the nested `SharedOrPrivateValue` the platform reads | `mdl/settingsoverlay/settingsoverlay.go` (`Configurations`, `ServerConfiguration`, `ConstantValues`) — called by both `mdl/backend/modelsdk/settings_write.go` and `sdk/mpr/writer_settings.go` | Overlay onto the raw document instead of rebuilding: write only the fields the read path populates, take each list's marker from what is stored, and update a constant override in the slot it already occupies (nested if nested, flat if flat). New override → nested. New configuration → clone a sibling's shape, empty its collections, mint a fresh `$ID`. Refuse the write when `RawParts` is empty. Repro `mdl-examples/bug-tests/801-alter-settings-preserves-configuration.mdl`. Issue #801 | -| `ALTER SETTINGS` / `CREATE CONFIGURATION` prints "Updated …" but `DESCRIBE SETTINGS` shows the old value — an Integer property was given a non-numeric value, or a Boolean anything other than `true` | `strconv.Atoi`'s error was discarded (`if v, err := …; err == nil`) so the assignment was skipped while the caller still printed success; the boolean form compared against `"true"`, silently mapping every other spelling to false | `mdl/executor/cmd_settings.go` (`settingsInt`, `settingsBool`) + `mdl/executor/validate_settings.go` (`typedSettingsKeys`, MDL-SET01/MDL-SET02) | Parse through a helper that returns a validation error naming the setting and the offending value, and register the property in `typedSettingsKeys` so `mxcli check` and the LSP flag it before the project is opened for writing. `TestTypedSettingsKeys_MatchExecutor` guards the table against drifting from the executor's switch. Repro `mdl-examples/bug-tests/805-alter-settings-typed-values.fail.mdl`. Issue #805 | -| MCP op reports `MCP error -32000: Request timed out` but the page/document/entity EXISTS in Studio Pro afterwards | Studio Pro's ~30s server-side per-call limit fires while the op still applies — a client false failure, not a server rejection | `mdl/backend/mcp/timeout.go` (`isTimeoutErr`, `timeoutVerifyDelay`, `pedUpdateVerify`, `pedDocumentExists`) | Verify-on-timeout, never blind-retry a non-idempotent op: idempotent root-replace (`pgWritePage`) retries once; creates confirm via `ped_find_document`; entity adds confirm via a shallow `/entities` read. Unverified → error with save-before-re-run guidance (a blind re-run from a fresh session duplicates elements) | -| Retrieve/datasource XPath with a `[%…%]` token (e.g. `[System.owner = '[%CurrentUser%]']` or `[Title = '[%CurrentUser%]']`) fails `mx check` CE0161, but `[Title='abc']` is clean | NOT a token-storage bug — tokens store intact and a type-valid token (`[DueDate < '[%CurrentDateTime%]']`) passes. The failures are semantically-invalid XPath: (1) String/scalar attr compared to a User token = type mismatch; (2) `System.owner`/`changedBy`/… referenced on an entity that doesn't store it (needs `alter entity X add attribute owner: autoowner`) | `mdl/executor/validate.go` (`validateRetrieveConstraints`, `baseSystemMemberRe`) — diagnose with `mxcli bson dump --type microflow` + `mx check`; verify the token alone works | Don't "fix" storage — it's correct. Add a `--references` check: collect retrieve `(entity, constraint)` in `flowRefCollector`, look up the entity via `buildEntityIndex` (`ListDomainModels`), and flag a base-entity `System.` ref (regex excludes `/`-traversed refs) when the entity flag (`HasOwner` etc.) is off, with the `alter entity … add attribute …: auto…` hint. Same-script-created entities aren't in the project index, so the check only fires against existing project entities. **Also fixed**: a bare `[%token%]` *inside* a bracketed constraint (`[DueDate < [%CurrentDateTime%]]`) stored unquoted (the inline path keeps the raw source) → CE0161. `normalizeXPathTokens` (`mdl/visitor/visitor_page_v3.go`) requotes bare tokens; wired into `buildXPathSourceExpression`, the multi-predicate `predicateSources`, `buildXPathString`, and `bracketedXPathFromExpr` (already-quoted tokens untouched). Issue #641 | -| `describe` shows `$var = list operation ...;` | Missing parser case | `sdk/mpr/parser_microflow.go` → `parseListOperation()` | Add `case "microflows$XxxType":` returning the correct struct | -| `describe` shows `$var = action ...;` | Missing formatter case | `mdl/executor/cmd_microflows_format_action.go` → `formatActionStatement()` | Add `case *microflows.XxxAction:` with `fmt.Sprintf` output | -| `describe` shows `$var = list operation %T;` (with type name) | Missing formatter case | `mdl/executor/cmd_microflows_format_action.go` → `formatListOperation()` | Add `case *microflows.XxxOperation:` before the `default` | -| Compile error: `undefined: microflows.XxxOperation` | Missing SDK struct | `sdk/microflows/microflows_actions.go` | Add struct + `func (XxxOperation) isListOperation() {}` marker | -| `TypeCacheUnknownTypeException` in Studio Pro | Wrong `$type` storage name in BSON write | `sdk/mpr/writer_microflow.go` | Check the storage name table in CLAUDE.md; verify against `reference/mendixmodellib/reflection-data/` | -| A `create page` reported success but the built page is EMPTY — every widget gone — and `mx check` fails with CE1613 "The selected layout 'dummyModule.dummyName' no longer exists" | The page had no `Layout:` clause, so `buildPageV3` created no `LayoutCall`; the widget tree is built into the LayoutCall's placeholder arguments, so with no LayoutCall the widgets have nowhere to attach and are silently dropped. `dummyModule.dummyName` is *Mendix's* placeholder for a missing layout, not something mxcli writes | `mdl/executor/cmd_pages_builder_v3.go` (`buildPageV3`, the `if page.LayoutCall != nil` block) | Reject a page that has body widgets (or placeholder blocks) but no LayoutCall — distinguish "no Layout: clause" from "layout not found" in the message. A Mendix page always needs a layout; snippets (buildSnippetV3) are layout-less and unaffected. Repro `mdl-examples/bug-tests/266-page-without-layout-drops-widgets.mdl` | -| `ALTER PAGE INSERT`/`REPLACE` into a list bound `from association` **or** `datasource: microflow/nanoflow` produces a widget whose Attribute binds to the WRONG entity (the outer data view's) or nothing — `mxcli check` ✓ but `mx check` fails **CE1613** ("attribute no longer exists") or **CE0402** ("No value specified"); DESCRIBE masks it by printing only the short attribute name | The ALTER mutator read the enclosing entity from `DataSource.EntityRef.Entity`, only set for a DIRECT ref (database). An `AssociationSource` stores its destination on the last `DomainModels$EntityRefStep` of an `IndirectEntityRef`; a `MicroflowSource`/`NanoflowSource` stores NO entity at all (its entity is the flow's RETURN type, in the flow document). Either way the list reported no entity, so the context stayed at the outer data view (or empty) | `mdl/backend/pagemutator/mutator.go` (`extractEntityFromDataSource`+`lastStepDestinationEntity` for association; `EnclosingDataSourceFlow`+`findNearestDataSourceDoc` for flows) + `mdl/executor/cmd_alter_page.go` (`resolveDataSourceFlowEntity`) | Association: read the `IndirectEntityRef`'s last `EntityRefStep.DestinationEntity`. Microflow/nanoflow: the mutator returns the nearest-enclosing (or own, for INTO) flow QN — a nearer non-flow source shadows an outer flow — and the executor resolves its return entity via `getMicroflowReturnEntityName`/`getNanoflowReturnEntityName`. Guards `TestEnclosingEntity_AssociationSource`, `TestEnclosingDataSourceFlow`; repro `mdl-examples/bug-tests/55-alter-page-insert-assoc-binding.mdl`. FINDINGS #55 | -| `mx check` fails to LOAD the project — `StorageLoadException: ... 'Module.Name' is not a valid AttributeIdentifier` after a `create`/`change` with a `Module.Assoc = …` member, yet `mxcli exec` reported success | A one-qualifier member (`Module.Name`) that isn't a known association was written as an *attribute* ref, but a one-qualifier name can't be a valid attribute (attributes are bare or `Module.Entity.Attribute`) → unloadable .mpr. Usually the association's `create` failed earlier (non-idempotent) leaving it absent | `mdl/executor/cmd_microflows_builder_actions.go` (`resolveMemberChange`, the "Not an association in the authored module" branch) | When the domain model is available and the one-dot member isn't in `dm.Associations`/`dm.CrossAssociations`, `fb.addError` with an actionable "create the association first" message instead of writing an Attribute. Same-script associations are visible via `GetDomainModel`, so no false positive. Repro `mdl-examples/bug-tests/264-create-member-unknown-association.mdl`. FINDINGS #51 | -| Runtime `Failed to load model: ... Class 'Workflows$CallMicroflowTask' could not be found` — the WHOLE app won't boot, yet `mxcli check` ✓ and `mx check` → 0 errors | Mendix 11.9 (WOR-2802) split MicroflowBasedActivity into CallMicroflowActivity + AIAgentTaskActivity, renaming the workflow call-microflow on-disk `$Type` from the pre-11.9 `CallMicroflowTask` to `CallMicroflowActivity`. Writing the old name to an 11.9+ project is fatal at boot only. Evidence: 11.6.3 modeler = only Task; 11.10 modeler = both (Task marked "Removed ... WOR-2802" + a conversion routine); 11.10+ runtime jars = only Activity | `mdl/backend/modelsdk/workflow_write.go` (`applyCallMicroflowStorageName`, `useCallMicroflowActivityName`) + legacy `sdk/mpr/writer_workflow.go` (`renameCallMicroflowTypeBSON`) | Version-gate the emitted `$Type` at 11.9 (same boundary as the `HasOwner`→`HasOwnerAttr` gate): build with the legacy name, rewrite the tree to `CallMicroflowActivity` for `pv.IsAtLeast(11,9)`; register codec TypeDefaults + list-marker under BOTH names; read path already folds both into one semantic type. Repro `mdl-examples/bug-tests/263-workflow-callmicroflow-storage-name.mdl`. FINDINGS #39 | -| `mxcli check` rejects a **valid** microflow: **MDL048** on `retrieve … where [id = '[%CurrentUser%]']` (the standard signed-in-user idiom) — but `mx check` → 0 errors | MDL048 targets constraining `id` against a STORED value (String/Long var or plain literal), which Mendix XPath can't do; it also matched the `'[%CurrentUser%]'` **server token**, which Mendix DOES resolve to a GUID | `mdl/executor/validate_microflow.go` (`checkXPathIdConstraint`) | Skip an operand of the form `'[%…%]'` (a resolved token) before flagging. Case still fires for real stored-id values. Test `TestValidateMicroflow_XPathIdConstraint` (CurrentUser case); repro `mdl-examples/bug-tests/52-53-microflow-check-false-positives.mdl`. FINDINGS #53 | -| `mxcli check` rejects a **valid** microflow: **MDL045** ("`/` is division") on `round($a div $obj/Attr * 100)` — division whose divisor is an association-attribute path — but `mx check` → 0 errors | The MDL grammar parses `div`/`*`/`/` at one precedence level, so `$a div $obj/Attr` mis-nests as `($a div $obj) / Attr`; MDL045 saw the `/ Attr` as division. But `Attr` is a bare member name — Mendix has no `/` division operator and re-parses the raw `$obj/Attr` as a path (serialized output preserves the `/`, so the build is clean) | `mdl/executor/validate_microflow.go` (`exprHasSlashDivision`) | Don't flag a `/` BinaryExpr whose RIGHT operand is a bare `IdentifierExpr` (member navigation); real division has a numeric/paren/variable divisor. Test `TestValidateMicroflow_SlashDivision` (div-by-assoc cases); repro `mdl-examples/bug-tests/52-53-microflow-check-false-positives.mdl`. FINDINGS #52 | -| `describe microflow` prints `-- Empty action` for a `set task outcome` / `open user task` / `notify workflow` statement (default engine); a describe→drop→exec round-trip silently drops it. Legacy engine (`MXCLI_ENGINE=legacy`) describes it fine | The modelsdk read path (`actionFromGen`) had no case for the workflow microflow actions, so they read back as nil → "Empty action". The write path + DESCRIBE formatter already handled them; only the modelsdk read case was missing | `mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`) | Add cases for `genMf.SetTaskOutcomeAction` / `OpenUserTaskAction` / `NotifyWorkflowAction`, mirroring the legacy parsers. Test `TestActionFromGen_WorkflowActions`; repro `mdl-examples/bug-tests/54-describe-set-task-outcome.mdl`. FINDINGS #54 | -| `create association X …` errors "association already exists" on re-run and aborts the script | Correct SQL-shaped semantics (like `CREATE TABLE`) — `create` is not idempotent. The idempotent form is `create or modify association`, but it was undiscoverable from the bare error | `mdl/executor/cmd_associations.go` (the `NewAlreadyExists("association", …)` sites) | Not a code bug in the write path — improve the error to name `create or modify association …` and `drop association …`. Repro `mdl-examples/bug-tests/51-create-or-modify-association.mdl`. FINDINGS #51 | -| `dynamictext x (Content: '')` builds with **CE0720** "Place holder index 1 is greater than 0" — `mxcli check` ✓, describe shows `Content: '{1}'` with no params | The builder unconditionally defaulted empty content to the template `{1}`, creating a placeholder with no matching parameter (orphaned) | `mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildDynamicTextV3`, final `content == ""` guard) | Only default to `{1}` when there IS a parameter (`autoGeneratedParams`/`explicitParams`); empty content with no params is a literal empty template. Test `TestBuildDynamicTextV3_EmptyContent`; repro `mdl-examples/bug-tests/traceops-9-10-17-dynamictext-listview.mdl`. traceops #9 | -| `dynamictext s (Content: '$318')` builds with **CE0402/CE1613** ("attribute '$318' no longer exists") — the literal was turned into an unbound `{1}` param | The auto-bind check treated ANY `$`-prefixed content as a variable; `$318` (dollar + digits) is not a valid Mendix variable | `mdl/executor/cmd_pages_builder_v3_widgets.go` (`isDynamicTextVariableRef` / `dynamicTextVariableRe`) | Treat `$` as a variable ONLY when followed by a letter/underscore (`^\$[A-Za-z_]`); `$318` stays literal content. Tests `TestBuildDynamicTextV3_DollarDigitLiteral`, `TestIsDynamicTextVariableRef`. traceops #10 | -| `listview lv (… PageSize: 200)` always pages at 20 — `mxcli check` ✓, `mx check` ✓, describe shows no PageSize | The property parsed into the AST but three layers ignored it: `buildListViewV3` hardcoded `PageSize: 20`, the describe parse never read it, and the listview describe formatter never emitted it | `mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildListViewV3`) + `cmd_pages_describe_parse.go` (Forms$ListView case) + `cmd_pages_describe_output.go` (listview case) | Read `w.GetIntProp("PageSize")` on write; read `w["PageSize"]` on describe; emit a non-default PageSize in the listview formatter. Test `TestBuildListViewV3_PageSize`. traceops #17 | -| `mxcli oql -p app.mpr "…"` fails against a `mxcli run --local` app — first `admin password required`, then (with the password) `OQL error: Action not found.` and 0 rows — even though the same query works under `mxcli docker up` | Two-part gap. (a) The local runtime booted the JVM with no system properties, so `/dev/preview_execute_oql` was never mounted (docker mode passes `-Dmendix.live-preview=enabled -Dmendix.running.locally.by.studiopro=true` via docker-compose; the local boot did not). (b) `run --local` never printed or wrote the admin password, and `mxcli oql` errored instead of defaulting to it | `cmd/mxcli/docker/localboot.go` (`LocalRuntimeOptions.jvmArgs` — new) + `cmd/mxcli/docker/m2ee.go` (`resolveM2EEDefaults` token fallback) + `cmd/mxcli/docker/oql.go` (Action-not-found hint) + `cmd/mxcli/docker/runlocal.go` (banner) | Always pass the two live-preview `-D` flags in `jvmArgs` (run --local is always DTAPMode=D, matching docker); default the oql admin token to `defaultLocalAdminPass` when nothing else supplies it (admin API is loopback-only); branch the "Action not found" hint to cover the `--local` case; print a `Query data: mxcli oql …` line in the run banner. Tests `TestJVMArgs`, `TestResolveM2EEDefaults_Defaults`. traceops #36 | -| A custom Starlark lint rule can't detect a **microflow-datasource** widget (e.g. a ListView with no DB pushdown) — `widget.microflow_ref` is unavailable, so a rule keyed on it returns zero hits at any threshold, even though `CATALOG.WIDGETS.MicroflowRef` is populated | The linter's `Widget` projection (struct + `Widgets()` query + Starlark dict) dropped `MicroflowRef`/`NanoflowRef` — the catalog records them but they never reached the rule tier | `mdl/linter/context.go` (`Widget` struct, `Widgets()` SELECT+Scan) + `mdl/linter/starlark.go` (`widgetToStarlark`) | Carry `MicroflowRef`/`NanoflowRef` through from the catalog and expose `microflow_ref`/`nanoflow_ref` on the Starlark widget struct (mirroring `entity_ref`). Doc: `.claude/skills/mendix/write-lint-rules.md` widget field table. Test `TestWidgets_ProjectsMicroflowNanoflowRef`. traceops #35 | -| A `/** … */` doc comment between `alter entity … add attribute` clauses is a **parse error** (`no viable alternative at input '/**'`) | `alterEntityAction` accepted a doc comment only INSIDE an `attributeDefinition` (after the ADD ATTRIBUTE keyword), not between clauses. `--` line comments are NOT an equivalent workaround — they are discarded, whereas a `/** */` doc comment is persisted as the attribute's Mendix documentation | `mdl/grammar/domains/MDLDomainModel.g4` (`alterEntityAction`) + `mdl/visitor/visitor_entity.go` (`ExitAlterEntityAction` ADD branch) | Add `docComment?` before `ADD ATTRIBUTE`/`ADD COLUMN`; the visitor attaches it as the added attribute's documentation when the attributeDefinition has none. `make grammar` regenerates the parser (not committed). Test `TestAlterEntityAddAttributeDocComment`; repro `mdl-examples/bug-tests/traceops-27-doc-comment-between-clauses.mdl`. traceops #27 | -| `combobox (Association: Mod.Ref, …)` drops the binding — `mxcli check` ✓ but MxBuild fails **CE0642** "Property 'Attribute' is required" | The widget engine's `Association` source read the reference only from the `attribute:` keyword (`w.GetAttribute()`), so an explicit `Association:` keyword was ignored and the widget fell back to enumeration mode | `mdl/executor/widget_engine.go` (`case "Association"`) + `mdl/executor/validate_widgets.go` (`validateComboBoxAssociation`) | Read the reference from `Association:` OR `attribute:`; and add MDL-WIDGET16 flagging an association combobox that lacks the required `datasource:` (option list). A complete association combobox needs reference + `datasource:` + `CaptionAttribute:`. Tests `TestValidateComboBoxAssociation`; repro `mdl-examples/bug-tests/traceops-23-combobox-association.mdl`. traceops #23 | -| A bare MDL keyword used as a WIDGET name (`container body`, `dynamictext content`) is a parse error (`mismatched input 'body' expecting {IDENTIFIER, QUOTED_IDENTIFIER}`) | `widgetV3`'s name only accepted `IDENTIFIER \| QUOTED_IDENTIFIER`, not `keyword` — unlike `attributeName`/placeholder names | `mdl/grammar/domains/MDLPage.g4` (`widgetV3`) + `mdl/visitor/visitor_page_v3.go` (`buildWidgetV3` name extraction) | Add `keyword` to the widget-name alternatives; the visitor reads `wCtx.Keyword()` too. `make grammar` regenerates the parser. Test `TestKeywordWidgetName`; repro `mdl-examples/bug-tests/traceops-11-12-16-strings-names.mdl`. traceops #12 | -| A `'…'` string literal spanning multiple lines fails to parse (`missing END at '…'`) — the newline terminated the token | `STRING_LITERAL` excluded `\r\n` (`~['\r\n\\]`) | `mdl/grammar/MDLLexer.g4` (`STRING_LITERAL`) | Drop `\r\n` from the exclusion (`~['\\]`); mxbuild accepts a multi-line String value (verified). A missing close-quote now spans lines — the standard multi-line-string trade-off. Test `TestMultiLineStringLiteral`. traceops #11 | -| `alter entity … add : ` (missing the `attribute` keyword) fails with an opaque `no viable alternative at input 'add'` | ALTER ENTITY requires the `attribute` keyword (SQL-shaped); the raw ANTLR error doesn't say so | `mdl/visitor/visitor.go` (`enhanceErrorMessage` / `addMissingAttributeRe`) | Source-aware hint: when the offending line is `add :` and `` isn't a real clause keyword (attribute/column/index/event/value/…), append the correct `add attribute : ` form. Gated to the primary "no viable alternative" error. Test `TestAlterEntityMissingAttributeKeywordHint`. traceops #16 | -| CE0066 "Entity access is out of date" | MemberAccess added to wrong entity | `sdk/mpr/writer_domainmodel.go` | MemberAccess must only be on the FROM entity (`ParentPointer`), not the TO entity — see CLAUDE.md association semantics | -| `grant view on page` / `grant execute on microflow\|nanoflow` / `grant access on odata\|published rest service` to a role from **another module** passes `mxcli check`/`exec` but fails the Mendix build with **CE0148 "reselect roles"** — the own-module role works | Document access (page/microflow/nanoflow/service `AllowedModuleRoles`) may only reference the document's **own** module roles; Studio Pro's picker only offers those. The grant path wrote `role.Module + "." + role.Name` verbatim with no same-module check (only `validateModuleRole` = role-exists-in-its-module), so a cross-module reference reached the model. The MOVE path already guarded this (`remapDocumentAccessRoles`) — GRANT didn't | `mdl/executor/cmd_security_defaults.go` (`checkDocumentAccessRolesSameModule`) + the 5 grant handlers in `mdl/executor/cmd_security_write.go` | Pre-check each grant: reject when any `role.Module != docModule` with an actionable message (name the doc's module + suggest the own-module role). Reject (don't silently remap) — a GRANT is explicit, so a wrong role/doc shouldn't be substituted. Wired into page/microflow/nanoflow/OData/published-REST grants. Repro `mdl-examples/bug-tests/ce0148-cross-module-grant.mdl` | -| CE0463 "widget definition changed" | Object property structure doesn't match Type PropertyTypes | `sdk/widgets/templates/` | Re-extract template from Studio Pro; see `sdk/widgets/templates/README.md` | -| Parser returns `nil` for a known BSON type | Unhandled `default` in a `parseXxx()` switch | `sdk/mpr/parser_microflow.go` or `parser_page.go` | Find the switch by grepping for `default: return nil`; add the missing case | -| MDL check gives "unexpected token" on valid-looking syntax | Grammar missing rule or token | `mdl/grammar/MDLParser.g4` + `MDLLexer.g4` | Add rule/token, run `make grammar` | -| CE7054 "parameters updated" / CE7067 "does not support body entity" after `send rest request` | `addSendRestRequestAction` emitted wrong BSON: all params as query params, BodyVariable set for JSON bodies | `mdl/executor/cmd_microflows_builder_calls.go` → `addSendRestRequestAction` | Look up operation via `fb.restServices`; route path/query params with `buildRestParameterMappings`; suppress BodyVariable for JSON/TEMPLATE/FILE via `shouldSetBodyVariable` | -| `CREATE X` returns "already exists — use create or replace to overwrite" but OR REPLACE is not valid for that type | Error message in executor points to wrong keyword | `mdl/executor/cmd__*.go` — find the `NewAlreadyExistsMsg` call | Change hint from `or replace` to `or modify`; verify the AST stmt uses `CreateOrModify` not `CreateOrReplace` | -| `mx check` CE0126 "Missing value for parameter X" on `call java action ... ($Param = empty)` for typed (non-entity, non-microflow) parameters | Builder emitted `BasicCodeActionParameterValue.Argument: ""` instead of the literal `"empty"` keyword | `mdl/executor/cmd_microflows_builder_calls.go` → `addCallJavaActionAction` | Capture all resolved BasicParameterType params into `resolvedBasicParams`; when bound to MDL `empty`, emit `Argument: "empty"` so Studio Pro recognises an explicit empty literal rather than treating the slot as missing | -| `DESCRIBE microflow` puts shared activities inside an `if … then` block — they should appear after `end if;` | Nested guard split inside `traverseFlowUntilMerge` crosses the outer merge boundary | `mdl/executor/cmd_microflows_show_helpers.go` — guard path in `traverseFlowUntilMerge` (~line 854) | Add `if contID != mergeID` guard before the `isMerge` skip-through so the guard continuation never crosses the outer merge | -| `DESCRIBE microflow` on the **modelsdk** engine renders a branching microflow as `if cond then end if;` — the entire then/when body is dropped (legacy engine renders it fully). Affects projects Studio Pro saved with the older single-case storage | The split's branch case is stored under the BSON storage name `NewCaseValue` (single child), but (1) the codec had no `CaseValue`→`NewCaseValue` alias so it was never decoded, and (2) the adapter read only the plural `CaseValuesItems()`, not the singular `CaseValue()` | `modelsdk/codec/decoder.go` (`fieldAliases`) + `mdl/backend/modelsdk/microflow.go` (`caseValueFromGen`/`mapGenCaseValue`) | Add `"CaseValue": "NewCaseValue"` to `fieldAliases` (mirrors `Type`↔`NewType`); read the singular `g.CaseValue()` first, then the plural list, mapping `EnumerationCase`/`InheritanceCase`. A round-trip MDL bug-test can't reproduce it (the write path emits the new format) — covered by codec + adapter unit tests | -| `DESCRIBE microflow` on the **modelsdk** engine renders a rule-based split as `if true then …` instead of the real `Module.Rule_X(args)` condition (body renders, only the condition text is wrong) | `splitConditionFromGen` only handled `*ExpressionSplitCondition`; a `RuleSplitCondition` returned nil → renderer fell back to `true`. Even when handled, the rule name lives under the `RuleCall.Microflow` storage key, but gen decodes the property as `Rule` (and `ByNameRef` decode bypasses `fieldAliases`), so `RuleQualifiedName()` is empty | `mdl/backend/modelsdk/microflow.go` (`splitConditionFromGen`) | Reconstruct `*microflows.RuleSplitCondition` from the gen `RuleCall`; read the rule name from `rc.Raw().Lookup("Microflow")` when `RuleQualifiedName()` is empty (same raw-storage-key pattern the action readers use). The existing `formatSplitCondition` renders it | -| `DESCRIBE microflow`/`nanoflow` on the **modelsdk** engine omits the `grant execute on microflow … to ;` line that legacy emits | `microflowFromGen`/`nanoflowFromGen` never read the allowed module roles, so `Microflow.AllowedModuleRoles` was empty and the describer skipped the grant block | `mdl/backend/modelsdk/microflow.go` (`microflowFromGen`/`nanoflowFromGen`) | Populate `out.AllowedModuleRoles` from the gen `AllowedModuleRolesQualifiedNames()` (BY_NAME role refs, stored as qualified-name strings) | -| `DESCRIBE microflow` on the **modelsdk** engine renders a loop `break;`/`continue;` as `-- Empty action` | `flowObjectFromGen` had no cases for `Microflows$BreakEvent` / `Microflows$ContinueEvent`, so they fell through to the ActionActivity default with a nil action | `mdl/backend/modelsdk/microflow.go` (`flowObjectFromGen`) | Add both object cases (renderer already maps them to `break;`/`continue;`) | -| `DESCRIBE microflow` on the **modelsdk** engine renders `export to mapping …` / `import from mapping …` as `-- Empty action`; and single-object mapping results render `as list of Entity` instead of `as Entity` | `actionFromGen` lacked `ExportXmlAction`/`ImportXmlAction` cases; `restResultHandlingFromRaw` set `SingleObject` only from `Range.SingleObject`, ignoring an object var type (REST) and `ForceSingleOccurrence` (XML import) | `mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`, `restResultHandlingFromRaw`, `readMappingCall`) | Add both XML-action cases reading mapping/arg/output from raw `ResultHandling`/`OutputMethod`/`ImportMappingCall`; extract `readMappingCall`; apply the object-type rule (REST) and force fallback (XML import) — mirrors legacy `parseExportXmlAction`/`parseImportXmlAction`/`parseResultHandling` | -| `DESCRIBE microflow` on the **modelsdk** engine drops a database retrieve's `sort by …` clause | `retrieveSourceFromGen` read Entity/XPath/Range but not the sort columns, which live in the `DatabaseRetrieveSource`'s `NewSortings` child | `mdl/backend/modelsdk/microflow_read_actions.go` (`retrieveSourceFromGen`, `sortItemsFromRaw`) | Generalize `sortItemsFromRaw` to accept the `NewSortings` wrapper key (it already handled the Sort list-op `Sortings` key); set `s.Sorting = sortItemsFromRaw(g.Raw())` | -| A database retrieve's `sort by …` is **completely missing** from DESCRIBE after any modelsdk **write** (regression on switching to the modelsdk engine; "no more sort") — reproduces on a fresh `create microflow … retrieve … sort by …` round-trip | The WRITE counterpart of the row above: `retrieveSourceToGen` set an **empty** `genMf.NewSortItemList()` and never serialized `s.Sorting`, so nothing was stored for the read to render. The generated `SortItemList` type also stores items under `"Items"`, but Mendix stores retrieve sort columns under `"Sortings"` (like the Sort list-op), so the gen convenience type is the wrong tool | `mdl/backend/modelsdk/microflow_write.go` (`retrieveSourceToGen`) | Build the envelope manually, mirroring the `SortOperation` branch: `newElem("Microflows$SortingsList","")` + `addPartList(sl,"Sortings", sortItemToGen(each))` + `g.SetSortItemList(sl)`. Guard with a toGen→encode→decode→fromGen round-trip test asserting the sort items survive. Note "completely missing" ⇒ write dropped it (nothing stored); a *blank-attribute* sort line ⇒ a read/name-resolution gap instead. Issue #727 | -| Consumed OData service **Configuration microflow** has no effect in Studio Pro ≥ 11.10 — the "Configuration source" dropdown stays on "Constants only" even though `describe odata client` shows the microflow | Mendix **renamed the BSON storage field** across versions (from the `Rest$ConsumedODataService` reflection metadata): `ConfigurationMicroflow` was introduced 10.12.0 and **DELETED 11.10.0**; `ConfigurationEntityMicroflow` was introduced 11.10.0. mxcli wrote the pre-11.10 key on all versions, so ≥ 11.10 Studio Pro ignored the unknown field. Both engines were affected (identical BSON). The codebase had flip-flopped this key (573→`ConfigurationEntityMicroflow`, then reverted) — the reflection `VersionInfos` Introduced/Deleted is the authority | `model.ODataConfigMicroflowBSONKey` (new, version-gated), wired into `mdl/backend/modelsdk/odata_write.go` (`consumedODataServiceToGen` via `Backend.configMicroflowKey()`) **and** `sdk/mpr/writer_odata.go` (`serializeConsumedODataService` via `w.reader.ProjectVersion()`) | Gate the write key on project major/minor (≥ 11.10 → `ConfigurationEntityMicroflow`). On read, coalesce **all** historical keys (`model.ODataConfigMicroflowBSONKeys()` in the legacy parser; `firstNonEmpty(ConfigurationEntity…, Configuration…, Headers…)` of the gen getters in `integration_read.go`) so a service authored by any version round-trips. When a storage name looks version-dependent, check `modelsdk/gen/*/version.go` `VersionInfos` before guessing. Issue #728 | -| Published OData service: `mx check` reports **CE5016** "Attribute X has type String(50), but is published as ." (empty published type) for every exposed attribute — modelsdk engine only (legacy slides by) | mxcli never wrote `ODataPublish$PublishedAttribute.EdmType`, the OData EDM type Studio Pro stores on every published attribute. Both engines omitted it, but legacy's `$Type` field order let Studio Pro recompute; modelsdk's order tripped the check. **The legacy output was non-canonical too — don't treat "legacy passes mx check" as "legacy is correct."** Found by diffing mxcli BSON vs a Studio-Pro-duplicated-and-fixed copy (the definitive method for these "empty field" checks) | `mdl/executor/cmd_odata.go` (`mendixAttrTypeToEdm`, `lookupEntityMembers`, `astEntityDefToModel`) + both writers (`publishedMemberToGen`, `serializePublishedMember`) + both reads | Add `EdmType` to `model.PublishedMember`; derive it from the attribute's Mendix type (inverse of `edmToDomainModelAttrType`: String→Edm.String, Integer→Edm.Int32, Long/AutoNumber→Edm.Int64, Decimal→Edm.Decimal, Boolean→Edm.Boolean, DateTime→Edm.DateTimeOffset, Binary→Edm.Binary); emit + read in both engines | -| Published OData service: `mx check` reports **CE5022** "Published association X has changed multiplicity" for every exposed association — modelsdk engine only | Same shape as CE5016 but for `ODataPublish$PublishedAssociationEnd.IsMany` (the exposed navigation's multiplicity), which mxcli never wrote. Confirmed via the same Studio-Pro-duplicate-and-diff method | `mdl/executor/cmd_odata.go` (`assocMembership.Type`, `astEntityDefToModel`) + both writers/reads | Add `IsMany` to `model.PublishedMember`; compute from the association type + exposed side (ReferenceSet ⇒ to-many either end; Reference ⇒ to-many only from the TO/Child side, to-one from the FROM/Parent side); emit + read in both engines | -| `DESCRIBE microflow` on the **modelsdk** engine renders an inheritance split header as `split type $` (empty variable) and drops its `@caption` | `flowObjectFromGen` created a bare `InheritanceSplit` without reading `SplitVariableName`/`Caption`/`Documentation` off the gen element | `mdl/backend/modelsdk/microflow.go` (`flowObjectFromGen`) | Populate `VariableName`/`Caption`/`Documentation` from the gen `InheritanceSplit` accessors | -| `mx check` **CE4899** "concurrent execution: error message or microflow required" after a microflow round-trip (`GetMicroflow` → `UpdateMicroflow`), even though the microflow allowed concurrent execution | `microflowFromGen` never read `AllowConcurrentExecution`/`MarkAsUsed` back (the write path set them), so the round-trip reset them to `false` → "disallow concurrent execution" with no error message configured | `mdl/backend/modelsdk/microflow.go` (`microflowFromGen`) | Read both flags back from the gen getters (`mf.AllowConcurrentExecution()`, `mf.MarkAsUsed()`) — keep the FromGen converter lossless. Guarded by a `toGen→encode→decode→fromGen` Go test, not MDL (create-or-modify rebuilds the microflow from MDL, so it can't reproduce a read-back bug). Issue #723 A1 | -| Studio Pro renders **every activity/decision as a 1-px sliver** (caption wraps one letter per line) after a modelsdk round-trip; `mx check` reports **NO** error (it ignores box size, so the corruption is silent) | `flowObjectFromGen` carried each object's `Position` but not its `Size`, so the round-trip rewrote every node with size `0;0` | `mdl/backend/modelsdk/microflow.go` (`sizeFromGen`, `splitFlowObjects`) | Add `sizeFromGen` (mirrors `pointFromGen`) and apply it at the `splitFlowObjects` call site — covers nested loop bodies via recursion; add `GetSize`/`SetSize` to `BaseMicroflowObject`. Go round-trip test (not MDL, same reason as A1). Issue #723 A2 | -| `mx check` **CE0117** "Error in expression" when creating a rule-based decision (`if Module.SomeRule(...)`) via MDL on the **modelsdk** engine; the decision's subtype is demoted Rule→Expression on every round-trip | modelsdk `*Backend` never implemented `IsRule` → fell back to the embedded `unimplemented` stub (which errors); the builder's `isRule, err := backend.IsRule(...); if err != nil || !isRule` guard treated the error as "not a rule" and emitted an invalid `ExpressionSplitCondition` | `mdl/backend/modelsdk/microflow.go` (`IsRule`) | Implement `IsRule` on `*Backend` (list `Microflows$Rule` units, match qualified name via `moduleNameFor`), mirroring the legacy reader; it shadows the generated stub. With the existing builder mock-test this guards the full chain modelsdk.IsRule→RuleSplitCondition. Issue #723 A4 | -| `create enumeration … ("Value" = 'Caption')` fails with cryptic `mismatched input '=' expecting ')'`; user blames the *quotes* | Not a quoting bug — enum value names quote fine (`enumValueName` accepts `QUOTED_IDENTIFIER`). The `=` is invalid: MDL enum values are `Value 'Caption'` (or `Value caption 'Caption'`), no equals sign | `mdl/visitor/visitor.go` (`enhanceErrorMessage`/`looksLikeEnumEquals`) | Add an error hint keyed on `mismatched input '=' expecting ')'` (specific to enum value lists — attribute-default `=` gives a different message) pointing at the `=`. Clarify in `check-syntax.md` that captions use `Name 'Caption'`, never `=` | -| A genuine parse error on a short lowercase MDL keyword/identifier (`mismatched input 'on'`, `'in'`, `'as'`, `'to'`, `'by'`, …) is misdiagnosed with the "unescaped apostrophe" hint, sending the user to look for a quote that isn't there | `looksLikeUnescapedApostrophe` matched **any** 1–4 char lowercase token as a contraction leftover, so real short keywords tripped it | `mdl/visitor/visitor.go` (`looksLikeUnescapedApostrophe`, `contractionSuffixes`) | Match only the fixed contraction-suffix set (`s`/`t`/`d`/`m`/`re`/`ve`/`ll` — the leftovers from it's/don't/he'd/I'm/you're/we've/you'll), not arbitrary short lowercase words. Real apostrophe errors still hint; `on`/`in`/`as`/`to`/`by` no longer do. Findings #4 | -| Docs/skills document MDL that doesn't parse (drift): `ALTER ENTITY X ADD (a, b)` / `DROP (a)` / `MODIFY (a)` / `RENAME a TO b`, `ALTER ENUMERATION … REMOVE VALUE`, `CREATE CONSTANT … TYPE X;` (no default), `DELETE_BEHAVIOR ` | Docs were written against a SQL-DDL mental model, never `mxcli check`-validated. Correct forms: `ADD ATTRIBUTE a: type` (one action/statement), `DROP ATTRIBUTE a`, `MODIFY ATTRIBUTE a: type`, `RENAME ATTRIBUTE a TO b`, `DROP INDEX `; `DROP VALUE`; enum `ADD VALUE X CAPTION 'y'` (CAPTION keyword required for ALTER, unlike CREATE); a constant `DEFAULT` is mandatory; delete-behavior ∈ {DELETE_AND_REFERENCES, DELETE_BUT_KEEP_REFERENCES, DELETE_IF_NO_REFERENCES, CASCADE, PREVENT} | `scripts/check-skill-mdl.sh` + `make check-skill-mdl` (CI) | The guard extracts DDL statements from `.claude/skills/mendix/` **and** `docs-site/src/` and runs `mxcli check`; keeps this class of drift from recurring. Run it after editing any MDL example | -| `DESCRIBE microflow` on the **modelsdk** engine renders `download file $X …;` as `-- Empty action` | `actionFromGen` lacked a `DownloadFileAction` case | `mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`) | Add the case reading `FileDocumentVariableName()`/`ShowFileInBrowser()`, defaulting an empty `ErrorHandlingType` to Rollback. Note: the storage key is `ShowFileInBrowser` (legacy's `parseDownloadFileAction` reads the wrong `ShowInBrowser` key — a latent legacy bug; the gen reads it correctly) | -| `DESCRIBE microflow` on the **modelsdk** engine renders a legacy SOAP `call web service …` as `-- Empty action` | `actionFromGen` lacked a `WebServiceCallAction` case | `mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`, `webServiceActionRequiresRawBSON`) | Add the case: read the structured fields (ImportedService / OperationName / NewResultHandling / RequestHandling) and, when the action carries any field the structured form can't represent, set `RawBSON = a.Raw()` so the renderer emits `call web service raw ''`. Mirror legacy's supported-key set exactly; `canonicalRawBSON` makes both engines' base64 byte-identical | -| `DESCRIBE microflow` on the **modelsdk** engine emits a REST `body mapping X` line with no `from $var`, which fails to re-parse (`mismatched input … expecting FROM`) — breaks the DESCRIBE roundtrip | `restRequestHandlingFromRaw` read the export-mapping source variable from the wrong key `ParameterVariable`; the real storage key is `MappingVariableName` (same key ExportXmlAction uses), so it came back empty and the renderer dropped the grammar-mandatory `from $var` | `mdl/backend/modelsdk/microflow_read_actions.go` (`restRequestHandlingFromRaw`) | Read `ParameterVariable` from `MappingVariableName` (fallback to `ParameterVariable`). Note this makes modelsdk *more* faithful than legacy, whose `parseRequestHandling` drops the REST request body mapping entirely ("would be parsed here if needed") | -| MDL widget property `mxcli check`s clean but Studio Pro renders the default (e.g. `dataview ... (FormOrientation: Vertical)` always Horizontal) | V3 grammar generic-property branch parks the value in `w.Properties`, but the V3 builder never reads it and the writer never emits it; the widget struct has no field for it | `mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildXxxV3`) + `sdk/mpr/writer_widgets_*.go` (`serializeXxx`) + `sdk/pages/pages_widgets_*.go` | Add field to `pages.Xxx`; read via `w.GetStringProp` / `w.GetIntProp`; write in `serializeXxx`. If the Studio Pro UI label differs from the BSON storage name (e.g. DataView "Form Orientation" → `LabelWidth: 0/N`), confirm by diffing a Studio Pro-saved page against the reflection-data defaults | -| `ALTER ...` parse error "no viable alternative at input 'ALTER'" but `CREATE ...` works | The grammar lists ALTER variants explicitly per document type; a new doc type was added to `createStatement` but not to `alterStatement` | `mdl/grammar/MDLParser.g4` `alterStatement` block + dedicated visitor + new `Alter*Stmt` AST type + `register_stubs.go` | Add ALTER rule (mirror the closest sibling — `ALTER ODATA CLIENT` for SET-only, `ALTER PUBLISHED REST SERVICE` for SET+ADD/DROP); regenerate grammar; add `Alter*Stmt` with `Changes map[string]string`; route via `exitAlterStatement` dispatcher; register handler. Add the new AST type to `registry_test.go`'s `allKnownStatements()` | -| Pluggable widget datasource property (`optionsSourceAssociationDataSource: Module.Entity`) passes `check` + `exec` but `mx check` reports CE0642 "Property 'Entity' is required" | A `datasource`-operation property was authored by name; the engine reads the widget's `datasource:` clause (`Properties["DataSource"]`), not the named key, so the value is silently dropped (and `hasDataSource` mode-selection also misfires) | `mdl/executor/validate_widgets.go` (`datasourceTypedKeys`) — and author via the `datasource:` clause | `check` now rejects named datasource-typed props (MDL-WIDGET05); the fix for the user is `datasource: database Module.Entity`. Persisting a *named* datasource property (multi-datasource widgets) needs a Studio-Pro-verified mapping — deferred. Real-but-unmapped props get MDL-WIDGET06 warning via def.json `knownProperties` (not a false MDL-WIDGET01) | -| CE7247 "The name 'X' is a reserved word." on non-persistent entity attributes (Owner/Type/Context/Id/CreatedDate/ChangedDate/ChangedBy) — mxcli accepts the MDL silently, Studio Pro rejects the project | `ValidateEntity` early-returned for NPEs; reserved-word check was not wired into the executor | `mdl/executor/cmd_enumerations.go` (`ValidateEntity`) and `mdl/executor/cmd_entities.go` (`execCreateEntity`) | Drop the `EntityPersistent` early-return; gate only `mendixSystemAttributeNames` (MDL020) to persistent; run `mendixReservedWords` (MDL021) for all kinds; call `ValidateEntity` from `execCreateEntity` before any backend write. Issue #552 | -| `SHOW ACCESS ON PAGE` / Page section of `SHOW SECURITY MATRIX` reports "no roles" for a restricted page on the **modelsdk** engine (legacy is correct) — under-reports page access, a security-audit hazard | `pageFromGen` never read the page's allowed module roles into `Page.AllowedRoles`, so it defaulted to empty. (The gen `Page` decode is correct — it has the storage-name override `allowedRoles`↔`AllowedModuleRoles`.) The microflow/nanoflow equivalent was fixed separately | `mdl/backend/modelsdk/page.go` (`pageFromGen`) | Populate `out.AllowedRoles` from the gen `AllowedRolesQualifiedNames()` (mirrors `microflowFromGen`). Fixes both `SHOW ACCESS ON PAGE` and the matrix Page section. Issue #722 | -| `mxcli report` (and `lint`) hangs for many minutes on a large project — appears to deadlock after "Catalog ready", 130-byte banner-only output, process pinned at ~140% CPU (not blocked → not a lock deadlock) | O(N²) BSON re-decode: six lint rules loop `for mf := range ctx.Microflows()` and call `reader.GetMicroflow(mf.ID)` per microflow, but the modelsdk backend's `GetMicroflow` re-lists and re-decodes EVERY microflow unit on each call. N calls × N decodes → millions of full BSON parses. Diagnose with `kill -QUIT ` (GOTRACEBACK=all) — the running-goroutine stack names the stuck rule + `ListUnitsWithContainer` | `mdl/linter/context.go` (`FullMicroflow`, `LintReader`) + the six rules (`validation_feedback`, `conv_loop_commit`, `conv_split_caption`, `conv_error_handling`×2, `mpr008_overlapping_activities`) | Add `LintContext.FullMicroflow(id)` — loads ALL microflows once via `ListMicroflows()` and serves lookups from a per-run cache (safe: lint is read-only). Swap every `reader.GetMicroflow(mf.ID)` in a loop to `ctx.FullMicroflow(mf.ID)`. Turns O(N²) into O(N); report went from >40min hang to ~5s on 3259 microflows. Issue #720 | -| `DESCRIBE MICROFLOW` (mdl/json) times out at 300s on a high-McCabe flow, but `--format mermaid` renders in ~1s — extraction is fast, the serializer hangs | Exponential path enumeration: `duplicateOutputVariableWarnings` (run during `formatMicroflowActivities`) walked EVERY execution path, cloning the visited map at each branch (`cloneIDBoolMap`), to find output vars assigned twice on one path — O(2^branches). ~20 sequential `if/end if` diamonds already took ~10s. Diagnose with `DBGPROF=… ` CPU profile / add a pprof block to `describeMicroflow`; top-cum names the offender (not the describe traversal, which is linear) | `mdl/executor/cmd_microflows_show.go` (`duplicateOutputVariableWarnings`) | Replace the all-paths walk with **reachability**: a name is a duplicate iff two of its assignments are path-ordered (one reaches the other); exclusive-branch assignments never reach each other. Memoized `reachableFrom` is O(V·E). Loop bodies inherit names assigned by activities that reach the loop node. Went 9.5s→0.1s at 20 diamonds; 120 diamonds completes instantly. Issue #710 | -| Microflow `set $x = ` passes `mxcli check` but `mx check` reports a generic CE0117: (#17) `/` used as division (`$Dec / 2`), (#18) a decimal literal loses its fraction (`2.0` → `2`, breaks Decimal typing), or (#19) a small decimal becomes scientific notation (`0.000001` → `1e-06`, which Mendix rejects) | (#18/#19) the value is re-serialized from the AST when `shouldPreserveExpressionSource` returns false and the AST serializer is lossy for decimals (`%v` on a float64 drops `.0` and uses sci-notation). (#17) **MDL division is `div`, not `/`** — `/` is the member-access separator, so `/`-as-division is a user error that mxcli silently wrote instead of flagging. **Do NOT preserve source on `/`** — that source-freezes every legit association path (`$Order/Assoc/Name` → regressed `TestAssociationNavParsing`) | `mdl/visitor/visitor_microflow_statements.go` (`shouldPreserveExpressionSource`); `mdl/executor/validate_microflow.go` (`checkDivisionSlash`/`exprHasSlashDivision`, MDL045) | (#18/#19) preserve raw source only when the expression contains a **decimal literal** (`.` adjacent to a digit) — same philosophy as the XPath-where path. (#17) add **MDL045**: walk the expression tree for a `BinaryExpr` with operator `/` (`$Dec / 2`, `(...) / $x`) and reject with "use `div`". The variable/variable form `$Dec / $Dec2` parses as a member-access path (the `$` on the RHS is stripped), so the visitor narrowly preserves source when a `/` is immediately followed by `$` (a real association path never has `$` after `/`) and MDL045 flags it by scanning the preserved source for a `/ $` **outside string literals** (`exprHasSlashDollarDivision` / `sourceHasSlashDollarDivision`). **Round-2 gap (ledger #17 re-test): the `$a / $b` division must be caught even when EMBEDDED in a larger expression** — `$a / $b + 1`, `round($a / $b)`, `$a / $b * 100` — where it degrades to a member-path `AttributePathExpr` nested under a `BinaryExpr`/`FunctionCallExpr`, so the structural `/`-BinaryExpr walk (which only sees literal division) misses it. The earlier `exprIsSlashDollarDivision` matched only when the SourceExpr **directly** wrapped an AttributePathExpr, so embedded division slipped through silently. Fix: walk the tree for any `SourceExpr` and scan its raw source for `/ $` with a string-literal-aware scanner (skips `'path/$x'`). [verification round: the `$a / $b` form must be caught by bare `check`, not deferred to `--references`.] **Lesson: `/` is overloaded (path separator vs the division a user *wanted*) — a blanket source-preserve on `/` corrupts the common case to rescue the rare misuse; reject the misuse explicitly instead. And an operator misuse that "degrades to a path" can hide anywhere in an expression tree, not just at its root — detect it from source, walking every sub-expression.** Tests: `TestShouldPreserveExpressionSource_Decimals`, `TestValidateMicroflow_SlashDivision` (incl. embedded cases + string-literal guard); repros `mdl-examples/bug-tests/ledger-17-19-expression-serialization.mdl` (pass) + `ledger-17-slash-division.fail.mdl` (MDL045, incl. embedded). Ledger findings #17–19 (round 2) | -| `DESCRIBE` of an entity/module emits `grant ... (read (Module.Entity.Attr))` that fails to re-parse with `mismatched input '.'` — breaks the DESCRIBE roundtrip | Member emitter used the fully-qualified BY_NAME reference; grant grammar accepts a bare `IDENTIFIER` only | `mdl/executor/cmd_entities_access.go` → `resolveEntityMemberAccess` | Strip `memberName` to the last `.`-segment before appending (bare names have no dot, so it's a no-op for them). Issue #633 | -| `exec` of a pluggable widget from a bundled multi-widget `.mpk` fails "no definition for widget … (run 'mxcli widget init')" even after init (e.g. only AreaChart of Charts.mpk registers) | `ParseMPK`/`getWidgetIDFromMPK` read only `WidgetFiles[0]`; a bundled `.mpk` (Charts) holds many widgets so only the first is registered/augmented | `sdk/widgets/mpk/mpk.go` (`ParseMPKAll`/`ParseMPKWidget`, `FindMPK`) + def-gen loops in `mdl/executor/widget_defs.go` + `mdl/catalog/builder_widget_definitions.go` + `cmd/mxcli/cmd_widget.go` | Parse every widgetFile (`ParseMPKAll`); register all ids in `FindMPK`; augment the specific id (`ParseMPKWidget`); bump `WidgetDefGeneratorVersion` so existing projects regenerate. Issue #679. (Per-series datasource + chart CE0463 are separate, still open.) | -| `buttonstyle: ` passes `mxcli check` but the button renders btn-default in Studio Pro (silent at build) — typically a mis-cased value (`Primary`) or one Mendix doesn't have (`secondary`, `link`) | Visitor stored the style verbatim and the executor cast it straight to `pages.ButtonStyle`; only an empty value got a default, so any unknown string was written as-is and MxBuild degraded it | `sdk/pages/pages_widgets_action.go` (`CanonicalButtonStyle`) + `mdl/executor/cmd_pages_builder_v3_widgets.go` (button builder) + `mdl/executor/validate_widgets.go` (`validateStaticWidget`) | Normalize case-insensitively against the metamodel `PagesButtonStyle` set (Default/Primary/Success/Warning/Danger/Info/Inverse); reject unknown values as MDL-WIDGET02 at check time and in the executor. Issue #672 | -| `style:` on a `dynamictext` crashes MxBuild with a NullReferenceException (fails at build, not check) | The generic appearance applier wrote an inline Style into the DynamicText's `Forms$Appearance`; Mendix's metamodel can't handle it | `mdl/executor/cmd_pages_builder_v3.go` (`applyWidgetAppearance`) + `mdl/executor/validate_widgets.go` (`validateStaticWidget`) | Reject an inline `style` on a dynamictext as MDL-WIDGET03 (check + executor); the workaround is to wrap it in a container and style the container. Issue #673 | -| Quoted where-clause `where '[Title=''abc'']'` (with `''` escapes) fails `mx check` CE0161 "Error(s) in XPath constraint." on every retrieve/datasource — but inline `where [Title='abc']` is clean. `mxcli exec` reports no error | The quoted string un-quoted correctly to `[Title='abc']`, but the visitor preserved the **raw** token (outer quotes + doubled `''`) as the constraint source; the builder then bracket-wrapped it → `['[Title=''abc'']']`. Affected the microflow retrieve and page datasource `expression`-form paths (the `grant … where '…'` path was fine — it uses `unquoteString` directly) | `mdl/visitor/visitor_microflow_statements.go` (`buildRetrieveWhereExpression`) + `mdl/visitor/visitor_page_v3.go` (`bracketedXPathFromExpr`, datasource WHERE) | When the whole WHERE clause is a bare string `LiteralExpr`, use its **unquoted** `.Value` as the constraint source (bracket-wrap only if it isn't already `[...]`); don't re-serialize via `xpathExprToString`/preserve-raw-source (which re-doubles the `''`). Verify both forms store identically with `mx check` = 0 errors. Issue #642 | -| `DYNAMICTEXT (Attribute: X)` (e.g. inside a LISTVIEW) is silently dropped — `describe`/BSON shows `Content: '{1}'` with no parameter binding; Studio Pro throws `System.NullReferenceException` (ClientTemplateFormPart.CollectControls) and MxBuild fails CE0720 "Place holder index 1 is greater than 0" | `buildDynamicTextV3` read `Content`/`ContentParams` but never read the `Attribute:` property, so the binding was discarded and the template defaulted to the orphaned "{1}". `mxcli check` had no orphan detection | `mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildDynamicTextV3`) + `mdl/executor/validate_widgets.go` (`validateStaticWidget`/`validateDynamicTextPlaceholders`) | Treat `Attribute: X` as a single auto-generated template param (route through `resolveTemplateAttributePathFull`, same as `ContentParams: [{1} = X]`; non-String attrs get `toString()`). Add **MDL-WIDGET04**: flag a dynamictext whose `Content` template references `{N}` with fewer than N bound params (counting ContentParams or a single Attribute). Issue #650 | -| A widget `show_page` whose target page is created later in the same script passes `mxcli check --references` but the executor fails with "page not found … defined later in this script" | The whole-script scriptContext used by reference validation tolerates forward refs (the page exists *somewhere*); the executor resolves page refs in statement order | `mdl/executor/validate.go` (`validateForwardPageRefs`) | An ordered pass flags a widget page ref that is neither in the project nor created earlier in the script, with the same "move the create statement earlier" hint the executor gives — keeping check consistent with execution. Issue #674 | -| Workflow constructs pass `mxcli check` but MxBuild rejects them: user task without a page (CE1834); a single-outcome user task whose one outcome has a nested activity flow (CE1876); a decision outcome that isn't a valid enum value identifier, e.g. `'Confirmed closed'` | `CreateWorkflowStmt` had **no** case in `validateWithContext` and no `ValidateWorkflow` at all — workflows received zero semantic validation | `mdl/executor/validate_workflow.go` (`ValidateWorkflow`, wired from `cmd/mxcli/cmd_check.go`) | Recursively walk `stmt.Activities`: **MDL-WF01** flag a `WorkflowUserTaskNode` with empty `Page`; **MDL-WF02** flag `len(Outcomes)==1 && len(Outcomes[0].Activities)>0`; **MDL-WF03** flag a decision/call-microflow outcome `Value` that isn't `True`/`False`/`Default` and fails the identifier regex (space/punctuation). Syntax-only (no project). The page-context-entity check (CE7412) and enum-membership form of WF03 are `--references` follow-ups. See `PROPOSAL_check_mxbuild_gap_heuristics.md` | -| A DataGrid **control-bar** button passing `$currentObject` (e.g. `Action: show_page P (Order: $currentObject)`) passes `mxcli check` but MxBuild fails CE1571 "No argument has been selected for parameter …" | A control bar sits above the grid and is not row-scoped, so `$currentObject` is unbound there; no check distinguished control-bar buttons from row-scoped (column) buttons | `mdl/executor/validate_page_button_context.go` (`ValidatePageButtonContext`, wired from `cmd/mxcli/cmd_check.go`) | **MDL-BUTTON01**: walk the widget tree tracking an `underControlBar` flag (any `controlbar`-typed ancestor, mirroring `checkLayoutGridTree`'s `underGrid`); flag any button action (and its `ThenAction` chain) whose args contain the string `$currentObject`. Row-scoped column buttons are unaffected. Syntax-only | -| `DESCRIBE` of a page/snippet emits `action: call_microflow X` / `call_nanoflow X` (button/widget actions) that fails to re-parse — breaks the DESCRIBE roundtrip | Action emitters prefixed `call_`; the page-action grammar (`actionExprV3`) accepts `MICROFLOW`/`NANOFLOW` only | `mdl/executor/cmd_pages_describe_output.go` (`extractButtonAction`) + `cmd_pages_describe_pluggable.go` (`extractCustomWidgetPropertyAction`) | Emit `microflow `/`nanoflow ` without the `call_` prefix. Issue #634 | -| `DESCRIBE` of a Java action (or `describe module … with all`) emits a body-less `create java action …;` that fails with `no viable alternative` and cascades into following statements — happens for add-on/Marketplace actions whose `.java` source isn't on disk | Body emitted only when `readJavaActionUserCode` returns non-empty, but the grammar requires `AS DOLLAR_STRING` (body is mandatory) | `mdl/executor/cmd_javaactions.go` → `describeJavaAction` | Always emit the `as $$ … $$;` block; when source can't be read, write a placeholder comment body. Issue #637 | -| Studio Pro crashes on export/upgrade/Save-As with `System.InvalidOperationException: Null value found in primitive property 'ImageDataStorage' of object of type 'Mendix.Modeler.CodeActions.MicroflowActionInfo'` after a Java action was created via MDL `exposed as … in …` (project opens fine; only re-serialization crashes) | The `MicroflowActionInfo` sub-document was written in the legacy shape: `$Type` = `JavaActions$MicroflowActionInfo`, an obsolete `Icon` key, and the four icon/image bitmaps null or absent. Mendix maps `ImageData`→ the mandatory binary `ImageDataStorage`; a null/absent value crashes `UnitWriter`. **Both engines** were wrong: legacy `sdk/mpr` emitted `ImageData: null`; the default modelsdk engine used `gen/javaactions.MicroflowActionInfo` (also legacy `$Type`, no binary fields) | struct `mdl/types/javaaction_types.go` + legacy `sdk/mpr/writer_javaactions.go` (`microflowActionInfoBSON`) + `sdk/mpr/parser_javaactions.go` (`parseMicroflowActionInfo`, shared with `parser_misc.go` JS actions) + modelsdk `mdl/backend/modelsdk/java_write.go` (`patchMicroflowActionInfo`) + reads `mdl/backend/modelsdk/javascript_read.go` | Replace `Icon string`/`ImageData string` in the struct with four `[]byte` (`IconData`/`IconDataDark`/`ImageData`/`ImageDataDark`). Emit `$Type` = `CodeActions$MicroflowActionInfo` with all four as **empty-non-null** binaries (`primitive.Binary{Subtype:0, Data:[]byte{}}`), no `Icon`. Parser reads the four binaries and **tolerates** the legacy null/`Icon` shape so corrupted units load and self-repair on rewrite. The modelsdk gen models these bitmaps as `Primitive[string]` (serialises to BSON string, can't emit `binary(0)`), so leave the gen MAI null and post-patch the top-level `MicroflowActionInfo` field via `codec.PatchBSONField` instead of editing generated code. Verify by decoding the written `.mxunit` under both engines (`grep -rlF 'MicroflowActionInfo' mprcontents/`). Issue #656 | -| `DESCRIBE` of a page emits `column Title (...)` / `column Description (...)` (datagrid columns named after a reserved keyword) that fails with `missing {IDENTIFIER, QUOTED_IDENTIFIER}` — breaks the DESCRIBE roundtrip | #619 added `mdlIdent` (quote-if-reserved) for general widget names but missed datagrid column names | `mdl/executor/cmd_pages_describe_output.go` (`column %s` header, ~line 837) | Wrap the column name: `fmt.Sprintf("column %s", mdlIdent(colName))`. Issue #638 | -| `CREATE JAVA ACTION`/`CREATE JAVASCRIPT ACTION` with an `Enumeration(Mod.Enum)`/`ENUM Mod.Enum` parameter or return type passes check+exec but `mxbuild`/Studio Pro fails: `Property 'Entity' of Java action parameter '…': The selected entity 'Mod.Enum' no longer exists` | The param/return converter mapped `ast.TypeEnumeration` to an `EntityType` (entity ref), because the parser records both a bare `Module.Name` *and* the explicit `ENUM`/`Enumeration(...)` syntax as `TypeEnumeration` with no distinguishing flag, and `EnumerationType` was never emitted/serialized | AST `mdl/ast/ast_datatype.go` (`ExplicitEnum`) + visitor `mdl/visitor/visitor_helpers.go` (`buildDataType`) + converter `mdl/executor/cmd_javaactions.go` (`astDataTypeToJavaActionParamType`/`…ReturnType`) + writers `sdk/mpr/writer_javaactions.go` (`serializeInnerType`/`serializeReturnType`) and `mdl/backend/modelsdk/java_write.go` (`codeActionInnerTypeToGen`/`codeActionReturnTypeToGen`) + reads `parser_javaactions.go` (already) / `mdl/backend/modelsdk/java_read.go` | Add `ExplicitEnum bool` to `ast.DataType`, set it in the `ENUM`/`Enumeration(...)` branch of `buildDataType` (a bare `Module.Name` stays ambiguous → entity). When `ExplicitEnum`, the converter emits `javaactions.EnumerationType{Enumeration: qn}`; both writers serialize `CodeActions$EnumerationType` (param wrapped in `BasicParameterType`) with the `Enumeration` by-name ref; modelsdk uses `genCa.NewEnumerationType().SetEnumerationQualifiedName(...)`. The legacy parser already read it; add the gen `*genCa.EnumerationType` case to the modelsdk read converters so DESCRIBE round-trips. Validate by decoding the `.mxunit` and `mx check` (build passes only after the fix). Issue #680 | -| `DESCRIBE` of a page/snippet emits widget-action microflow args as `Param = $value` that fails with `mismatched input '=' expecting ':'` — breaks the DESCRIBE roundtrip | Arg emitters used `=`, but `microflowArgV3` accepts `IDENTIFIER COLON expr` (`Param: $value`) or `VARIABLE EQUALS expr` (`$Param = …`) — not `IDENTIFIER EQUALS` | `mdl/executor/cmd_pages_describe_output.go` → `extractMicroflowParameters` / `extractNanoflowParameters` | Emit the canonical colon form: `paramName+": "+value`. Issue #640 | -| CE3637 "A data view cannot listen to the selection of Gallery 'X', because it is not available here." on a master-detail page generated from MDL | Gallery's `itemSelectionMode` pluggable-widget property was hardcoded to `clear` in the def.json; Mendix requires `toggle` for selection-listeners (DataViews bound via `DataSource: selection X`) to see the gallery's selection | `sdk/widgets/definitions/gallery.def.json` | Change the `itemSelectionMode` mapping from `value: "clear"` to `source: "ItemSelectionMode", default: "clear"` so MDL can write `gallery X (Selection: Single, ItemSelectionMode: toggle)`. The V3 engine's generic mapping path (`widget_engine.go` `resolveMapping` default case → `GetStringProp`) reads the value automatically — no grammar changes needed. Also add `ForceFullObjects: false` to `Forms$ListenTargetSource` serializer in `sdk/mpr/writer_widgets_display.go` | -| DESCRIBE drops `DataSource: selection X` for a DataView bound to a gallery/listview selection (master-detail pages) | `extractDataViewDataSource` only handled `Forms$MicroflowSource` / `Forms$NanoflowSource` / `Forms$DataViewSource` / `Forms$DatabaseSource`; `Forms$ListenTargetSource` fell through to `return nil` | `mdl/executor/cmd_pages_describe_parse.go` (`extractDataViewDataSource`) + `mdl/executor/cmd_pages_describe_output.go` (DataView case) + `mdl/executor/cmd_pages_describe.go` (rawDataSource doc-comment) | Add `case "Forms$ListenTargetSource":` returning `{Type: "selection", Reference: ds["ListenTarget"]}`; add `case "selection":` in the DataView output switch emitting `DataSource: selection ` | -| CE0463 "widget definition changed" on every pluggable widget that contains a caption/template parameter (gallery, datagrid2 captions, dynamictext with ContentParams) on Mendix 11.9 — cascades into CE3637 on master-detail pages | `serializeClientTemplateParameter` emitted `Forms$FormattingInfo` with a `TimeFormat: "HoursMinutes"` field, but the FormattingInfo reflection schema only declares CustomDateFormat / DateFormat / DecimalPrecision / EnumFormat / GroupDigits — the extra field made Studio Pro mark the embedded WidgetType as drifted | `sdk/mpr/writer_widgets.go` `serializeClientTemplateParameter` + `mdl/backend/mpr/widget_builder.go` (mirror copy of the same FormattingInfo block) | Drop the `TimeFormat` entry from both writers. Verify by diffing your BSON against a Studio Pro-saved page's FormattingInfo block — if you see keys outside the reflection schema, that's the trigger | -| Pluggable widget `Selection` BSON value is lowercase (`single` / `multi` / `none`) but Studio Pro stores PascalCase — contributes to CE0463 drift on stricter widgets and looks wrong in diffs | MDL passes the user's typed value verbatim to `SetSelection`; the builder didn't normalise | `mdl/backend/mpr/widget_builder.go` `SetSelection` | Canonicalise via `canonicalSelectionValue` helper (lowercase-keyed switch → `Single` / `Multi` / `None`); unknown values pass through | -| CE0463 on a pluggable widget whose TextTemplate property is *conditionally hidden* by an enum/boolean toggle (VideoPlayer `videoUrl`/`posterUrl` when `type=expression`; Timeline `title`/`description`/`timeIndication` when `customVisualization=true`) — engine clones the template's populated `ClientTemplate` for a property Studio Pro hides and nulls | Engine has no per-property visibility metadata; the hide rules live in the widget's compiled `editorConfig.js` (`hidePropertyIn`/`hidePropertiesIn`), not `widget.xml` | `mdl/executor/widget_defs.go` `widgetVisibilityRules` table + `mdl/backend/mpr/widget_builder.go` `ApplyPropertyVisibility` | Extract the widget's `.mpk` `*.editorConfig.js` (`unzip` + grep `hidePropertiesIn`), transcribe the rule into `widgetVisibilityRules[widgetID]` as `{propertyKey, hiddenWhen:{propertyKey, operator eq/ne/truthy/falsy, value}}`; the engine nulls hidden TextTemplate-typed props at build time. Bump `WidgetDefGeneratorVersion` so stale project `.def.json` auto-refresh. Issue #574 | -| CE0463 on a `datagrid` whose column uses `ColumnWidth: manual` + `Size: N` — Studio Pro resets the column `size` to `1` | The MDL `ColumnWidth:` keyword isn't mapped to the schema `width` enum, so `width` stays at its `autoFill` default; `size` only applies when `width=manual`, so the value is inconsistent. Regression from the Stream B keyword-path consolidation (the deleted `datagrid_builder.go` did `colPropString(col.Properties, "ColumnWidth", "autoFill")`) | `mdl/executor/widget_defs.go` `itemPropertyAliases` | Add the MDL→schema alias under `[datagrid]["columns"]`: `"width": {"ColumnWidth"}`. Bump `WidgetDefGeneratorVersion` so stale `.def.json` regenerate. General rule when a column/object-list property's MDL keyword differs from the `.mpk` schema key (not just case), add it to `itemPropertyAliases`; cross-check against the pre-B3 `datagrid_builder.go` `colProp*` calls for any other dropped mappings | -| CE0463 on an MDL-created **Combobox** (or other platform widget) at `mxcli docker build/check` time — but the SAME widget passes `mx check` on a project whose installed `.mpk` matches mxcli's embedded template (Mendix 11.6 / combobox 2.5.0). NOT the old incomplete-template bug (#112, fixed — a matching-version combobox is clean) | Widget-VERSION mismatch: mxcli emits the embedded 2.5.0-shaped PropertyTypes, but the project has a NEWER combobox (e.g. 2.8.1) that reorders/regroups properties. `augmentFromMPK` patches presence + enum values but can't restructure the baseline; `GenerateFromMPK` is less faithful still (fails even on a matching version). The designed remediation is `mx update-widgets` (docker build/check runs it before `mx check`) | `cmd/mxcli/docker/check.go` + `build.go` (`updateWidgetsPathArg`) — the update-widgets *invocation*, not the widget emission | The real trap was that `mx update-widgets ` **crashed** (`AddProjectDirAsAllowedPath` → `Path.GetDirectoryName("app.mpr")` = "" → `System.ArgumentNullException`) and some mx builds exit 0 after printing it, so the migration silently no-op'd and CE0463 survived. Pass an **absolute** path to update-widgets (always has a directory component). `mx check` is unaffected. Diagnosis: run the bundled `mx update-widgets ` yourself — if it throws ArgumentNullException on AddProjectDirAsAllowedPath, the path lacks a dir. Faithful multi-version widget emission is the larger fix (#529). Issue #112; repro `mdl-examples/bug-tests/112-combobox-enum-ce0463-widget-version.mdl` | -| **CE0463 "widget definition changed" at an `Image` widget** on Mendix 11.7+ (`mx check` without `update-widgets`) — a plain mxcli-authored Image, no custom config. `update-widgets` clears it (and on v2 destroys `mprcontents/`, so it's a data-loss trap) | NOT a version stamp, Type `$ID`s, property order, or missing properties (all ruled out empirically). The embedded `image.json` carried a **spurious default value**: a `WidgetValue.Image` pointing at `Atlas_Core.Content.Mendix` (Atlas's Mendix logo), captured when the template was extracted from a project that had it set. The installed 11.12 Image widget expects that field empty, so MxBuild flags the definition as changed | `modelsdk/widgets/templates/mendix-11.6/image.json` + `sdk/widgets/templates/mendix-11.6/image.json` (line ~63) | Clear the stale default: `"Image": "Atlas_Core.Content.Mendix"` → `"Image": ""` in both engine templates. **Diagnosis method for this whole CE0463 class**: dump the widget BSON, `mx update-widgets` on a COPY, dump again, diff the `CustomWidgets$CustomWidget` subtree **order-independently** (canonicalise key order + mask `$ID`/`TypePointer` blobs). Reordering and generic instance chrome (`LabelTemplate`, `Appearance.DesignProperties`) are cosmetic — a *passing* widget gets reordered/those-added too; the real cause is whatever value/structure survives that normalisation. Other hand-extracted templates likely hide similar stale defaults (audit with the same diff). Repro `mdl-examples/bug-tests/image-ce0463-stale-default.mdl`. DataGrid2 custom-content CE0463 (#600) is a *separate*, more complex delta — same method, own fix | -| `mxcli docker check`/`build` (or a bare `mx update-widgets`) **silently deletes `mprcontents/`** and rewrites an MPRv2 project into single-file v1 — `check` reports **0 errors** and looks successful, but the git working tree diverges from tracked files, a running `mxcli run --local` loop breaks (it watches `mprcontents/`), and Studio Pro may crash on open (`LibGit2RepositoryProvider.WriteBaseFile`). Triggered by following the CE0463 remediation on a `mxcli new` (always v2) project | The pre-check `mx update-widgets` step (run to suppress false CE0463) **performs the conversion**: it inlines every unit into the `.mpr` (`Unit.Contents` column) and deletes `mprcontents/`. The `check` itself is read-only; `update-widgets` is the mutator. docker check/build invoked it with no storage-format protection | `cmd/mxcli/docker/update_widgets.go` (`runUpdateWidgets` / `snapshotStorageFormat`) — call sites in `check.go` and `build.go` | Snapshot `.mpr` + `mprcontents/` to a temp dir before `update-widgets`, `defer restore()` after the check (restore removes the post-conversion single-file `.mpr` residue and puts the v2 tree back); MPRv1 projects need no protection. The check still runs against the widget-normalized model, so CE0463 stays suppressed — only the on-disk format is preserved. **Never tell an agent to run bare `mx update-widgets` on a v2 project** — the synced skills (`create-page.md`, `custom-widgets.md`, `migrate-design-prototype.md`, `download-marketplace-content.md`) + dev `debug-bson.md` route to `mxcli docker check`/`build` (v2-safe) instead. **Fix the operation, not the call site**: PR #764 wrapped the invocation inside `Check` only, and `Build` had a second bare copy — so `docker build`/`run`/`reload` kept converting projects for another 40 issues, until #808. The snapshot now lives in `runUpdateWidgets`, which is the only place that may exec `update-widgets`; grep for `"update-widgets"` should return exactly one hit. When a mutating external step is guarded, put the guard in a function that also *performs* the step, so a new caller cannot get it wrong. Issues #763 / PR #764, #808 | -| Nightly `mx check` reports `CE0117 "Error(s) in expression." at Log message activity 'Log message (warning)'` on Mendix 10.24.19+ but not 10.24.16 or 11.x | Mendix 10.24.19 tightened expression validation: `toString()` is now a type error (toString expects a non-string input). An example called `toString($OrderNumber)` where `$OrderNumber` was already a string parameter | The offending `log warning ... with ({1} = toString($stringVar))` — find via `~/.mxcli/mxbuild/{ver}/modeler/mx check`, then bisect with `drop microflow ...` until CE0117 disappears | Remove the redundant `toString()` wrapper around already-string values. Only wrap non-string values (integers, decimals, dates, enums) in `toString()`. The Mendix 11.x parser is more lenient and lets this slide, but 10.24.19+ rejects it | -| A page-level property can't be set — `ALTER PAGE X { SET PopupWidth = 800; }` (or any page-level prop other than Title/Url) fails with `unsupported page-level property: …` | The page-level SET handler only special-cased a couple of properties; everything else fell through to the default error | `mdl/backend/pagemutator/mutator.go` → `applyPageLevelSetMut` (shared by both engines) | Add a `case` writing the field at the top level of the Forms$Page doc via `dSetOrAppend` with the on-disk BSON type (int64 for PopupWidth/PopupHeight, bool for PopupResizable — verify against a Studio Pro page with `mxcli bson dump --format bson`). Page-level prop names are **case-sensitive**. For DESCRIBE roundtrip, emit the values back in the CREATE PAGE header. CREATE-time support: add a generic `IDENTIFIER COLON propertyValueV3` to `pageHeaderPropertyV3` (regen grammar), recognize the keys in `parsePageHeaderV3` (`applyGenericPageHeaderProp`, error on unknown), carry `*int`/`*bool` on `CreatePageStmtV3`, default to 600/600/false in `buildPageV3`, and have both writers honour `page.Popup*` (legacy `sdk/mpr/writer_pages.go` int64; codec `mdl/backend/modelsdk/page_write.go` int32 via gen — tolerated by mx check). The MCP backend has its **own** `mcpPageMutator` (pg content tree, not raw BSON) — page-level SET there reaches `SetWidgetProperty("")`; map it or reject honestly (it rejects, pending a `pg_read_page` probe of the pop-up keys). Issue #661 | -| `PopupWidth: 0` / `PopupHeight: 0` rejected ("must be a positive number") on CREATE or ALTER PAGE; user can't make an auto-size pop-up | 0 is actually Studio Pro's **default** for pop-up dimensions (auto-size) — verified live on 11.12: a pg-created PopupLayout page stores 0/0 and `mx check` = 0 errors. Two validators rejected ≤0, and both writers coerced ≤0→600, so even an allowed 0 became 600 | `mdl/visitor/visitor_page_v3.go` (`popupDimensionValue`) + `mdl/backend/pagemutator/mutator.go` (`coercePopupDimension`) + `mdl/executor/cmd_pages_builder_v3.go` (builder default) + `sdk/mpr/writer_pages.go` & `mdl/backend/modelsdk/page_write.go` (`popupDimension`) + `mdl/executor/cmd_pages_describe.go` | Relax both validators to reject only **negative**; default the builder to **0** (not 600, matching Studio Pro); drop the `≤0→600` coercion in both writers (clamp only negatives to 0); have DESCRIBE suppress only the real default 0 (emit an explicit 600). No `*int` needed — 0 is a valid stored value. Bug-test `mdl-examples/bug-tests/713-popup-zero-dimensions.mdl`. Issue #713 | -| `CREATE EXTERNAL ENTITIES FROM Module.Service` imports external entities as plain **PERSISTED** entities and NPEs **without the "from Service" link** (regression on the default modelsdk engine; legacy `--engine legacy` was correct) | The modelsdk write adapters dropped all external-entity serialization: `entityToGen` only handled `DomainModels$OqlViewEntitySource`, `attributeToGen` always emitted `StoredValue`, and `assocToGen` forced `Source` null. So `e.Source = "Rest$ODataRemoteEntitySource"` (+ RemoteServiceName/EntitySet/KeyParts) was ignored and the entity fell to the plain NoGeneralization path | `mdl/backend/modelsdk/domainmodel_write.go` (`entityToGen`, `attributeToGen`, `assocToGen`) — the executor (`applyExternalEntityFields` in `mdl/executor/cmd_contract.go`) already stamps the right `domainmodel` fields; only the write adapter was lossy. The legacy serializer `sdk/mpr/writer_domainmodel.go` is the parity reference | Port the legacy logic into the codec adapters using the gen `Rest$OData*` types (all present in `modelsdk/gen/rest`): `externalEntitySourceToGen` builds `Rest$ODataRemoteEntitySource`/`…EntityTypeSource`/`…PrimitiveCollectionEntitySource` (+ `ODataKey`/`ODataKeyPart`, with fresh IDs since `assignEntityIDs` only stamps the top source); `attributeToGen` takes an `isExternal` flag and emits `Rest$ODataMappedValue` / `…MappedPrimitiveCollectionValue`; `externalAssociationSourceToGen` emits `Rest$ODataRemoteAssociationSource` / `…PrimitiveCollectionAssociationSource` (the `DomainModels$Association` NullFields:["Source"] default still nulls Source for plain associations because SetSource only emits when set). Tests in `external_entity_write_test.go`. Issue #718 | -| `describe page` shows a widget's default placeholder text (e.g. the Dutch `'Tekst'` for a DynamicText) instead of the configured content; round-tripping describe→create then overwrites the real caption | Text extraction was language-blind: `extractTextContent`/`extractTextCaption`/`extractTextFromTemplate` returned the **first** `Texts$Text` `Items[]` entry, and the page title hardcoded `GetTranslation("en_US")`. In a multi-language project the first Items entry is often a non-default-language placeholder, so the wrong translation is shown. (The MDL surface is single-language regardless — this is display-only; note the real text isn't lost on disk, only mis-displayed) | `mdl/executor/describe_language.go` (new) + the three extractors in `cmd_pages_describe_output.go`/`cmd_pages_describe_parse.go` + title in `cmd_pages_describe.go` | Select the translation by **project default language → en_US → first non-empty** via `selectTranslationText` / `pickTextTranslation`; get the default language from `ctx.Backend.GetProjectSettings().Language.DefaultLanguageCode`, cached on `executorCache` and pre-warmed in `preWarmCache` (race-free for parallel describe). No MDL bug-test possible (MDL can't author >1 translation) — covered by unit tests in `describe_language_test.go`. Issue #702 | -| "Class is not a Page property" — can't set a CSS class/style on a page via `CREATE PAGE (Class: '…')` (grammar rejects it) or ALTER PAGE | A page's Class/Style live on its `Forms$Appearance` sub-doc, but nothing wired it to MDL: `pages.Page` had no Class/Style field, the writers hardcoded the appearance empty, and `Class`/`Style` are reserved lexer tokens so the header's generic `IDENTIFIER` branch never matched them | grammar `mdl/grammar/domains/MDLPage.g4` (`pageHeaderPropertyV3`) + `pages.Page` + AST `CreatePageStmtV3` + visitor `visitor_page_v3.go` + builder `cmd_pages_builder_v3.go` + writers `sdk/mpr/writer_pages.go` & `mdl/backend/modelsdk/page_write.go` + describe `cmd_pages_describe.go` + ALTER `mdl/backend/pagemutator/mutator.go` (`applyPageLevelSetMut`) | Add explicit `CLASS COLON STRING_LITERAL` / `STYLE COLON STRING_LITERAL` alternatives to `pageHeaderPropertyV3` (regen grammar — keyword tokens don't match `IDENTIFIER`); add `Class`/`Style` to `pages.Page` + AST + visitor header handling + builder; legacy `serializePage` and modelsdk `newAppearance(page.Class, page.Style, …)` honor them; describe reads `rawData["Appearance"].Class/Style` into the CREATE PAGE header; ALTER page-level SET writes the `Appearance` sub-doc (create if absent). Same pattern as PopupWidth #661/#713. Bug-test `mdl-examples/bug-tests/714-page-class.mdl`. Issue #714 | -| `CreatePage: ListView source *pages.DatabaseSource not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy` — a native `listview` with a `database`/`database from … where …` datasource fails on the default engine | `listViewSourceToGen` only handled `*pages.MicroflowSource`; database (and context/listen/association) sources fell through to the "not supported" error. DataView, DataGrid2, and Gallery database sources already worked — only native ListView was missing | `mdl/backend/modelsdk/widget_write.go` (`listViewSourceToGen`); mirror the `customWidgetDataSourceToGen` database branch and the legacy `serializeListViewDataSource` | Add a `*pages.DatabaseSource` case building `genPg.NewListViewXPathSource()` with EntityRef (`DirectEntityRef`), XPathConstraint, a `NewGridSortBar()` (GridSortItem per `d.Sorting`, note gen uses **SortBar** not the legacy `Sort`/`ListViewSort`), and a `NewListViewSearch()` — the codec already has all types. Verify with `mx check` = 0 errors. Tests in `listview_source_write_test.go` | -| `Visible: [Attr != '']` / `Editable: […]` on a widget (CONTAINER, textbox, …) → CE0117 "Error(s) in expression." in Studio Pro on the legacy engine, and silently *ignored* on the modelsdk engine | Two bugs (NOT BSON structure): (1) the expression was emitted with a bare attribute reference (`Name != ''`) — a Mendix client expression must root attributes in the widget data context (`$currentObject/Name != ''`); (2) the modelsdk codec hardcoded `ConditionalVisibilitySettings` to null, dropping the expression entirely | `mdl/visitor/visitor_page_v3.go` → `buildConditionalExpression`/`conditionalExprToString`; `mdl/backend/modelsdk/widget_write.go` → `applyWidgetBase` + TypeDefaults | (1) For `VISIBLE`/`EDITABLE` xpath constraints, build the xpath AST and serialize via `conditionalExprToString`, which prefixes bare `IdentifierExpr` / non-variable-rooted `XPathPathExpr` with `$currentObject/` (leaves `$…`-rooted paths, literals, functions, enum qualified-names untouched). Do **not** apply this to datasource `where` clauses (those are real XPath). (2) In `applyWidgetBase`, type-assert `SetConditionalVisibilitySettings`/`SetConditionalEditabilitySettings` and emit a settings element when the model has one; register `Forms$ConditionalVisibilitySettings`/`…EditabilitySettings` TypeDefaults (`NullFields: Attribute,SourceVariable`; `MandatoryLists: Conditions[,ModuleRoles]`) so the null-when-unset slots stay null (the encoder only fills NullFields when not already emitted). **ALTER PAGE** had the same gap (`SET Visible = [expr]` parsed as a `propertyValueV3` array and silently no-op'd): add `VISIBLE/EDITABLE EQUALS xpathConstraint` alternatives to `alterPageAssignment` (regen grammar), route to `VisibleIf`/`EditableIf` in `buildAlterPageAssignment` (reusing `buildConditionalExpression`), and add `VisibleIf`/`EditableIf` cases to `setRawWidgetPropertyMut` (`mdl/backend/pagemutator/mutator.go`) that build the settings node via `setWidgetConditionalSettingMut` (rejecting editability on non-input widgets). The shared mutator covers both engines. Verify both engines with `mx check` = 0 errors. Issue #627 | -| `visible: [$currentObject/Status = Mod.Enum.Value]` stored as `… = 'Value'` (string) → MxBuild CE0117 "Error(s) in expression" (v0.13.0 regression; both engines) | `conditionalExprToString` (added with #627) sent a qualified-enum `QualifiedNameExpr` to its `default` case → `xpathExprToString`, which converts a 3-part enum name to a string literal. Correct for an XPath *datasource* constraint (DB level), WRONG for a *client* visibility/editability expression (compares to the qualified enum value) | `mdl/visitor/visitor_page_v3.go` → `conditionalExprToString` | Add an explicit `*ast.QualifiedNameExpr` case returning `e.QualifiedName.String()` (the qualified literal) before the `default`. Leaves the datasource `where` path (`buildXPathString`/`xpathExprToString`) untouched, so DB-level enums still stringify to `'Value'`. Applies to CREATE and ALTER (both use `conditionalExprToString`). Verify the datasource enum still stores `'Value'` and the visibility enum stays qualified; `mx check` = 0 | -| CE0488 "No entity configured for the data source of this data view" on a context (page-parameter-bound) data view authored over **MCP** — DESCRIBE looks correct (`dataview dvX (DataSource: $Param)`), but the model has no entity on the source. DataGrids with database sources are fine | `mapDataViewSource`'s `*pages.DataViewSource` case used a mutually-exclusive `switch`: when `ParameterName != ""` it wrote only `sourceVariable` and never `entityRef`. Studio Pro/pg write **both** entityRef AND sourceVariable for a context source (see `testdata/pg-page-contact-newedit.json`) | `mdl/backend/mcp/page_widgets.go` → `mapDataViewSource` | Drop the exclusive switch: set `entityRef` (DomainModels$DirectEntityRef) whenever `EntityName != ""` AND `sourceVariable` (Pages$PageVariable) whenever `ParameterName != ""`. The executor already resolves the parameter's entity into `EntityName` (`cmd_pages_builder_v3.go` parameter case), so both are available. MCP-only — the MPR engines were never affected | -| Mendix can't resolve the microflow named in `CREATE ODATA CLIENT (ConfigurationMicroflow: microflow X.Y)` / `ErrorHandlingMicroflow:` — error names the literal string `"MICROFLOW X.Y"` as the missing microflow | Case-mismatched prefix strip: visitor emits uppercase `"MICROFLOW "` from `odataValueText`, but `extractMicroflowRef` only trimmed lowercase `"microflow "`, so the keyword survived into BSON | `mdl/executor/cmd_odata.go` → `extractMicroflowRef` | Use a case-insensitive strip: `if strings.EqualFold(ref[:10], "microflow ") { return ref[10:] }`. Whenever a value goes from a visitor that emits a keyword-prefixed form to an executor that strips it, the strip must match the case the visitor produces — grep visitor files for `"MICROFLOW " +`/`"ENTITY " +`/etc. when adding a new property. Issue #573 | -| `describe`/catalog reports a numeric BSON field (Length, MinOccurs, MaxOccurs, MaxLength, FractionDigits, TotalDigits, Interval, NumberOfPagesToClose, …) as `0` / `unlimited` even though Studio Pro shows a real value | BSON numeric width mismatch — Studio Pro writes the field as `int64`, but the parser asserted `raw["X"].(int32)` so the type assertion failed silently and the field defaulted to its zero value | `sdk/mpr/parser_*.go` — grep for the field name; the fix point is the narrow assertion | Replace narrow type assertions on BSON numeric fields with the existing `extractInt(raw["X"])` helper (`sdk/mpr/parser.go`). It handles int32/int64/int/float64. When a non-zero default must survive a missing field, gate with `if _, ok := raw["X"]; ok { … = extractInt(...) }`. Sweep `grep -n '\.(int32)' sdk/mpr/parser_*.go` and ignore matches whose comment says "marker" (BSON-array-prefix probes are intentional). Issues #583, #585 | -| Studio Pro shows a dropdown / property as its default value even though MDL set it explicitly (e.g. CREATE ODATA CLIENT with `ConfigurationMicroflow:` set, but the "Configuration source" dropdown reads "Constants only") | The BSON key mxcli writes isn't what Studio Pro reads — either the key name is wrong, or multiple dropdown options actually share a single field discriminated by something other than the key name (return type of a referenced microflow, sibling property, etc.) | The `serializeXxx` function in `sdk/mpr/writer_*.go` for the affected document type | (1) Ask the user to duplicate the offending object in Studio Pro and **explicitly pick each dropdown option** on the duplicate(s). An unconfigured duplicate just looks like "Constants only" and tells you nothing about which field the option uses. (2) Re-dump the duplicates from `mprcontents/**/*.mxunit` **after every Studio Pro change** — cached `/tmp/svc-*.json` files go stale the instant the user edits the project (see [[feedback-refresh-bson-dumps]]). (3) Diff against mxcli's output to find the renamed key. (4) Don't assume one-state-per-key. The OData "Configuration microflow" / "Headers microflow" case stores BOTH options in the single `ConfigurationMicroflow` BSON field — Studio Pro picks the dropdown label from the referenced microflow's return type. When a discriminator like that exists, have both MDL keywords write to the same model field. Issues #573, #587 and 2026-05-27 unify-config-microflow fix | -| `DESCRIBE` of a document type omits a property the user set in the CREATE (e.g. enum-level `/** doc */` vanishes after a roundtrip; `CREATE OR REPLACE` silently runs as plain CREATE for some types) — even though the AST struct has the field, the model has the field, the writer serialises it, and DESCRIBE prints it | Visitor never copied the parsed value into the AST struct — every layer below the visitor is wired but the visitor's `ExitCreateXxxStatement` is missing the assignment | `mdl/visitor/visitor_.go` → `ExitCreateXxxStatement` | Diff the visitor against a known-good sibling (e.g. enumeration vs constant). Standard wiring: `stmt.Documentation = findDocCommentText(ctx)` for doc-comments; `if createStmt.OR() != nil && (createStmt.MODIFY() != nil \|\| createStmt.REPLACE() != nil) { stmt.CreateOrModify = true }` for OR MODIFY/REPLACE. When adding a new CREATE statement, grep `mdl/visitor/visitor_constant.go` and copy these two blocks verbatim. Issue #393 | -| `create [or modify] association ... to System.X` passes `mxcli check --references` and `mxcli diff` but fails at `mxcli exec` with `child entity not found: System.X` | Two divergent entity resolvers: the write path's `findEntity` resolved the owning module via `h.FindModuleID(dm.ID)`, but the virtual System domain model is not a real unit, so the hierarchy walk yielded an empty module name and System entities never matched. The validation path (`buildEntityQualifiedNames`) keyed on `dm.ContainerID` and worked, hence the check-passes/exec-fails split | `mdl/executor/oql_type_inference.go` → `findEntity` | Resolve the module from `dm.ContainerID` (the module ID `BuildSystemDomainModel` sets), not by walking up from the DM's own unit ID. When a symptom is "passes check/diff but fails exec," suspect two resolvers and make the write-path one match the validation-path one; add an `exec`-level test, not just a `check` test. Issue #610 | -| `ALTER STYLING ON PAGE/SNIPPET ... SET ...` fails with `unsupported container type: PAGE` (and `DESCRIBE STYLING` silently shows "No widgets found"); even past that, design-property writes never reached builder-created pages | Two layered bugs: (1) the visitor emits uppercase `ContainerType` `"PAGE"`/`"SNIPPET"` but `execAlterStyling`/`execDescribeStyling` compared lowercase, so it fell through to the unsupported-container error; (2) ALTER STYLING used the reflection walker `walkPageWidgets` (legacy `ListPages`/`UpdatePage`), which can't locate widgets in MDL-builder pages and violates the mutator-pattern rule | `mdl/executor/cmd_styling.go` (`execAlterStyling`, `execDescribeStyling`) + `mdl/backend/mpr/page_mutator.go` | Normalise container type with `strings.ToLower`. Route ALTER STYLING through `ctx.Backend.OpenPageForMutation(unitID)` like ALTER PAGE; check `mutator.FindWidget`. Add `SetDesignProperty`/`RemoveDesignProperty`/`ClearDesignProperties` to the `PageMutator` interface, writing the widget's `Appearance.DesignProperties` BSON array (`Forms$DesignPropertyValue` → `Toggle`/`Option`/`Custom` value), preserving an existing custom kind on option updates. When an ALTER uses a container-type discriminator, mirror the casing fix already done for ALTER PAGE (#402). Issue #631 | -| `declare $x list of T = empty;` (or any list-typed `declare`) passes `mxcli check` but Studio Pro rejects with CE0053 ("type not allowed") + CE0038 ("value required") | `declare` maps to a Create Variable activity, which Mendix forbids from producing a list — but the validator only flagged an empty list *used as a loop source* (MDL002), never the declaration itself | `mdl/executor/validate_microflow.go` → `walkBody` `*ast.DeclareStmt` case | Emit `MDL040` (SeverityError) for any `stmt.Type.Kind == ast.TypeListOf`, regardless of initializer. Lists must come from a microflow parameter, a `retrieve`, or `$x = create list of T;`. Also fix the synced skills that present declare-list as valid (`write-microflows.md`, `cheatsheet-variables.md`, `check-syntax.md`, `patterns-*`). Issue #607 | -| `DESCRIBE PAGE` emits a widget whose name is a reserved keyword (`container List`, `dynamictext Template`) and the output fails `mxcli check` (`mismatched input 'List' expecting IDENTIFIER`); quoting it by hand (`container "List"`) also failed | The widget-name position `widgetV3` accepted only a bare `IDENTIFIER`, so neither the keyword nor a `QUOTED_IDENTIFIER` parsed; and DESCRIBE emitted the name unquoted. Note this is *not* the general reserved-word problem — qualified names / params / attributes already go through `identifierOrKeyword` (a 552-entry keyword allowlist). The gap is the ~103 strict-`IDENTIFIER` positions, of which widget names are the highest-impact | grammar `mdl/grammar/domains/MDLPage.g4` (`widgetV3`) + `mdl/visitor/visitor_page_v3.go` (`buildWidgetV3`) + `mdl/executor/cmd_pages_describe_output.go` | Widen the name position to `(IDENTIFIER \| QUOTED_IDENTIFIER)`, `make grammar`, and `unquoteIdentifier` it in the visitor. On output, quote via `executor.mdlIdent` (which lexes the name and quotes only when it does *not* lex as a bare `IDENTIFIER` — no hardcoded keyword list, no false positives like a widget named "Dot"). For other strict-`IDENTIFIER` name positions apply the same pattern: `pageParameter`/`snippetParameter` bare names were widened the same way (issue #114 — note the SHOW_PAGE colon arg it reported already worked via `identifierOrKeyword`, and DESCRIBE emits params `$`-prefixed so output was already safe; the fix only closed the bare-declaration gap). Security `read(...)`/`write(...)` member lists and the validation-rule `attributeReference` were widened to `(IDENTIFIER \| QUOTED_IDENTIFIER)` via dedicated sub-rules (`entityMemberName`, `attributeRefSegment`); the grant DESCRIBE emitter quotes members through `mdlIdent` for a clean roundtrip (issue #675). Issues #619, #114, #675 | -| `DESCRIBE` of a navigationlist item (`item List`), a fragment widget (`container List`), or a workflow `user task Value` / `jump to Value` emits the name BARE and the output fails `mxcli check` (`mismatched input … expecting IDENTIFIER`) — same root cause as #619 but four further emitter positions the widget-name slice didn't reach | These four output sites interpolated the name directly instead of via `mdlIdent`. Unlike the strict-`IDENTIFIER` cases, the receiving grammar rules already accept `(IDENTIFIER \| QUOTED_IDENTIFIER)` (`widgetV3` ITEM, `widgetV3`, `workflowUserTaskStmt`, `workflowJumpToStmt`), so **no grammar change is needed** — wrapping the emitted name is sufficient | `mdl/executor/cmd_pages_describe_output.go` (`item %s`, ~line 633), `mdl/executor/cmd_fragments.go` (`outputASTWidgetMDL`, ~line 169), `mdl/executor/cmd_workflows.go` (`jump to %s` ~line 321 + `formatUserTask` ~line 418) | Wrap each name in `executor.mdlIdent(...)`. Regression tests in `mdl/executor/issue619_emitter_quoting_test.go`; roundtrip fixture `mdl-examples/bug-tests/619-quoted-reserved-emitter-names.mdl`. The *other* class of #619 gaps (page-domain `microflowArgV3` show_page/microflow arg param names, `dataSourceExprV3` SELECTION ref, `sortSpec` list-sort attr) also needed a grammar widen — now done: each rule widened to `(IDENTIFIER \| QUOTED_IDENTIFIER)` (`make grammar`), the visitor unquotes (`buildMicroflowArgV3`, `buildDataSourceV3` SELECTION, both `sortSpec` consumers), and the emitters quote via `mdlIdent` (`extractMicroflowParameters`/`extractNanoflowParameters`/`extractPageParameters`, `DataSource: selection`, list `sort(...)`, plus the fragment `selection` builder). Tests: `mdl/visitor/visitor_page_quoted_test.go` (`TestQuotedReservedSelectionAndArgNames`), `mdl/visitor/visitor_microflow_sort_quoted_test.go`, `cmd_microflows_format_listop_test.go`; fixture `mdl-examples/bug-tests/619-quoted-reserved-grammar-positions.mdl`. NOTE a reserved attribute like `Date` in a list `sort(...)` was *already* emitted bare and broken before this — the fix quotes it. Issue #619 | -| Under the default modelsdk engine a SHOW/DESCRIBE errors `… not implemented yet — rerun with MXCLI_ENGINE=legacy`, OR the read "works" but DESCRIBE drops nested config (return type, parameters, exposed-as info) into a half-shell that's fine under `--engine legacy` | The modelsdk `Backend` never overrode the read method (falls through to the generated `unimplemented` embed), OR a gen-accessor converter read children under the codegen property names while real Studio-Pro BSON stores them under the original storage names — the gen codec decodes those keys into empty lists/parts | `mdl/backend/modelsdk/*_read.go` (add the override) + `mdl/backend/modelsdk/unimplemented_gen.go` (confirm the stub) | First confirm the gap: `comm -23 <(grep -hoP 'errUnimplemented\("\K[^"]+' mdl/backend/modelsdk/unimplemented_gen.go \| sort -u) <(grep -rhoP 'func \([a-z]* \*Backend\) \K[A-Za-z0-9_]+' mdl/backend/modelsdk/*.go \| grep -v _test \| sort -u)`. Then **dump the real raw keys before writing the converter** (a gen-accessor port silently half-shells): iterate `b.ListRawUnitsByType(typePrefix)` + `b.GetRawUnit(id)` and read the fidelity-sensitive children from the empirical keys, not the gen accessors. JS actions store `JavaReturnType`/`Parameters`/`TypeParameters`/`MicroflowActionInfo` (gen wants `ActionReturnType`/`ActionParameters`/`ActionTypeParameters`/`ModelerActionInfo`) — see `javascript_read.go`. Validate with describe-parity vs `--engine legacy` (modelsdk may be *more* correct, e.g. recovers `Enum X` where legacy drops to `Object`). Issue 7 | -| `DESCRIBE PAGE` / `DESCRIBE STYLING` silently drops a **compound** (nested) design property — e.g. Atlas `Spacing` → margin-top/bottom — even though it was written to BSON correctly and shows in Studio Pro; only flat toggle/option props survive the read-back | The describe-side parser `extractDesignProperties` had no `Forms$CompoundDesignPropertyValue` case (dropped it entirely), and the emitters had no `compound` branch — the write half of #668 was done but the read/roundtrip half wasn't | `mdl/executor/cmd_pages_describe_parse.go` (`extractDesignProperties` → `parseDesignProperty`) + `cmd_pages_describe_output.go` (`formatDesignPropertiesMDL` → `joinDesignPropertyEntries`) + `cmd_styling.go` (DESCRIBE STYLING emitter) | Parse `Forms$CompoundDesignPropertyValue` by recursing over its `Properties` list (each child is again a `Forms$DesignPropertyValue`) into `rawDesignProp.Nested`; emit recursively as `'Key': ['sub': 'v', …]`. Share one formatter (`joinDesignPropertyEntries`) across both describe paths so toggle/option/compound render identically. Verify with a write→`describe`→re-`check` roundtrip (the describe output must re-parse). When a feature writes a construct, always confirm the **describe read-back** too — write-only completeness is the recurring half-shell trap. Issue #668 | -| `mxcli -p "C:\\path\\App.mpr"` (Windows backslash path) fails with `unable to open database file (14)` and the echoed path is mangled (`C:\temp` → `C:emp`, backslashes dropped); forward slashes (`-p C:/path/App.mpr`) work | The CLI fabricates `CONNECT LOCAL ''` MDL from the flag value and re-parses it, so the lexer's `unquoteString` interprets `\t`/`\n`/`\r` and escape backslashes in the path. The positional `.mdl` arg is a raw path, so it's unaffected | every `cmd/mxcli/*.go` `fmt.Sprintf("CONNECT LOCAL '%s'...", projectPath)` site + `mdl/visitor/visitor_helpers.go` | Never interpolate a raw filesystem path into MDL source. Wrap it in `visitor.QuoteString(projectPath)` (escapes `\`→`\\`, `'`→`''` — the exact inverse of `unquoteString`) at every CONNECT-string site. Round-trip test in `mdl/visitor/visitor_connection_test.go`. Issue #644 | -| `ALTER ENTITY ... ADD ATTRIBUTE` (or any ALTER) succeeds and `DESCRIBE ENTITY` is byte-identical before/after, but `mx check` then reports CE1613 "attribute … no longer exists" — often on a *different* entity's inherited system member (`createdDate`) referenced by grids/sort-bars in another module | The ALTER target is rebuilt from the semantic `domainmodel.Entity` (`entityToGen` → fresh `NewEntity`, `raw==nil`), so the codec's `EmitGUID` default writes `GUID = $ID`, discarding the entity's real on-disk GUID. The GUID is the stable cross-reference identity inheriting entities/pages resolve members through, so changing it dangles those refs. The forgiving reader re-derives DESCRIBE from flags and never shows GUID, so mxcli can't see its own damage. (Legacy `sdk/mpr` had the broader form: it re-serializes the *whole unit* and forces `GUID = $ID` on every sibling too) | `mdl/backend/modelsdk/domainmodel_alter.go` → `UpdateEntity` | Transplant the original element's raw bytes onto the rebuilt target (`ge.SetRaw(orig.Raw())`) so the codec treats it as an EXISTING element: dirty properties re-encode from `ge`, unmodeled fields (GUID, Capabilities, …) pass through verbatim. Siblings already survive via the list-rebuild raw passthrough. When a write "succeeds" but `DESCRIBE` is suspiciously byte-identical, suspect an identity (`$ID`/`GUID`) the reader doesn't surface — assert on raw BSON, not DESCRIBE. Issue #657 | -| `mx check` reports CE6621 "Attribute 'X' has max length '200' which is not the same as max length in the OData service which is 'unlimited'" on every unlimited-length string attribute of a `create external entities from …` import — **only under the default modelsdk engine**; `--engine legacy` is clean | The codec's encoder only emits *dirty* properties for a new element. `attributeTypeToGen` skipped `SetLength` when `Length == 0` (unlimited), so the `Length` property stayed non-dirty and the field was omitted from the BSON entirely. Studio Pro then applies its own UI default of 200, which contradicts the OData service's "unlimited" type. Legacy's `writer_domainmodel.go` always writes `Length: t.Length` (including 0), so it never regressed | `mdl/backend/modelsdk/domainmodel_write.go` → `attributeTypeToGen` (`*domainmodel.StringAttributeType` case) | Always call `g.SetLength(int32(at.Length))` — drop the `if at.Length > 0` guard — so `Length: 0` is emitted explicitly, matching the legacy serializer. General rule for the codec write path: any field whose *zero value is meaningful* (0 = unlimited here) must be Set unconditionally, because an omitted field falls back to Studio Pro's own default, not to zero. Assert on the serialized `NewType` map (not DESCRIBE) — the reader treats a missing Length as unlimited so it hides the damage. Test `TestEntityToGen_ODataRemoteEntitySource` in `external_entity_write_test.go`. Issue #718 | -| `mxcli oql` against a docker-deployed app returns **0 rows** for tables that clearly have data (false negative — tester concluded "saving is broken") | NOT a database-config problem (that's a red herring — proven: aligning the runtime to Postgres with data still returned 0). The OQL preview servlet (`/dev/preview_execute_oql`) is registered by `com.mendix.basis.livepreview.LivePreviewRegistry` → `AppContainer.addDevelopmentServlet`, which **only mounts the servlet when the JVM system property `mendix.running.locally.by.studiopro=true` is set** (Studio Pro sets it; a deployed `bin/start` PAD doesn't). Without it the endpoint returns HTTP 200 `{"result":-5,"message":"Action not found"}`, which mxcli silently parsed as empty data. (Found by decompiling `com.mendix.mxruntime.jar` / `com.mendix.appcontainer.jar`.) | `cmd/mxcli/docker/templates/docker-compose.yml` (the `command`) + `cmd/mxcli/docker/oql.go` (`oqlDevError`) | Add `-J -Dmendix.running.locally.by.studiopro=true` to the start command (each JVM opt needs its own `-J`), next to the existing `-Dmendix.live-preview=enabled` — this mounts the dev servlets and `mxcli oql` returns live data (verified end-to-end; DTAP mode is NOT required, DB config is irrelevant — the preview runs in-process against the runtime DataStore). Separately, make `oqlDevError` surface `{"result":<0,"message":…}` (not just `{"error":…}`) so the failure isn't swallowed as 0 rows. **Related startup bug found:** the generated `etc/Default` inherits the project's ports, which are `0` when unset → the runtime rejects port 0 and won't start; `patchRuntimePorts` (patch.go) forces `runtime.http { port = 8080 }` / `admin { port = 8090 }`. Verified with a full docker build/run/seed cycle | -| `check --references` on a view entity reports `could not parse select clause from OQL query` for a valid OQL query (any `select … from …`), AND MDL031 OQL type-mismatch checks never fire (misses real errors) | `extractSelectClause` searched an **uppercased** query (`upperOql`) for the **lowercase** needle `"select"`, and compared `strings.ToUpper(oql[i:i+4])` against lowercase `"from"`/`"union"` — case mismatches that never match, so it returned `""` for *every* query. That single `""` both triggers the false-positive warning (`inferOQLTypes`) and short-circuits `ValidateOQLTypes` (early `return` on empty select clause) so no type checking runs | `mdl/executor/oql_type_inference.go` → `extractSelectClause` | Compare case-consistently: `strings.Index(upperOql, "SELECT")` and slice keyword comparisons from `upperOql` (`word := upperOql[i:i+4]; word == "FROM"`). The static inferrer stays conservative (division / attribute refs → `TypeUnknown` → skipped), so re-enabling it doesn't over-fire on case/division aggregates. Tests: `TestExtractSelectClause`, `TestValidateOQLTypesNoFalsePositive` in `oql_type_inference_test.go`. Bug 9b | -| `mxcli widget docs` omits all but the first widget of a bundled multi-widget `.mpk` (e.g. Charts.mpk emits only `areachart.md`; ColumnChart/BarChart/PieChart/LineChart/BubbleChart missing) | Same class as #679 in an unfixed code path: `RegenerateWidgetDocs` used `mpk.ParseMPK` (returns `WidgetFiles[0]` only), while the def-generation loop (`RefreshWidgetDefinitions`) already used `ParseMPKAll`. Charts.mpk bundles 10 widgetFiles | `mdl/executor/widget_defs.go` → `RegenerateWidgetDocs` | Swap the per-`.mpk` `ParseMPK` for `ParseMPKAll` and loop over every returned `mpkDef` (mirror the def-gen loop). Test `TestRegenerateWidgetDocsMultiWidgetMPK` (uses the `testdata/expr-checker/widgets/Charts.mpk` fixture). Bug 9a | -| Chart `series` datasource/attribute sub-properties don't parse — `staticDataSource: database from View` → `extraneous input 'from'`; and even if parsed, were silently dropped (`buildObjectListItem` `default: continue` for datasource) | Object-list item sub-properties of datasource/attribute type had no grammar, visitor, or executor support (deferred at #538). The **BSON layer already handled it** (`widgetobj/builder.go` `overlayItemValue` datasource case) — only the upstream plumbing was missing | grammar `mdl/grammar/domains/MDLPage.g4` (`widgetPropertyV3` generic datasource alt + `propertyValueV3` QUOTED_IDENTIFIER) + visitor `mdl/visitor/visitor_page_v3.go` (generic datasource branch + quoted value) + executor `mdl/executor/widget_engine.go` (`buildObjectListItem` pre-pass, `datasource` case, `DataSource:` alias, empty-CT emission) + `mdl/backend/mutation.go`/`widgetobj/builder.go` (`EmptyTemplate` flag) + `mdl/executor/widget_defs.go` (`ItemPropertyMapping.DataSource` from `PropertyDef.DataSource`, bump `WidgetDefGeneratorVersion`) | Add `(IDENTIFIER\|keyword) COLON dataSourceExprV3` (before scalar generic branches; leading DATABASE/MICROFLOW/etc token disambiguates from `propertyValueV3`) + `QUOTED_IDENTIFIER` to `propertyValueV3`; `make grammar`. Visitor stores the datasource under the property name and unquotes quoted values. In `buildObjectListItem`: a pre-pass resolves datasource sub-props (setting the item's entity context so `staticXAttribute` resolves against the SERIES' own datasource, not the parent); the friendly `DataSource:` alias routes to `staticDataSource`/`dynamicDataSource` by dataSet mode; emit the pre-built datasource from the `prebuiltDataSources` map (not the child.Properties lookup, which misses the alias). **CE0463 nuance**: a VISIBLE-but-unset texttemplate series sub-prop (staticName always in static mode; static·/dynamic· names once their datasource is configured) must serialize an empty `Forms$ClientTemplate`, not null. Visibility = dataSet-mode gate (from editorConfig `hideNestedPropertiesIn`) + dataSource-binding gate; scoped to SERIES/LINE containers so Accordion `GROUP`/DataGrid columns are untouched. **Diagnose CE0463** with the update-widgets diff: dump the page, `mx update-widgets`, dump again, `diff` the series WidgetObject — the delta was one `TextTemplate: null` → empty CT. The residual chart CE0463 (bare chart too, on a version-mismatched mxbuild) is orthogonal WidgetType drift (#529/#600) that `mxcli docker check`/`build` fix via update-widgets. Tests: `chart_series_test.go`, `visitor_chart_series_test.go`; bug-test `mdl-examples/bug-tests/bug9-chart-series-datasource.mdl`; spec `mdl-examples/doctype-tests/34-chart-widget-examples.mdl`. Bug 9a | -| An **invalid enumeration value** on a pluggable-widget object-list sub-property passes `mxcli check` but silently defaults in Studio Pro/mxbuild — e.g. a Maps marker `LocationType: 'coordinates'` (real values are `address`/`latlng`) defaults to `address` → CE "a dynamic marker requires an address"; a chart series `Interpolation: 'wobbly'` degrades. The value is dropped with no diagnostic | The def.json never captured a property's enumeration member keys, so `check` had nothing to validate against. (This is *not* a persistence bug — a **valid** enum value round-trips fine.) | mpk parser `sdk/widgets/mpk/mpk.go` (`PropertyDef.EnumValues` from ``) + `mdl/executor/widget_defs.go` (`ItemPropertyMapping.EnumValues`, bump `WidgetDefGeneratorVersion`) + validator `mdl/executor/validate_widgets.go` (`validateObjectListItemEnums`) | Capture `` keys into `PropertyDef.EnumValues` at all three PropertyDef build sites; carry into `ItemPropertyMapping.EnumValues`; add **MDL-WIDGET08**: for each object-list-item enum sub-property present in the AST, flag a value (case-insensitive) outside the member set, listing the valid values. Thread the parent's `ObjectListMapping` into `validateWidgetTreeIn` (was just the container-keyword set). Tests: `TestValidateObjectListItemEnums`, `TestParseMPK_CapturesEnumValues` (the MDL-WIDGET08 check needs project context/`-p`, so it can't be a `.fail.mdl` — `make check-mdl` runs those syntax-only; example `32-pluggable-widget-object-lists-v010.mdl` OL06/OL07 exercise the valid `latlng` path end-to-end). NOTE: top-level (non-object-list) widget enum props aren't validated yet — `PropertyMapping` doesn't carry `EnumValues`. Bug 9a follow-up | -| `ALTER ENTITY … MODIFY ATTRIBUTE X: Type NULLABLE` reports "Modified attribute" but the attribute stays NOT NULL — no way to make a required attribute optional. Any specified constraint (NOT NULL / UNIQUE / DEFAULT) is likewise ignored; there is no `NULLABLE` keyword, and it "parsed" only because trailing tokens were swallowed | Three-layer drop: (1) the visitor's MODIFY branch captured only `CALCULATED`; (2) `AlterEntityStmt` had no constraint fields; (3) the executor only set `attr.Type`. NOT NULL / UNIQUE are stored as **entity ValidationRules** (`Type:"Required"`/`"Unique"`, keyed by AttributeID), not attribute flags — so making nullable means **removing** the Required rule. Gotcha: the modelsdk read sets `ValidationRule.AttributeID` to the qualified name (`Mod.Ent.Attr`) while a created attr's ID is a UUID, so matching by ID alone fails | grammar `mdl/grammar/MDLLexer.g4` (`NULLABLE` token) + `MDLDomainModel.g4` (`attributeConstraint`) + `MDLSettings.g4` (keyword lists) + AST `mdl/ast/ast_entity.go` (`ModifyNotNull *bool` etc.) + visitor `mdl/visitor/visitor_entity.go` (MODIFY constraint switch) + executor `mdl/executor/cmd_entities.go` (`setAttributeValidationRule`, `ruleTargetsAttribute`) | Add a real `NULLABLE` token (+ to both `keyword` lists so it stays a usable identifier) and an `attributeConstraint` alt; `make grammar`. **Semantics: apply-specified, preserve-unspecified** (non-breaking — the documented "DEFAULT survives a type change" behavior stays): NULLABLE→`ModifyNotNull=&false` (drop Required rule), NOT NULL/REQUIRED→`&true` (re-add), UNIQUE→add Unique rule, DEFAULT→set value; nil pointer = leave as-is. Match the existing rule via `ruleTargetsAttribute` (UUID **or** last-dot-segment name, since read-back rules use the qualified name); store new rules with the qualified name (`validationRuleToGen` writes a dotted AttributeID verbatim). Verify with `describe entity` + `mx check`=0. Bug-test `mdl-examples/bug-tests/bug12a-modify-attribute-nullable.mdl`; tests `TestModifyAttributeConstraintsCaptured`, `TestRuleTargetsAttribute`, `TestSetAttributeValidationRule`. Bug 12a | -| `MOVE ENUMERATION … TO FOLDER` prints "Moved … to new location" but `DESCRIBE ENUMERATION` and `CATALOG.ENUMERATIONS.Folder` still show the module root (looks like a silent no-op) | **NOT a write bug** — the move persists (the enum's `Unit.ContainerID` becomes the folder; `show enumerations` shows it correctly). Two *read* paths misreported it: (1) `buildEnumerations` hardcoded the catalog Folder to the module name (`// Folder as module name for now`) instead of `buildFolderPath` — same latent bug in `builder_microflows.go`/`builder_rest.go`/nanoflows; (2) DESCRIBE ENUMERATION emitted no `FOLDER` clause (unlike DESCRIBE PAGE), and CREATE ENUMERATION had no folder grammar to round-trip one | catalog `mdl/catalog/builder_modules.go` (+ microflows/rest) + grammar `MDLDomainModel.g4` (`enumerationOption` `FOLDER`) + AST `ast_enumeration.go` (`Folder`) + visitor `visitor_enumeration.go` + executor `mdl/executor/cmd_enumerations.go` (`execCreateEnumeration` via `resolveFolder`, `describeEnumeration` emit) | Catalog: replace hardcoded `moduleName` Folder with `b.hierarchy.buildFolderPath(x.ContainerID)`. Grammar: add `FOLDER STRING_LITERAL` to `enumerationOption` (parity with pages' `Folder:`); place the enum via the shared `resolveFolder(ctx, moduleID, folder)`; DESCRIBE emits `) FOLDER 'path';` when `BuildFolderPath != moduleName`. **Diagnosis tip**: when a MOVE "doesn't persist," check the raw `Unit.ContainerID` and `show ` before assuming a write bug — a hardcoded catalog column or a missing DESCRIBE clause is a read-path illusion (cf. #722). Bug-test `bug12b-enumeration-folder.mdl`; tests `TestCreateEnumerationFolderCaptured`. Bug 12b | -| `CREATE PAGE` with a widget `Action: nanoflow …` fails on the **default (modelsdk)** engine: `client action *pages.NanoflowClientAction not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy` (legacy works). Blocks any nanoflow-triggered button/link written via MDL | `clientActionToGen` had cases for save/cancel/close/delete/page/microflow/create-object client actions but **no `*pages.NanoflowClientAction` case** → hit the `default` refuse-loudly branch. (The sibling microflow-arg drop was the separate Bug 1, already fixed on `microflowSettingsToGen`.) | `mdl/backend/modelsdk/widget_write.go` → `clientActionToGen` | Add a `*pages.NanoflowClientAction` case building a gen `CallNanoflowClientAction`. Unlike the microflow action (which nests a `Forms$MicroflowSettings`) it carries the Nanoflow name + `ParameterMappings` **directly**: `SetNanoflowQualifiedName`, `SetProgressBar("None")`, `SetDisabledDuringExecution(true)`, and one `NanoflowParameterMapping` per arg (`SetParameterQualifiedName(nanoflow+"."+param)` BY_NAME + `SetExpression($var)`); register `Forms$CallNanoflowClientAction` TypeDefaults (`MandatoryLists:[ParameterMappings]`, `NullFields:[ProgressMessage,ConfirmationInfo]`). Mirror the legacy `writer_widgets_action.go`. The modelsdk **read** already round-trips it (describe shows `nanoflow M.NF(Form: $Form)`). Verify both engines with `mx check`=0. Bug-test `bug2-modelsdk-nanoflow-widget-action.mdl`; tests `TestNanoflowClientAction_Serialized` (+ `TestMicroflowClientAction_PersistsParameters` guards Bug 1's mapping stays wired through the shared helper). Bug 2 | -| A `create page` over a layout with **more than one placeholder** (e.g. Main + Right/Topbar) can only populate Main — there is no MDL syntax to target a second placeholder, so the layout renders wrong | The page body was a flat widget list; the executor built exactly **one** `Forms$FormCallArgument` for the Main placeholder (`getMainPlaceholderRef` → hardcoded `".Main"`). NOT a BSON/engine gap — placeholders are BY-NAME (`Parameter="."`) and `layoutCallToGen` already iterates *multiple* arguments; the whole gap was upstream (grammar/AST/visitor/executor produced a single arg). DESCRIBE also *flattened* every argument's widgets into one list | grammar `mdl/grammar/domains/MDLPage.g4` (`placeholderBlockV3`) + AST `mdl/ast/ast_page_v3.go` (`PagePlaceholderV3`) + visitor `mdl/visitor/visitor_page_v3.go` (`buildPagePlaceholdersV3`) + executor `mdl/executor/cmd_pages_builder_v3.go` (one FormCallArgument per placeholder) + describe `mdl/executor/cmd_pages_describe.go` (`getPageWidgetGroupsFromRaw`) | Add a `placeholder { … }` block (name = `identifierOrKeyword` so `Right`/`Left`/`Content` parse); bare widgets still bind to Main (backward-compatible). `make grammar`. Executor groups widgets by placeholder (Main = bare body + any `placeholder Main` block) and emits one arg each with `ParameterID = "."` and a numbered `conditionalVisibilityWidget` wrapper. DESCRIBE groups by argument's `Parameter` and emits explicit `placeholder` blocks only when >1 placeholder has widgets (single-Main stays bare → no test churn). Verified both engines `mx check`=0; stable describe→exec→describe round-trip. Bug-test `mdl-examples/bug-tests/532-page-layout-placeholders.mdl`; tests `TestPagePlaceholderBlocksParsed`, `TestPageWithoutPlaceholdersUnaffected`. Layout placeholder names come from the layout's `Forms$Placeholder` widgets (e.g. Atlas_SideBar = Main + Topbar). Issue #532 | -| `GRANT VIEW ON PAGE Module.Page TO Module.Role` prints success but the page's allowed roles appear to stay empty: `mxcli lint` still reports `CE0557`/`MPR007` ("used as home page … but has no allowed roles") across a freshly rebuilt catalog, while entity-access grants on the same roles verify fine (`describe entity` shows them). Reported on **modelsdk** engine (default), Mendix 11.11 | NOT a write bug — the grant **does** persist (`AllowedModuleRoles: [3, "Module.Role"]` on disk; `mx check` = 0 errors; Studio Pro would load clean). The regression was on the **read** path: modelsdk `pageFromGen` didn't populate `Page.AllowedRoles`, and `mxcli lint`'s CE0557 rule counts `len(pg.AllowedRoles)` from `ListPages()` — so it read every page as having zero roles and false-fired. `SHOW ACCESS ON PAGE` / `SHOW SECURITY MATRIX` under-reported the same way. The entity-vs-page asymmetry the reporter saw = different read paths (entity `describe` worked; page `lint` didn't), not different writes | `mdl/backend/modelsdk/page.go` → `pageFromGen` | Already fixed by `88a2d50b1` (issue #722): populate `out.AllowedRoles` from the gen `AllowedRolesQualifiedNames()`, mirroring `microflowFromGen`. Guard for the reporter's exact end-to-end flow: `TestUpdateAllowedRoles_PagePersistsForLintRead` (`mdl/backend/modelsdk/page_roles_test.go`) writes a page role then reopens fresh and asserts the lint read path (`ListPages`→`AllowedRoles`) sees it. **Diagnosis tip**: when a write "reports success but the value stays empty" *only in a read/lint/show command*, dump the raw unit (`mxcli bson dump`) — if the bytes are there, it's a read-adapter (`*FromGen`) gap, not a write bug; confirm by reverting the suspected read commit and re-running | -| `MOVE ENTITY X to OtherModule` where a **view entity's OQL** joins through `X`: passes `mxcli check`, but `mx check` on **Mendix 10.x** reports `CE0174 "…path '…/DmTest.Customer' does not resolve to an entity"` at the view entity — **only on the modelsdk engine**, only on 10.x (11.x is clean; legacy is clean) | Mendix <11 stores a view entity's OQL in **two** places: the top-level `DomainModels$ViewEntitySourceDocument` unit AND inline on the entity's `DomainModels$OqlViewEntitySource` (see `entityToGen`, gated `major < 11`). modelsdk's `UpdateOqlQueriesForMovedEntity` rewrote only the source document, leaving the inline copy pointing at the old module. 11.x doesn't write the inline copy (OQL lives only on the source doc), so it was never affected — hence 10.x-only. Legacy updates the inline copy incidentally by re-serializing the domain model | `mdl/backend/modelsdk/move_view_write.go` → `UpdateOqlQueriesForMovedEntity` | After the ViewEntitySourceDocument sweep, also iterate every domain model (`ListDomainModels` → `loadDomainModelGen`), find each entity whose `Source()` is `*genDm.OqlViewEntitySource` with an `Oql()` containing the old qualified name, `SetOql(strings.ReplaceAll(...))`, and `persistDM`. Mirrors the `UpdateEnumerationRefsInAllDomainModels` walk. **Diagnosis tip**: when a value updates in one storage location but a stale copy elsewhere still breaks `mx check`, dump raw `mprcontents` and grep for the value across `$Type`s — Mendix often duplicates a field (OQL, captions) and a version gate (`major < 11`) can make one copy version-conditional. Repro: `mdl-examples/bug-tests/move-entity-oql-view-inline-ref.mdl` | -| `MOVE ENTITY` / `MOVE ENUMERATION` cross-module prints `Warning: Could not update OQL queries` / `Could not update enumeration references: load domain model 00000000-0000-0000-0000-000000000002: open …/mprcontents/00/00/00000000-…002.mxunit: no such file or directory` — **modelsdk engine only**. The move itself succeeds and (today) user modules are still updated, because `ListDomainModels` appends the failing System DM **last** — but the warning is spurious and the ordering dependence is fragile | Both reference-update sweeps iterate `ListDomainModels()`, which **injects the virtual System-module domain model** (ID `…002`, `buildSystemDomainModel`) so platform entities resolve. Each sweep then re-loads every listed DM from disk via `loadDomainModelGen(info.ID)` — but the System module isn't stored in `mprcontents`, so its `.mxunit` read fails. Legacy never hit this: it walks in-memory `*domainmodel.DomainModel` objects and never re-reads from disk | `mdl/backend/modelsdk/association_move_write.go` → `UpdateEnumerationRefsInAllDomainModels`; `mdl/backend/modelsdk/move_view_write.go` → `UpdateOqlQueriesForMovedEntity` | Skip the System DM at the top of each DM loop: `if string(info.ID) == meta.SystemDomainModelID { continue }` (import `modelsdk/meta`). It never holds user enum refs or user view-entity OQL, so skipping is correct and removes the disk-read failure regardless of iteration order. **Diagnosis tip**: a "no such file / not found" on the fixed ID `…002` (or module `…001`) means code is trying to load the *virtual* System unit from disk — filter it out, don't try to read it. Regression: `TestUpdateRefsForMovedEntity_SkipsSystemDomainModel` (`move_refs_system_test.go`) | -| A page whose widget has a **conditional visibility/editability** setting fails to **load** on **Mendix 11.12** — `StorageLoadException: Conditional editability settings … has an invalid value '' for property Attribute. The text ' ' is not a valid AttributeIdentifier` (the `' '` is `\x03\x00\x00\x00` = an int32 list marker; sometimes the value is the object's own `$Type` string). **Both engines**, ≤ 11.11 tolerated it. Reproduces only in the full doctype gate, not minimally — a red herring: it looked like `mx update-widgets` (harness step) was the cause, but the corruption is on disk from mxcli. My pymongo scans missed it because the value is a non-string type / the reader mis-parses | mxcli serialized `Forms$ConditionalVisibilitySettings` / `ConditionalEditabilitySettings` `Attribute` as **BSON null** when there's no attribute-based condition. Studio Pro writes the **empty string `""`** (Attribute is a BY_NAME `AttributeIdentifier`). 11.12's stricter streaming reader rejects the null and mis-reads the following bytes. Found by comparing a Studio-Pro-authored settings node (`str ""`) against mxcli's (`NoneType`) | legacy: `sdk/mpr/writer_widgets.go` (`serializeConditionalVisibility`/`serializeConditionalEditability`); modelsdk: `mdl/backend/modelsdk/widget_write.go` (RegisterTypeDefaults) + new `codec.TypeDefaults.EmptyStringFields` | Emit `Attribute: ""` not null. Legacy: change the `bson.D` value. Modelsdk: move `"Attribute"` from `NullFields` to the new `EmptyStringFields` (encoder emits `""`). **Diagnosis pattern**: when mx reports a corrupt value that pymongo *can't see on disk*, and the value looks like a `$Type` string or `\x03\x00\x00\x00`, suspect a wrong BSON *type* for a field (null where a string is required) that the streaming reader mis-aligns — compare field types against a Studio-Pro-authored node, don't just compare key presence. Repro: `mdl-examples/bug-tests/conditional-settings-attribute-empty-1112.mdl` | -| A CLOSE PAGE microflow/nanoflow activity (`close page;`) fails `mx check` on **Mendix 11.12** with `CE0117 "Error(s) in expression"` at the Close page activity — **legacy engine only** (modelsdk passes; ≤ 11.6 tolerated it). Any annotation (`@caption`/`@color`/`@position`) on the activity reliably exposes it | The legacy `sdk/mpr` writer serialized the `Microflows$CloseFormAction` page-count under the field name `NumberOfPagesToClose`, but the metamodel storage name is `NumberOfPages` (the codec/modelsdk already used it — found by diffing the two engines' BSON: `legacy=NumberOfPagesToClose, modelsdk=NumberOfPages`). 11.12 doesn't recognise the old name, so the real `NumberOfPages` field is absent → defaults to an empty expression → CE0117 | `sdk/mpr/writer_microflow_actions.go` (`*microflows.ClosePageAction` case) + `sdk/mpr/parser_microflow_actions.go` | Write `NumberOfPages` (not `NumberOfPagesToClose`); the reader accepts both (`NumberOfPages` first, legacy `NumberOfPagesToClose` fallback) for round-trip fidelity on existing projects. **Diagnosis pattern**: a CE that says "invalid/empty expression" on an activity with no visible expression usually means a *renamed/missing* field defaulting to empty — diff the passing engine's BSON against the failing one to spot the field-name drift. Repro: `mdl-examples/bug-tests/close-page-numberofpages-1112.mdl` | -| On **Mendix 11.12**, `mx check` fails to load projects on the **legacy** engine for several doctypes (`01-domain-model`, `10-odata`, `14-project-settings`, `22-published-rest`) with `System.InvalidOperationException: Expected '$ID' as the first property of a storage object, but got 'X'` at `StreamingBsonUnitReader`. The bad first-key varies run-to-run (`ThemeModuleOrder`, `EnableRspackBundler`, `Oql`, `MarkAsUsed`, `PopupResizable`…) — the tell-tale of random map-key order. modelsdk passes; ≤ 11.11 tolerates any order | Several legacy `sdk/mpr` writers preserve round-trip fidelity by carrying parsed subtrees as Go maps (`ProjectSettings.RawParts`, ref-marking `raw`, rename `raw`, domain-model `raw`) and marshalling them back. `bson.Marshal` emits **map keys in random order**, so `$ID` only lands first by luck. 11.12 rejects any storage object whose first key isn't `$ID`. The earlier `$ID`-first fix (bson.D reorder in writer_domainmodel/security) missed every map-passthrough site | `sdk/mpr/writer_settings.go` (+ `writer_refs`, `writer_rename`, `writer_domainmodel`, `writer_odata`, `writer_rest`, `writer_customblob`, `writer_security`, `writer_modules`, `writer_microflow`, … — all unit-boundary marshals) via new `sdk/mpr/writer_order.go` | Wrap every unit-boundary `bson.Marshal(doc)` in `marshalUnitIDFirst(doc)` = `bson.Marshal(bsonutil.HoistStorageID(doc))`. **Use the non-sorting `HoistStorageID`, NOT `OrderStorageValue`**: `OrderStorageValue` *sorts* the non-`$ID` keys, which corrupts template-derived pluggable-widget/datagrid page trees (mx then aborts with `got 'LabelTemplate'`, regressing `29-datagrid`/`33-alter-page`). `HoistStorageID` only lifts `$ID`/`$Type` to the front and preserves every other key's order → no-op on already-correct writers, fixes only the map-passthrough ones. **Do NOT normalize `writer_pages.go` or the page/workflow mutators** — their output is already `$ID`-first and they embed delicate widget maps the hoist would sort-corrupt. Verify on 11.12 with the doctype gate; regression-check `29`/`33` specifically. (Left unfixed: `03-page` LabelTemplate — a separate pre-existing issue where on-disk bytes read `$ID`-first yet mx still objects) | -| A `NUMBERFILTER` in a DataGrid column passes `mxcli check` but corrupts the `.mpr`: MxBuild / Studio Pro **fails to load** the whole project on **Mendix 11.12** with `System.InvalidOperationException: Type …CustomWidgets.WidgetProperty does not contain a constructor with a parameter of type …CustomWidgets.WidgetValue` at `StreamingBsonUnitReader.ConstructObject`. `TEXTFILTER`/`DATEFILTER`/`DROPDOWNFILTER` in identical grids load fine; ≤ 11.11 loads even the number filter. Isolating it needs dropping widgets one-by-one under `mx check` | The embedded `datagrid-number-filter.json` template authored its 3 placeholder / screen-reader `Forms$ClientTemplate` blocks with **markerless empty arrays** — `"Items": []` and `"Parameters": []`. Every Mendix list serializes with a leading marker int (`Texts$Text.Items`→`[3]`, `ClientTemplate.Parameters`→`[2]`, `Widgets`/`Objects`→`[2]`); a bare `[]` has no marker. 11.12's stricter streaming reader mis-parses the markerless array and mis-associates the following bytes, so a nested `WidgetValue` lands where the reader is constructing a `WidgetProperty`. The template shape is otherwise identical to the working filters — the only diff is the missing markers (the blocks had hand-authored `dd2b3c4d…` placeholder IDs, i.e. not cleanly extracted from Studio Pro) | `sdk/widgets/templates/mendix-11.6/datagrid-number-filter.json` **and** `modelsdk/widgets/templates/mendix-11.6/datagrid-number-filter.json` (both engines embed their own copy) | Add the marker int to every empty `TextTemplate` array: `"Items": []`→`"Items": [3]`, `"Parameters": []`→`"Parameters": [2]`. Rebuild (`go:embed`), re-exec, verify with `scripts/mx-check.sh --version 11.12.0` (0 errors; regression-check 11.9 still clean). Guard: `TestTemplates_NoMarkerlessEmptyArrays` (in both `sdk/widgets` and `modelsdk/widgets`) walks every embedded template and fails on any bare `[]` — a Mendix list must always carry its marker. Repro: `mdl-examples/bug-tests/datagrid-numberfilter-array-marker.mdl`. **Diagnosis tip**: when a widget "passes check, fails 11.12 load" with the WidgetProperty/WidgetValue constructor error, suspect a markerless empty array in the template (shape-diff the failing widget's template against a working sibling; the smoking gun is `[]` vs `[]`) | -| `mxcli check --references` flags `attribute 'Type' is a reserved word (CE7247) [MDL021]` even though the attribute is **quoted** (`"Type": String`), and the skills say "always quote to avoid reserved-word conflicts" — tester assumed quoting should exempt it | NOT a bug — the check is correct. Quoting only escapes **MDL parser** keywords (`unquoteString` strips the quotes and the *bare* name is validated). `Type`, `ID`, `GUID`, `CurrentUser`, and the audit names `CreatedDate`/`ChangedDate`/`Owner`/`ChangedBy` are reserved by the Mendix **platform**, so they fail regardless of quoting. The gap was documentation: several skills claimed quoting is "always safe" without the platform-name carve-out | `mdl/executor/cmd_enumerations.go` (`mendixReservedWords`, `mendixSystemAttributeNames`) — no code change needed; docs only | Add the carve-out to the "always quote" guidance (`check-syntax.md`, `generate-domain-model.md`, `demo-data.md`, docs-site `lexical-structure.md`) and a CLAUDE.md note: quoting is *parser*-safe, not *platform*-safe. Rename `Type`→`ResourceType`; use `AutoCreatedDate`/… pseudo-types for audit fields. Adjacent Mendix rule documented alongside: the after-startup microflow must return `Boolean` (CE0142) — a void seed microflow fails the build | -| A clickable `CONTAINER` can't be expressed: `OnClick: MICROFLOW …` / `Click:` error in the parser (`mismatched input 'MICROFLOW' expecting {',', ')'}`), and the one form that parses (`Action: MICROFLOW …`) is silently dropped — the container's BSON always has `OnClickAction = Forms$NoAction` | Layers all hardcoding "no click": (1) only the `Action:` keyword's `actionExprV3` accepted an action value, so `OnClick:`/`Click:` had no rule taking `MICROFLOW …`; (2) `buildContainerV3` never read `GetAction()`; (3) the `Container` model had no `OnClickAction` field and both writers (`serializeContainer`, modelsdk `widgetToGen`) emitted `serializeClientAction(nil)`/`noActionGen()`; (4) DESCRIBE never read it back | grammar `mdl/grammar/domains/MDLPage.g4` (`widgetPropertyV3`) + `mdl/visitor/visitor_page_v3.go` (`buildWidgetPropertyV3`) + `sdk/pages/pages_widgets_container.go` (`Container`) + `mdl/executor/cmd_pages_builder_v3_layout.go` (`buildContainerV3`) + `sdk/mpr/writer_widgets_layout.go` (`serializeContainer`) + `mdl/backend/modelsdk/widget_write.go` (DivContainer case) + `mdl/executor/cmd_pages_describe_parse.go`/`cmd_pages_describe_output.go` | Add an `ONCLICK COLON actionExprV3` alternative aliasing `Action:` (`make grammar`), stored under the same `Properties["Action"]` key. Add `OnClickAction ClientAction` to `Container`; in `buildContainerV3` read `GetAction()` → `buildClientActionV3()`; both writers serialize the real action (nil falls back to NoAction, so non-clickable containers are unchanged). DESCRIBE: extract `w["OnClickAction"]` via `extractButtonAction` (synthesize `{"Action": …}`) and emit `Action: ` in the container output. Verified with `mx check` = 0 errors. `Forms$DivContainer.OnClickAction` is required & introduced 8.3.0, so no version gate. Issue #603 | -| A **quoted** association/attribute name in a microflow `SET`/`Change` target (`SET $x/Module."Assoc" = $y`, following the "always quote identifiers" guidance) passes `mxcli check` and `exec` but corrupts the `.mpr` — Studio Pro/MxBuild fails to load with `StorageLoadException: … 'Module."Assoc"' is not a valid AttributeIdentifier`. The unquoted form (`SET $x/Module.Assoc = $y`) is fine | The SET target was captured via `ap.GetText()` (raw source, quotes intact) and later sliced on `/` into the member name; `resolveMemberChange` then compared the quoted `"Assoc"` against the unquoted domain-model `a.Name`, never matched, and wrote the quoted string verbatim into `AttributeQualifiedName`. Attributes in `CREATE (…)` were unaffected because that path unquotes per-segment | `mdl/visitor/visitor_microflow_statements.go` (`buildSetStatement`) + `mdl/visitor/visitor_microflow_actions.go` (`buildAttributePathFromContext`, `attributePathTargetText`) + `mdl/executor/cmd_microflows_builder_actions.go` (`resolveMemberChange`, `isValidMemberIdentifier`) | Build the SET target from the structured path with each qualified-name segment unquoted (`getQualifiedNameText`, not `qn.GetText()`), so `Target` is normalized (`$x/Module.Assoc`) at parse time for every downstream consumer (check + exec). Defense-in-depth: `resolveMemberChange` rejects a non-empty member name that isn't a valid identifier (`isValidMemberIdentifier`) so a leaked quote errors instead of corrupting. Tests: `mdl/visitor/visitor_microflow_set_quoted_test.go`, `mdl/executor/cmd_microflows_member_identifier_test.go`. Repair a corrupted microflow in place with `CREATE OR REPLACE MICROFLOW …` (unquoted form) | -| `set $IntVar = $a * 100 div $b;` (or `declare $I Integer = $a div $b;`) passes `mxcli check` but `mx check` rejects it with **CE0117** — Mendix integer division (`div`) yields a Decimal, so the target must be Decimal (or the result rounded) | `mxcli check`'s expression checker (`mdl/exprcheck`) was only wired into the Starlark linter, not `mxcli check`, and `inferKind` didn't type arithmetic operators (`div`/`*`/`-`/`mod` fell through to `KindUnknown`); nothing compared a SET/declare value's kind against the Integer/Long target | `mdl/exprcheck/parser.go` (`inferKind` + `arithResult`), `mdl/exprcheck/infer.go` (`InferSourceKind`, `SourceIsArithmeticDecimal`), `mdl/executor/validate_microflow.go` (`ValidateMicroflow` → MDL041) | Type `div`→Decimal and propagate Decimal through `+`/`-`/`*`/`mod` (`arithResult`); add **MDL041** in `ValidateMicroflow` (runs in syntax-only `check`, no project) flagging a Decimal-valued *arithmetic* expression assigned to an Integer/Long var. Narrow to arithmetic roots only via `SourceIsArithmeticDecimal` — a rounding-function result (`round(sqrt($x))`, `floor($a div $b)`) is accepted by Mendix into an Integer and must NOT be flagged (that was a false positive on `04-math-examples.mdl`). Tests: `mdl/exprcheck/infer_test.go`, `mdl/executor/validate_microflow_div_test.go` | -| `linkbutton` (a documented CREATE PAGE / ALTER PAGE INSERT widget) fails `exec` with "unsupported widget type: linkbutton — refresh widget definitions", even though `actionbutton` works in the same spot | `linkbutton` had a grammar token, a `pages.LinkButton` stub, and docs, but **no builder and no serializer** — `buildWidgetV3`'s switch only had `button`/`actionbutton`, so it fell to `default`. Trap: the `Forms$LinkButton` metamodel type requires an `address` (a legacy static hyperlink); the documented `linkbutton (caption, action)` is really a `Forms$ActionButton` with **RenderType "Link"** (the toolbox "link button"). The `RenderMode` field already existed on `pages.ActionButton` but the serializer hardcoded `RenderType: "Button"` | `mdl/executor/cmd_pages_builder_v3.go` (switch) + `mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildButtonV3`) + `sdk/mpr/writer_widgets_display.go` (`serializeActionButton`) + `mdl/backend/modelsdk/widget_write.go` (ActionButton case) + `mdl/executor/cmd_pages_describe_parse.go`/`cmd_pages_describe_output.go` | Route `linkbutton` → `buildButtonV3` with `RenderMode = Link`; serialize `RenderType` from `ab.RenderMode` (default "Button") on both engines; DESCRIBE reads `RenderType` back and emits the `linkbutton` keyword when it is "Link". Reuses the proven `Forms$ActionButton` BSON (only the enum differs), so no CE0463 risk. Tests: `mdl/executor/cmd_pages_linkbutton_test.go`, `sdk/mpr/writer_widgets_linkbutton_test.go`; example in `mdl-examples/doctype-tests/03-page-examples.mdl` | -| In an interactive REPL running scripts back-to-back, a script that completes fine (e.g. `11-navigation-examples.mdl`, which contains `REFRESH CATALOG FULL`) leaves the session **silently disconnected** — the *next* `execute script` (or any statement) fails with `not connected to a project`. Only reproduces when a `.mxcli/catalog.db` already exists on disk (from a prior session/script) AND the project has since been written to; a from-scratch piped run never hits it | `REFRESH CATALOG FULL` finds the on-disk cache stale (`Cache invalid: project file modified`) and calls `reconnect(ctx)`, which swaps `e.backend` for a fresh connection and syncs it back. When that reconnect fires **inside `execute script`**, the *outer* script statement's `ExecContext` was snapshotted before the reconnect, so it still holds the pre-reconnect (now-closed) backend. `executeInner`'s `syncBack` after the script then clobbers `e.backend` with that stale, closed connection → `IsConnected()` false → next statement reports not-connected. Any handler that runs nested statements via `ExecuteFn`/`ExecuteProgramFn` (EXECUTE SCRIPT, SQL connector-gen) is exposed | `mdl/executor/executor_dispatch.go` → `executeInner`/`syncBack` | Before `syncBack`, snapshot `e.backend` prior to dispatch; if the handler didn't change its own `ectx.Backend` (`ectx.Backend == before`) but a nested call swapped `e.backend` (`e.backend != before`), adopt the live connection and its project-scoped caches (`Backend`, `MprPath`, `Cache`, `Catalog`) into `ectx` so `syncBack` preserves the reconnect instead of reverting it. **Diagnosis pattern**: "works in a from-scratch/piped run, disconnects only in a long interactive session" + the only state that nils the modelsdk reader is `Disconnect()` → suspect a stale outer `ExecContext` clobbering executor-global state changed by a *nested* statement; the trigger for the reconnect is a pre-existing `.mxcli/catalog.db` + a modified project. Regression: `TestReconnectInsideScript_KeepsConnection` (`reconnect_in_script_test.go`, integration-tagged) | -| A widget's `DynamicClasses: ''` passes `mxcli check` but is silently dropped on write — `describe page` shows `Class` but no `DynamicClasses`; on `alter page ... set DynamicClasses` a core widget instead hard-errors "widget has no pluggable Object" | `DynamicClasses` (a `Forms$Appearance` string, sibling of `Class`) was unwired at every layer: parked in the generic Properties map, never read by `applyWidgetAppearance`, no `BaseWidget` field, both writers hardcoded `DynamicClasses: ""`, mutator had no case (fell to the pluggable-property setter), and describe never surfaced it | `sdk/pages/pages_widgets.go` (`BaseWidget.DynamicClasses`) + `mdl/ast/ast_page_v3.go` (`GetDynamicClasses`) + `mdl/executor/cmd_pages_builder_v3.go` (`applyWidgetAppearance`) + `mdl/executor/widget_engine.go` (`isBuiltinPropName`) + both writers (`sdk/mpr/writer_widgets.go` `serializeAppearance`; `mdl/backend/modelsdk/widget_write.go` `newAppearance`) + describe (`cmd_pages_describe*.go`) + `mdl/backend/pagemutator/mutator.go` (`case "DynamicClasses"`) | Carry it on `BaseWidget` (the appearance path — one place — covers core + pluggable widgets since `applyWidgetAppearance` runs for both); thread it through the appearance serializer of **both** engines (it's just a sibling string of `Class`); add a mutator `case "DynamicClasses"` writing `Appearance.DynamicClasses`; add a read at each `appearance["Class"]` site + an emit in `appendAppearanceProps`. Add `"DynamicClasses"` to `isBuiltinPropName` so the pluggable explicit-property pass skips it and the unknown-property guard permits it. Bug-test `mdl-examples/bug-tests/widget-dynamicclasses.mdl`; `mx check` = 0 on both engines. | -| A widget's conditional visibility set via the **string / static-boolean** form — `Visible: ''`, `set Visible = ""`, or `Visible: false` — passes `check`+`exec` but is silently dropped (only the `[ … ]` bracket form worked) | The visitor routes a bracket constraint to `Properties["VisibleIf"]` (consumed by `applyConditionalSettings`) but any other value to `Properties["Visible"]`, which nothing consumed; the ALTER mutator's `case "Visible"` wrote a bare `Visible` string (a DataGrid2-column alias) that Studio Pro ignores on a general widget. A page widget has **no** plain boolean Visible field — visibility is always `ConditionalVisibilitySettings` | `sdk/pages/pages_widgets.go` (`StaticVisibleExpression`) + `mdl/executor/cmd_pages_builder_v3.go` (`applyConditionalSettings`) + `mdl/backend/pagemutator/mutator.go` (`case "Visible"`) | Add `pages.StaticVisibleExpression(v)` mapping the value to a client expression: `false`→`"false"`, an expression string passes through (caller roots it; the bracket form auto-roots), `true`/`""`→"no settings / clear". CREATE's `applyConditionalSettings` reads `Properties["Visible"]` as a fallback after `VisibleIf`; the ALTER mutator's `case "Visible"` builds the `ConditionalVisibilitySettings` node via `setWidgetConditionalSettingMut` (or clears it to nil for `true`). Both engines: CREATE via the shared model→`applyWidgetBase`, ALTER via the shared raw-BSON mutator. describe converges every form to the canonical `Visible: [expr]`. NB datagrid **column** visibility is a separate `columnPropertyAliases` path — untouched. Bug-test `mdl-examples/bug-tests/widget-visible-expression.mdl`; `mx check` = 0 on both engines. | -| An unrecognized property on a **built-in** (non-pluggable) widget — a typo like `Contnet`, or a genuinely unsupported key — passes `mxcli check`+`exec` with no error and is silently dropped on write (pluggable widgets already got MDL-WIDGET01) | `validateStaticWidget` had no unknown-property check; core widgets have no single property registry — builders read keys imperatively, and `describe` even emits keys (`WidthUnit`) the native builder doesn't consume — so a hard reject would false-positive on valid MDL and break the describe→create roundtrip | `mdl/executor/validate_widgets.go` (`staticWidgetKnownProps`, `validateStaticWidgetUnknownProps`, wired in `validateWidgetTree` gated on `lookupWidgetDef==nil`) | Add **MDL-WIDGET07** as a **WARNING** (never an error — the core-widget vocabulary can't be proven complete): flag any `Properties` key not in `staticWidgetKnownProps` (the union of grammar keyword props + builder-consumed keys + the full `describe`-emit vocabulary, harvested by grep), with a `nearestKey` "did you mean" hint. Runs only for non-pluggable widgets. Guard the allow-list against describe-vocabulary drift with `TestStaticWidgetKnownPropsCoverDescribe`, and sweep `mdl-examples/**` for zero false positives after any change. Bug-test `mdl-examples/bug-tests/widget-unknown-property.mdl`. | -| A **quoted** attribute/association name inside an *expression* — a microflow decision/`if`/`while`/`return` (`if $Expense/"Justification" != empty`), a widget `contentparams` entry (`contentparams: [{1} = "Amount"]`), a `visible:`/`editable:` expression (`visible: [ "Amount" > 1000 ]`), or an **XPath database datasource WHERE** (`datasource: database from M.E where [ "Status" = 'Submitted' ]`) — passes `mxcli check` (and `--references`). The first three fail MxBuild "Error(s) in expression"; the **datasource WHERE is the worst variant** — it *also* passes MxBuild, then **silently returns zero rows at runtime** because Mendix reads the quoted `"Status"` as the string literal `'Status'`, making the constraint `'Status' = 'Submitted'` always false. The unquoted form is fine. Sibling of the SET/Change-**target** row above, but for expression contexts (the "always quote identifiers" guidance is parser-safe, not platform-safe here) | Two sub-causes. (a) Expression contexts stored the **raw source text verbatim** — `SourceExpr.Source` from `extractOriginalText` (microflow), `expr.GetText()` (contentparams), the serialized visibility string. (b) The **inline-bracket XPath** path built member names via `buildXPathQualifiedName` (`words[i].GetText()`, no unquote) → an `IdentifierExpr{Name:"\"Status\""}` that `xpathExprToString` re-emits verbatim. Either way the quotes leaked, whereas binding contexts route each identifier through `unquoteIdentifier`. The general (non-XPath) expression AST was already unquoted | `mdl/visitor/visitor_helpers.go` (`stripExpressionIdentifierQuotes`) + `mdl/visitor/visitor_microflow_statements.go` (`buildSourceExpression`, `buildXPathSourceExpression`, `buildRetrieveWhereExpression`) + `mdl/visitor/visitor_page_v3.go` (`buildConditionalExpression`, `buildParamAssignmentV3`) + `mdl/visitor/visitor_xpath.go` (`buildXPathQualifiedName`) | Strip double-quote/backtick identifier quotes at the source. For raw-text slots: `stripExpressionIdentifierQuotes` (tracks single-quote string state incl. `''` escapes, so `'he said "hi"'` is untouched — safe because in Mendix expr/XPath only identifiers are double-quoted). For the inline-bracket XPath AST: `unquoteIdentifier` each `xpathWord` in `buildXPathQualifiedName` (root fix, feeds datasource WHERE + inline retrieve + conditional visibility); guard the `empty`/`true`/`false` special-value switch to fire only on the **unquoted** form so a quoted `"empty"` stays a member name. `dbSource.XPathConstraint = ds.Where` verbatim, so the visitor value is the persisted BSON constraint. Not caught by the expression **type** checker (`PROPOSAL_expression_type_checking.md`) — it works on the clean AST, not the raw string, and is microflow-scoped. Tests: `mdl/visitor/visitor_expr_quoting_test.go`; bug-test `mdl-examples/bug-tests/expr-quoted-identifiers.mdl` | -| A `dynamictext`/`datagrid` **contentparams**/**captionparams** entry that navigates an association (`[{1} = Module.Assoc/Attr]`, e.g. `Expense_Employee/Name`) passes `mxcli check --references` but fails MxBuild "Value of text template parameter {1} … No value specified." A **direct** attribute (`[{1} = Title]`) persists fine — the drop is specific to association navigation | The value string (`Module.Assoc/Attr`) reached the AttributeRef serializer, which rejects any path with `< 2` dots (the association hop has 1 dot; `/Attr` is navigation, not counted) → **null AttributeRef** → CE0402. Nothing built the association-step structure Mendix needs. Verified against a Studio-Pro-authored page: the binding is a `DomainModels$AttributeRef` whose `Attribute` is the FINAL attr (`Module.Employee.Name`) and whose `EntityRef` is a `DomainModels$IndirectEntityRef` of `DomainModels$EntityRefStep{Association, DestinationEntity}` hops | `sdk/pages/pages_widgets_display.go` (`ClientTemplateParameter.AttributeRefSteps`, `AttributeRefStep`) + `mdl/executor/cmd_pages_builder_v3.go` (`resolveTemplateAssociationPath`, `associationEndpoints`) + `mdl/backend/modelsdk/widget_write.go` (`attributeRefWithStepsToGen`) + `mdl/executor/cmd_pages_describe_output.go` (`associationTemplateParamPath`) | **modelsdk engine only.** Resolver splits the path into association hop(s) + final attribute, resolves each association's destination entity from the domain model (ParentID=FROM, ChildID=TO; direction from the context entity), and stores `AttributeRef`=final attr QN + `AttributeRefSteps`. The writer emits `AttributeRef.EntityRef` via `entityRefToGen` (reused from microflows — identical `IndirectEntityRef`/`EntityRefStep` storage names); the `< 2 dots` guard no longer trips since the final attr is 2-dot. DESCRIBE reconstructs `Assoc/Attr` from `EntityRef.Steps`. Legacy engine still drops it (out of scope). Generated BSON is byte-structurally identical to Studio Pro. **Non-String final attribute needs NO `toString()`** — verified against Studio Pro, an association-navigated param uses `AttributeRef` + `FormattingInfo` (DecimalPrecision/DateFormat/EnumFormat) for any type; the `toString()` wrapping is only the *direct*-attribute path's behavior. Do not "fix" the assoc path to wrap non-String — that diverges from Studio Pro. Tests: `mdl/backend/modelsdk/widget_template_assoc_test.go`, `mdl/executor/cmd_pages_describe_assoc_test.go`; bug-test `mdl-examples/bug-tests/dynamictext-contentparam-association.mdl` | -| Under the **default (modelsdk)** engine, any page widget with an **association datasource** (`datasource: $currentObject/Module.Assoc`, the master-detail idiom) fails at exec: `CreatePage: DataView source *pages.AssociationSource not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy`. Same for a nested `listview` | `customWidgetDataSourceToGen` (pluggable widgets) already handled `*pages.AssociationSource`, but `dataViewSourceToGen`/`listViewSourceToGen` had no case → hit the "not yet supported" default. (Legacy doesn't error but writes a **broken** `Forms$AssociationSource{EntityRef: nil}` in `serializeListViewDataSource`, so legacy is not a valid reference — tracked as **L1** in [`docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md`](../../docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md); do **not** fix legacy, it will be removed when modelsdk matures) | `mdl/backend/modelsdk/widget_write.go` (`associationSourceToGen`, wired into `dataViewSourceToGen`+`listViewSourceToGen`) + `mdl/executor/cmd_pages_describe.go` (`associationSourcePath`) + `cmd_pages_describe_parse.go` (DataView/ListView datasource cases) + `cmd_pages_describe_output.go` (`associationDataSourceExpr`) | **modelsdk engine only.** Extract the proven `AssociationSource` block from `customWidgetDataSourceToGen` into a shared `associationSourceToGen` (Forms$AssociationSource → `IndirectEntityRef` of `EntityRefStep{Association, DestinationEntity}` + optional page-param `SourceVariable`; `$currentObject` → nil source var) and add a `case *pages.AssociationSource` to the DataView and ListView source builders. Matches the canonical legacy `serializeAssociationSource` (writer_widgets.go) and the Studio-Pro-verified `EntityRefStep` structure — NOT the broken `serializeListViewDataSource` stub. DESCRIBE reconstructs `$currentObject/Module.Assoc` from `EntityRef.Steps`. **An association source is valid only on LIST-producing widgets (listview/datagrid/gallery/templategrid), NOT a plain DataView** — Studio Pro rejects a DataView association source ("cannot have a data source of type association"); the widget still exec-creates (valid BSON), so `check` flags it with **MDL-WIDGET08** (`validateStaticWidget`). Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go`, `mdl/executor/cmd_pages_describe_assoc_source_test.go`, `validate_widgets_test.go`; bug-tests `mdl-examples/bug-tests/assoc-datasource-modelsdk.mdl` (valid listview) + `dataview-association-source-rejected.fail.mdl` (MDL-WIDGET08) | -| `IF … ELSIF … ELSE … END IF` silently drops every ELSIF arm on write — DESCRIBE round-trips `if … else …` with the middle arms gone (read path unchanged by the fix ⇒ the arm was absent from the stored model — dropped on write); no error from `check`/`exec` (both engines) | `buildIfStatement` read only `exprs[0]`/`bodies[0]` and the trailing ELSE body; the grammar's `(ELSIF expression THEN microflowBody)*` pairs were never visited | `mdl/visitor/visitor_microflow_statements.go` (`buildIfStatement`) | Lower each ELSIF arm into a nested `IfStmt` in the ELSE branch of the arm before it (built innermost-first) — Mendix has no native elsif, nested exclusive splits are the canonical shape. Guarded by visitor tests (chain, no-else, plain-if regression) + `mdl-examples/bug-tests/745-elsif-arms-dropped.mdl`. Issue #745 | -| Under the **default (modelsdk)** engine, any page widget with an **association datasource** (`datasource: $currentObject/Module.Assoc`, the master-detail idiom) fails at exec: `CreatePage: DataView source *pages.AssociationSource not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy`. Same for a nested `listview` | `customWidgetDataSourceToGen` (pluggable widgets) already handled `*pages.AssociationSource`, but `dataViewSourceToGen`/`listViewSourceToGen` had no case → hit the "not yet supported" default. (Legacy doesn't error but writes a **broken** `Forms$AssociationSource{EntityRef: nil}` in `serializeListViewDataSource`, so legacy is not a valid reference — tracked as **L1** in [`docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md`](../../docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md); do **not** fix legacy, it will be removed when modelsdk matures) | `mdl/backend/modelsdk/widget_write.go` (`associationSourceToGen`, wired into `dataViewSourceToGen`+`listViewSourceToGen`) + `mdl/executor/cmd_pages_describe.go` (`associationSourcePath`) + `cmd_pages_describe_parse.go` (DataView/ListView datasource cases) + `cmd_pages_describe_output.go` (`associationDataSourceExpr`) | **modelsdk engine only.** Extract the proven `AssociationSource` block from `customWidgetDataSourceToGen` into a shared `associationSourceToGen` (Forms$AssociationSource → `IndirectEntityRef` of `EntityRefStep{Association, DestinationEntity}` + optional page-param `SourceVariable`; `$currentObject` → nil source var) and add a `case *pages.AssociationSource` to the DataView and ListView source builders. Matches the canonical legacy `serializeAssociationSource` (writer_widgets.go) and the Studio-Pro-verified `EntityRefStep` structure — NOT the broken `serializeListViewDataSource` stub. DESCRIBE reconstructs `$currentObject/Module.Assoc` from `EntityRef.Steps`. A `Forms$AssociationSource` is valid on **LIST-producing** widgets (listview/datagrid/gallery/templategrid). A plain **DataView** cannot use `Forms$AssociationSource` (MxBuild **CE6705**) — but the DataView *association* case is still valid via a **different** source type; see the "data from context over association" row below (an earlier fix wrongly rejected it with MDL-WIDGET08, now removed). Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go`, `mdl/executor/cmd_pages_describe_assoc_source_test.go`; bug-test `mdl-examples/bug-tests/assoc-datasource-modelsdk.mdl` (valid listview) | -| A **DataView bound to a to-one referenced object over an association** ("data from context", e.g. an inner DataView showing the referenced Employee's `Name`/`Email` inside an `Expense` DataView) can't be authored/round-tripped: `dataview dvEmp (datasource: $currentObject/Module.Assoc)` was rejected (MDL-WIDGET08), a bare nested `dataview { textbox (attribute: Name) }` bound children to the **parent** entity (CE1613), and `describe` dropped the source → describe/exec **silently strips** the association DataView (round-trip corruption) | A DataView over an association is **not** a `Forms$AssociationSource` (that's list-widgets-only; CE6705 on a DataView). It is a `Forms$DataViewSource` whose `EntityRef` is a `DomainModels$IndirectEntityRef` navigating the association (a DataViewSource *is* an `EntityPathSource`, so its EntityRef may be indirect — `generated/metamodel/types.go:558`). mxcli generated the wrong source type, then (Bug 5 v1) wrongly rejected the whole case | `mdl/backend/modelsdk/widget_write.go` (`dataViewContextAssociationSourceToGen`, wired into `dataViewSourceToGen`'s `*pages.AssociationSource` case) + `mdl/executor/cmd_pages_builder_v3_widgets.go` (removed the `buildDataViewV3` association guard) + `mdl/executor/validate_widgets.go` (removed MDL-WIDGET08) + `mdl/executor/cmd_pages_describe_parse.go` (`Forms$DataViewSource` reads `EntityRef.Steps` via `associationSourcePath`) | **modelsdk engine only.** For a DataView association datasource, emit `Forms$DataViewSource` with `EntityRef = IndirectEntityRef{EntityRefStep{Association, DestinationEntity}}` (+ optional page-param `SourceVariable`; `$currentObject` → none) — mirrors `associationSourceToGen` but wraps the IndirectEntityRef in a DataViewSource. Children then bind to the destination entity. DESCRIBE reconstructs `$currentObject/Module.Assoc`. Removes the earlier MDL-WIDGET08 over-rejection. **When two widget kinds share a navigation but MxBuild accepts it on only one, the distinction is the source `$Type` wrapper, not the EntityRef inside it** — check `generated/metamodel/types.go` for the widget's allowed `DataSource` subtypes. mxbuild-validated (`mxcli docker check --no-update-widgets` = 0 errors) + full describe→re-exec round-trip. Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go` (`TestDataViewAssociationSource_Serialized`); bug-test `mdl-examples/bug-tests/dataview-context-association-source.mdl`. Bug 5 (reclassified) | -| **DataGrid2** (`datagrid`) columns fail MxBuild **CE0463** "widget definition changed" on the **default (modelsdk)** engine — reported for custom-content columns, but actually **every** column (attribute too). Legacy is fine | The column WidgetObject's `Properties` were serialized in **alphabetical** order (`alignment, allowEventPropagation, attribute, …`) instead of the template's `PropertyTypes` order (`showContentAs, attribute, content, dynamicText, …`). Studio Pro hashes the object structure against the type and flags CE0463 on any order mismatch. Cause: the object-list item builder (`mdl/backend/widgetobj/builder.go:274`) orders by `NestedKeyOrder`, falling back to alphabetical when empty; the **modelsdk** registry loader never captured it (`types.PropertyTypeIDEntry` had no such field), while the MPR/legacy loader did → legacy correct, modelsdk broken. Top-level widget props were unaffected (ordered by a different path), which is why only the nested column object-list broke | `mdl/types/widget_property_type.go` (`PropertyTypeIDEntry.NestedKeyOrder`) + `modelsdk/widgets/loader.go` (thread `nestedKeyOrder` through `jsonValueToBSONWithNestedObjectType`→`extractNestedObjectType`→`extractNestedPropertyTypes`, append in template array order) + `mdl/backend/modelsdk/widget_pluggable_write.go` (`convertPropTypeIDs` copies `NestedKeyOrder`) | Mirror the `sdk/widgets` loader's existing `nestedKeyOrder` capture in the parallel `modelsdk/widgets` loader (dedup on first occurrence, append in PropertyTypes array order), add the field to `types.PropertyTypeIDEntry`, and copy it in the modelsdk `convertPropTypeIDs`. **Diagnose column-order CE0463 by mapping each column WidgetProperty's `TypePointer` → the type's `WidgetPropertyType.PropertyKey` and comparing the sequence to the template's PropertyTypes order.** Verified: modelsdk column order now equals the template (and legacy) order for both attribute and custom-content columns, and **`mxcli docker check --no-update-widgets` (raw output, no widget normalization) = 0 errors** for the report's exact pattern (attribute column + custom-content dynamictext-over-association + custom-content actionbutton with `show_page`). The custom-content child-widget subtree needed no separate fix — ordering was the whole cause. Tests: `modelsdk/widgets/nested_key_order_test.go`; bug-test `mdl-examples/bug-tests/datagrid2-custom-content-column.mdl`. **To validate CE-class MxBuild errors locally: `mxcli setup mxbuild -p app.mpr --force` then `mxcli docker check -p app.mpr --no-update-widgets`** (the `--no-update-widgets` is essential — the default runs `mx update-widgets` which auto-normalizes pluggable widgets and masks a real CE0463) | -| Pluggable-widget datasource `sort by desc` (DataGrid2, Gallery; quoted or unquoted attr) round-trips as `asc` — `describe page` shows the direction flipped, runtime renders oldest-first, no error (silent wrong-order). Reproduces on the **default (modelsdk)** engine | A Pages/`Forms$GridSortItem` stores its direction under the BSON key **`SortDirection`** (authoritative: the reflection-generated codec type `modelsdk/gen/pages` GridSortItem writes/reads `SortDirection`). The default engine wrote it correctly, but the DESCRIBE readers looked up the wrong key `SortOrder` (that key is only correct for `Microflows$SortItem` / `DocumentTemplates$GridSortItem`) → always fell back to `asc`. The legacy `sdk/mpr` writer *also* emitted the wrong `SortOrder` key, so under `--engine legacy` Studio Pro ignored it and reverted to ascending at runtime too | `mdl/executor/cmd_pages_describe_pluggable.go` (`gridSortDirection` helper + 3 call sites) & `cmd_pages_describe_parse.go` (1 call site); writer `sdk/mpr/writer_widgets.go` (`SerializeCustomWidgetDataSource`) | Add `gridSortDirection(sortItem)` reading `SortDirection` with a `SortOrder` fallback (keeps pre-fix files readable); route all four grid-sort readers through it. Fix the legacy writer to emit `SortDirection`. When a sort/direction field seems misnamed, check the element's gen type in `modelsdk/gen/*/types.go` — different metamodel types genuinely use different keys (`SortDirection` for Forms/Pages grids, `SortOrder` for microflow/document-template sorts). Tests: `mdl/executor/cmd_pages_describe_sortdir_test.go`; bug-test `mdl-examples/bug-tests/bug8-datagrid-gallery-sort-desc.mdl`. Bug 8 | -| A **compound** (nested) design property authored inline on a widget — `designproperties: ['Card style': on, 'Spacing': ['margin-bottom': 'Large', 'margin-top': 'Medium']]`, e.g. the Atlas `Spacing` group, and any block copied in via `use building block` — is silently dropped on write: `check`, `exec`, **and `mx check`** all pass, but the nested styling is simply gone (only the flat toggle/option props survive). The **legacy** builder write path. Sibling of #668, which fixed the describe **read** side but not this CREATE write path | `serializeDesignProperties` switched on `p.ValueType` with cases for `toggle`/`option`/`custom` and a `default: continue` that dropped `compound` entirely. A compound property's value is itself a set of sub-properties stored in a `Forms$CompoundDesignPropertyValue`, whose `Properties` list has the SAME marker-prefixed `Forms$DesignPropertyValue` shape as the outer array — but nothing serialized it, so the whole entry (key included) vanished | `sdk/mpr/writer_widgets.go` (`serializeDesignProperties`) | Add a `case "compound"` emitting `{$ID, $Type: Forms$CompoundDesignPropertyValue, Properties: serializeDesignProperties(p.Compound)}` — recurse into the sub-entries, which the marker-prefixed array handling already covers. The AST (`DesignPropertyEntryV3.Nested`), visitor (`buildDesignPropertyEntryV3`), and builder (`astDesignPropToValue` → ValueType `"compound"`, `Compound`) all already produced the nested model; only the serializer's terminal switch dropped it. Verified: `mxcli docker check` = 0 errors on 11.12.1 (authoritative) + write→describe round-trip preserves `Spacing` on **both** engines. Test `TestSerializeDesignProperties_Compound` (`sdk/mpr/writer_widgets_test.go`); bug-test `mdl-examples/bug-tests/compound-designproperties.mdl`. **Diagnosis pattern**: when a value passes every check *including `mx check`* yet the construct is absent, suspect a terminal `default: continue`/`default:` drop in a type-switched serializer — the value never reaches BSON, so nothing downstream can complain | -| A `use fragment` / `use building block` (or a content `slot`) nested **inside a container/layout/dataview** — not at the page-body top level — fails `exec` with `unsupported widget type: USE_FRAGMENT` (or `USE_BUILDING_BLOCK`). The same ref at the top level of the page body works. So a reusable card/panel can't be placed inside a layout column — the natural usage | `expandFragments` (the sentinel-expansion pass) only ran on the **top-level** widget list (the two call sites in `execCreatePage`/`execCreateSnippet`); the layout/container builders build children via `buildWidgetV3` directly, which has no case for the `USE_FRAGMENT`/`USE_BUILDING_BLOCK`/`SLOT` sentinel types → falls to `default` "unsupported widget type" | `mdl/executor/cmd_pages_builder_v3.go` (`expandFragments`) | Make `expandFragments` **recurse into each widget's children** after expanding the top sentinel: `for _, e := range expanded { if len(e.Children) > 0 { e.Children, _ = pb.expandFragments(e.Children) }; result = append(result, e) }`. The tree is fully expanded to concrete widgets *before* `buildWidgetV3` runs, so no builder needs a sentinel case. Expansion is idempotent on concrete widgets, so the extra traversal of an already-expanded slot payload is harmless. Verified: nested cards inside a layoutgrid column exec + `mx check` = 0 on 11.12.1. Test `TestExpandFragments_NestedInsideContainer`; bug-test `mdl-examples/bug-tests/nested-fragment-expansion.mdl`. **Diagnosis pattern**: "works at top level, `unsupported widget type` when nested" = an AST pre-pass (expansion/normalization) that only walks the root list; make it recurse into `.Children` | -| A single `alter entity` with **comma-separated** `add attribute` clauses fails to parse — e.g. `add attribute A: integer default 9, add attribute B: integer default 9` → `no viable alternative at input '9'`. Looks like "only the first `add` can carry a `default`" but the real cause is the comma | `alterStatement`'s `ALTER ENTITY qualifiedName alterEntityAction+` had **no separator** — commas between actions weren't allowed at all; the error surfaced on the second clause's `default` value token, which misdirects. Sudoku findings #5 | `mdl/grammar/MDLParser.g4` (`alterStatement`, ALTER ENTITY alt) | Change `alterEntityAction+` → `alterEntityAction (COMMA? alterEntityAction)*` (optional comma, mirroring `entityOptions`); `make grammar`. Newline-separated actions still parse. Bug-test `mdl-examples/bug-tests/f5-alter-entity-comma.mdl`. **Diagnosis pattern**: a parse error on the *value* inside the *second* item of a list usually means the list rule lacks a separator, not that the value form is wrong | -| An `autonumber` attribute with **no seed** passes `mxcli check` but fails the build with **CE7247 "Value cannot be empty"** (`alter entity … add attribute X: autonumber` or in a `create`). Docs showed seedless `autonumber` | `ValidateEntity` had no autonumber-seed rule; the writer emits no `AttributeValue` when `!attr.HasDefault`, so Studio Pro has no start value. Sudoku findings #6 | `mdl/executor/cmd_enumerations.go` (`ValidateEntity`) | Add **MDL023** (error): `attr.Type.Kind == ast.TypeAutoNumber && !attr.HasDefault` → "autonumber requires a seed (`default N`)". Also fixed the skill docs (`mdl-entities.md`, `generate-domain-model.md`) to show `autonumber default 1`. Test `TestValidateEntityAutonumberNeedsSeed`; negative bug-test `f6-autonumber-seed.fail.mdl` | -| An AutoX audit pseudo-type declared under a non-matching name — `StartedAt: autocreateddate` — silently becomes the fixed system member `CreatedDate` (declared name discarded), and binding that member in a widget then fails the build with **CE1613 "attribute … no longer exists"** (it's a system member, not a bindable attribute) | The write path discards the identifier for AutoX types and `ValidateEntity` skipped them with no name check, so the rename + unbindable-member trap was silent. Sudoku findings #7 | `mdl/executor/cmd_enumerations.go` (`ValidateEntity`, `autoMemberNames`) | Add **MDL022** (warning): when an AutoX attr's name (case-insensitive) ≠ its canonical member (`owner`/`ChangedBy`/`CreatedDate`/`ChangedDate`), warn that the name is discarded and the member isn't widget-bindable — use a plain attribute you set yourself if a widget must show it. Test `TestValidateEntityAutoMemberRename` | -| `mxcli run` warm-loop ergonomics from Sudoku findings: (a #17) a **relative** `-p` fails with MxBuild's raw "should be an absolute path" + a Windows JSON sample; (b #15/#23) a **build failure in the watch loop** (incl. a SCSS compile error like `Expected expression. _x.scss 180:35`) printed only the generic `build failed: `, swallowing the real detail | (a) `cmd_run.go` passed `-p` straight through without `filepath.Abs`; (b) the watch path at `runlocal.go` printed only `build.Message` while `build.Raw` (the full serve `/build` body, which the cold-build path already prints) held the compiler/model detail | `cmd/mxcli/cmd_run.go` + `cmd/mxcli/docker/runlocal.go` (watch build-failed branch) | (a) `projectPath, _ = filepath.Abs(projectPath)` after reading the flag; (b) also print `strings.TrimSpace(string(build.Raw))` (indented) when it differs from `build.Message`, matching the cold path. **Diagnosis pattern**: when a dev-loop error is unhelpfully generic, check whether a `Raw`/full-body field is already captured and just not printed on that code path | -| A **DataGrid2 column bound to an associated attribute** (`column c (attribute: Order_Customer/Name)`) passes `mxcli check` but fails MxBuild **CE1613** "The selected attribute 'Module.Entity.Order_Customer/Name' no longer exists." — an own-entity `attribute: Name` works. (Feature gap: no way to show an associated attribute in a column.) | The grammar `attributePathV3` already accepts a bare `Assoc/Attr` path (module-qualified `M.Assoc/Attr` does not — bare only), but the reader flattened it (`resolveAttributePath` just prefixes the entity, leaving the `/` embedded → `Module.Entity.Assoc/Attr`) and both column serializers hardcoded `EntityRef: nil`. So the column stored a flat, unresolvable attribute path with no association step | `mdl/executor/cmd_pages_builder_v3.go` (`resolveAssociationAttributePath`, extracted from `resolveTemplateAssociationPath`) + `mdl/executor/widget_engine.go` (full-page column `attribute` case) + `cmd_pages_builder_v3_widgets.go` (`buildColumnSpecFromAST`, ALTER) + `mdl/backend/mutation.go` (`ObjectListItemProperty`/`DataGridColumnSpec` gain `AttributeRefSteps`) + `mdl/backend/widgetobj/builder.go` (`setAttributeRefField`+`attributeEntityRefBSON`) + `datagrid_column.go` (`buildColumnAttributeProperty`) + `mdl/executor/cmd_pages_describe_pluggable.go` (`columnAttributeFromRef`) | Reuse the DynamicText contentparam machinery: resolve the `/`-path to a final attribute QN + `[]pages.AttributeRefStep` (hop → destination entity via `associationEndpoints`), carry the steps on the column spec, and emit `AttributeRef.EntityRef = IndirectEntityRef` of `EntityRefStep{Association, DestinationEntity}` (raw-BSON `attributeEntityRefBSON`, mirroring the codec-form `attributeRefWithStepsToGen`). DESCRIBE reconstructs the **short** `Assoc/Attr` (short association names — `attributePathV3` rejects module-qualified associations). mxbuild-validated (`mxcli docker check --no-update-widgets` = 0 errors) + describe round-trip. Tests: `mdl/backend/widgetobj/widget_builder_attribute_ref_test.go`, `mdl/executor/cmd_pages_describe_column_assoc_test.go`; bug-test `mdl-examples/bug-tests/datagrid2-associated-attribute-column.mdl`. Bug 7 | -| A DataGrid2 column's `DynamicCellClass: ''` (per-cell dynamic CSS class) parses, passes `check` + `mxbuild`, but is **silently dropped** — `describe` shows no `DynamicCellClass`, the `columnClass` slot is written as an **empty** expression, runtime cell is unstyled. Both engines | The DataGrid `columns` object-list mapping (`itemPropertyAliases`) had aliases for `header←Caption`, `dynamicText←Content`, `width←ColumnWidth` but **none** for `columnClass`. `buildObjectListItem` looks up the schema key + its MDL aliases in the AST property bag (case-insensitive via `lookupProperty`), so `DynamicCellClass` never matched → the property fell through to the template's empty default. Cached `.mxcli/widgets/datagrid.def.json` files also had to regenerate to carry the new alias | `mdl/executor/widget_defs.go` (`itemPropertyAliases`, `columns.columnClass`) + `mdl/executor/widget_engine.go` (`WidgetDefGeneratorVersion` bump) | Add `"columnClass": {"DynamicCellClass"}` to the datagrid `columns` aliases (schema property is `type: "expression"` → `operationForType` → written via the expression branch). **Bump `WidgetDefGeneratorVersion`** (5→6) so existing projects' cached def.json auto-regenerate via `RefreshStaleWidgetDefinitions` and pick up the alias — a code-only alias add is invisible until the stamped def is refreshed. DESCRIBE already reads `columnClass`→`DynamicCellClass` (`cmd_pages_describe_pluggable.go`). Test: `TestObjectListItemAliases`; bug-test `mdl-examples/bug-tests/bug10-dynamic-css-classes.mdl`. Bug 10a | -| A standard widget's generic property written in **lowercase** (e.g. `dynamicclasses:` instead of `DynamicClasses:`) is silently dropped on write, though the canonical-case form persists. Documented lowercase examples (`create-page.md`) don't work | Generic (non-tokenized) widget properties are stored in `WidgetV3.Properties` under the **user's original casing** (`visitWidgetProperty` generic branch: `Properties[id.GetText()]`). The builder reads appearance props via `GetStringProp("DynamicClasses")` — an exact-case map lookup — so a lowercased key never matched. MDL property names are documented case-insensitive (mirrors `lookupProperty` in the widget engine, used by the pluggable path) | `mdl/ast/ast_page_v3.go` (`GetStringProp`) | Make `GetStringProp` case-insensitive: exact-match fast path, then a lowercased scan (mirrors `lookupProperty`). Fixes any generic-stored standard-widget property read via `GetStringProp`/`GetDynamicClasses`, not just DynamicClasses. Test: `mdl/ast/ast_page_v3_getprop_test.go`; same bug-test as 10a. Bug 10b | -| A `dynamictext` contentparam over an association fails mxbuild ("No value specified") — **re-reported as still-broken after the modelsdk fix**. Investigation: the contentparam is actually **already correct on the default (modelsdk) engine** in every *valid* container (datagrid custom-content column, `listview + database`, `dataview` over a page parameter — all mx check 0 errors). The only failing repro is a `dataview (datasource: database …)`, which is an **invalid Mendix construct** — a data view shows one object, so Mendix offers only Context/Microflow/Nanoflow/Listen sources, never Database (that's for list widgets). mxcli wrongly accepted it: modelsdk errored "not yet supported — rerun with legacy", and legacy silently wrote a `Forms$DataViewSource` that mxbuild rejects with **CE7007** "Selected value is not valid for entity" | mxcli had no check for a DataView database source (only the association case, MDL-WIDGET08). So the invalid construct routed users to the deprecated legacy engine, which produced a broken page. **Do not "fix" legacy** (`LEGACY_ENGINE_KNOWN_ISSUES.md`: legacy is being removed, not patched) and **do not implement dataview+database in modelsdk** (Mendix doesn't allow it) — reject it at check, sibling to MDL-WIDGET08 | `mdl/executor/validate_widgets.go` (`validateStaticWidget`, MDL-WIDGET09) + `mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildDataViewV3` exec refusal) | Add **MDL-WIDGET09**: `dataview` + `database` source → error steering to a microflow/nanoflow source (or page parameter), or a list widget. Refuse it in `buildDataViewV3` too (both engines) so a bare `exec` can't create a broken page. When a construct fails only on legacy, first check whether it's valid on modelsdk in a *valid* container — the fix is usually a check-time rejection of the invalid form, not a legacy patch. Tests: `TestValidateStaticWidget_DataViewDatabaseSource`; bug-test `mdl-examples/bug-tests/dataview-database-source-rejected.fail.mdl` (negative). Bug 3 (re-report) | -| A **list** widget (listview/datagrid/gallery) reverse-navigating an association (`datasource: $currentObject/Module.Assoc` on a Customer showing its Orders) passes `mxcli check` but fails MxBuild **CE8812** "A grid association path must result in a list." — **only** when the association is `owner both` **and** its FROM entity has another association. Default-ownership references work; a DataView (to-one) over the same reverse works | **Not an mxcli encoding bug** — the `Forms$AssociationSource` + `EntityRefStep{Assoc, DestinationEntity}` is correct (verified via `bson dump`). It's correct MxBuild behavior: `owner both` on a plain `type reference` makes the reverse navigation **to-one**, so a *list* widget over it is invalid (a DataView is fine). The single-ref-vs-multi-ref inconsistency (single owner-both allows the reverse listview; adding a sibling association flips it to-one) hints at a **separate, unfixed `owner both` domain-model serialization quirk** — not yet root-caused (needs a Studio-Pro owner-both reference for comparison) | domain-model writer (`sdk/mpr/writer_domainmodel.go` / `mdl/backend/modelsdk/domainmodel_write.go` association owner/navigability) — **investigate before adding any check** | **No check added** (deliberately — the condition isn't statically precise and isn't always-wrong; adding one would repeat the MDL-WIDGET08 over-rejection mistake). Guidance only: use `default` ownership for a normal to-one reference (reverse is then to-many, list widgets work); reserve `owner both` for `ReferenceSet`. Documented in `generate-domain-model.md` (Owner Options). Diagnose CE8812 by dumping the widget source (encoding is usually right) then checking the association's `owner` — bisect the domain model, not the page. **Method note:** bisected this with `mxcli docker check --no-update-widgets` over a fresh test-project copy per variant | -| A `dynamictext` contentparam (or DataGrid2 column) navigating an association still fails mxbuild **CE0402 "No value specified"** — the *actually unsolved* Bug 3 case. Reproduces only when the widget's entity context is a **specialization** of the entity that declares the association: a grid over `SpecialExpense extends Expense` with `contentparams: [{1} = Expense_Employee/Name]` (the `Expense_Employee` association is on the base `Expense`). An exact-endpoint context (grid over `Expense`) works. `describe` shows `[{1} = ]`; the binding is dropped. **Very common in real apps** (entities inherit from a base, associations on the base) — which is why the earlier fixes looked incomplete | `resolveAssociationAttributePath` → `associationDestination` matched the context against the association's FROM/TO **by exact string equality**. A subclass context equalled neither endpoint, so it returned `ok=false` and the caller fell back to the flat, unresolvable `Assoc/Attr` path → no valid AttributeRef → CE0402. Affects both engines (shared builder) and both the contentparam and Bug 7 column-attribute paths | `mdl/executor/cmd_pages_builder_v3.go` (`associationDestination`, new `entityIsOrDescendsFrom` + `entityGeneralizations`) | Match the endpoint the context **is or descends from**: walk the generalization chain (`Entity.GeneralizationRef`, qualified parent name) so an association declared on a base entity resolves from a subclass context. Diagnose association-binding drops by checking whether the widget's entity context exactly equals the association endpoint — inheritance is the usual culprit. mxbuild-verified 0 errors across grid-over-base, listview, and grid-over-subclass. Tests: `TestResolveAssociationAttributePath_InheritedContext`; bug-test `mdl-examples/bug-tests/bug3-contentparam-inherited-association.mdl`. Bug 3 (inheritance) | -| A documented object-list widget child keyword parse-errors: `line lRevenue (...)` inside a LineChart/TimeSeries/BubbleChart, or `scalecolor scLow (...)` inside a HeatMap → `mismatched input 'line' expecting '}'`. `series` (etc.) works | The executor side was already complete — `deriveObjectListKeyword` maps `lines`→`LINE` and `scaleColors`→`SCALECOLOR`, and `buildObjectListItem` routes any object-list child generically via the widget's `.def.json`. The **grammar** just lacked the tokens: `LINE`/`SCALECOLOR` weren't lexer tokens nor `widgetTypeV3` alternatives | `mdl/grammar/MDLLexer.g4` (token) + `mdl/grammar/domains/MDLPage.g4` (`widgetTypeV3`) + `mdl/grammar/domains/MDLSettings.g4` (`keyword` rule) | Add the token, the `widgetTypeV3` alternative (alongside `SERIES`/`MARKER`), **and** the `keyword` rule entry (so a very common word like `line` stays usable as an identifier via `identifierOrKeyword`); `make grammar`. No executor/def changes needed — `deriveObjectListKeyword` already produces the singular-uppercase keyword and the def.json carries the mapping. Verify child data lands in BSON (`Lines`/`Series` array). Confirmed via `mdl-examples/doctype-tests/34-chart-widget-examples.mdl` (exec 0 errors; `line`/`scalecolor` persist). DESCRIBE round-trip is handled by the row below | -| `DESCRIBE` of a generic pluggable widget (chart) omits its object-list child blocks — `series`/`line`/`scalecolor` items are dropped, so describe→exec loses the chart's data. (Pre-existing; SERIES always had it.) DataGrid2/Gallery were unaffected — they have specialized column output | The generic `pluggablewidget` DESCRIBE branch only emitted scalar `ExplicitProperties`; nothing walked the widget's `WidgetObject` lists. (The DataGrid2 column reconstruction, `extractDataGrid2Columns`, was the only object-list reader and is hard-coded to `columns`.) | `mdl/executor/cmd_pages_describe_objectlist.go` (`extractObjectLists`, `buildObjectListNestedKeyMap`, `extractObjectListItem`, `objectListMDLKey`) + wired into `cmd_pages_describe_parse.go` (generic-pluggable branch) + `cmd_pages_describe_output.go` (emit child blocks) | Generalize the column-reconstruction pattern: `buildObjectListNestedKeyMap` (like `buildColumnPropertyKeyMap` but for any list key) resolves the item's `TypePointer→sub-key` map from `Type.ObjectType.PropertyTypes[listKey].ValueType.ObjectType.PropertyTypes`; per item, read datasource / AttributeRef / Expression / TextTemplate / PrimitiveValue and map the schema key to PascalCase MDL (`staticXAttribute`→`StaticXAttribute`). Keyword via `deriveObjectListKeyword` (lowercased). Only runs for the generic branch (`!isKnownCustomWidgetType`), so DataGrid2/Gallery keep their specialized output — no double-emission. Verified: describe→exec→describe is byte-identical on bar/line/heatmap. Tests: `TestExtractObjectListItem_ChartSeries`, `TestObjectListMDLKey`. | -| A HeatMap `scalecolor` entry's `ColorValue: '#rrggbb'` parses and passes `check` but the colour is **silently dropped on write** — describe shows only `ValuePercentage`, and the runtime scale has no colour. `valuePercentage` persists fine | The schema property is spelled **`colour`** (British), and `scaleColors` had no `ColorValue` MDL alias, so the engine looked up `colour`, didn't find `ColorValue`, and wrote the template default (empty). Identical mechanism to Bug 10a (`columnClass`←`DynamicCellClass`) — a missing `itemPropertyAliases` entry | `mdl/executor/widget_defs.go` (`itemPropertyAliases`, `heatmap.scaleColors.colour`) + `mdl/executor/widget_engine.go` (`WidgetDefGeneratorVersion` bump) | Add `"colour": {"ColorValue"}` to the HeatMap `scaleColors` aliases and **bump `WidgetDefGeneratorVersion`** so cached `.mxcli/widgets/heatmap.def.json` regenerate. DESCRIBE reconstructs it as `Colour:` (the PascalCase schema key — round-trips case-insensitively). Verified: `#rrggbb` now in BSON; describe→exec byte-stable. Test: `TestObjectListItemAliases_HeatMapColour`. Bug 10a class | -| A **PieChart/HeatMap** (charts that bind data at the WIDGET level, no series object-list) fails MxBuild **after `mx update-widgets`** with **CE0642** "Value attribute is required" and (PieChart) **CE4899** "Series name is required" — even though `mxcli check` + `exec` are clean. The datasource persists; the value attribute + series name don't. Only surfaces once CE0463 version-drift is cleared by update-widgets | These widgets expose several attribute-typed top-level properties (`seriesValueAttribute`, `seriesSortAttribute`) plus a required `seriesName` texttemplate. The engine's `resolveMapping` `"Attribute"` case read a single generic `w.GetAttribute()` — ambiguous across multiple attribute props and blind to the friendly MDL names — so `ValueAttribute:` never reached `seriesValueAttribute`; and `GenerateDefJSON` **skips all top-level texttemplate props**, so `seriesName` had no mapping. Item 1b | `mdl/executor/widget_defs.go` (`propertyAliases`, `GenerateDefJSON` attribute-alias + gated texttemplate case) + `mdl/executor/widget_engine.go` (`PropertyMapping.MdlAliases`, `namedPropValue`, `resolveMapping` `"Attribute"`/`"TextTemplate"`, version bump) | Add `PropertyMapping.MdlAliases` (top-level analog of `ItemPropertyMapping.MdlAliases`) + a `propertyAliases` map (piechart/heatmap `seriesValueAttribute`←`ValueAttribute`, piechart `seriesName`←`SeriesName`). `resolveMapping` `"Attribute"` now reads via `namedPropValue` (property key + aliases) and resolves against the widget datasource entity (the DataSource mapping is ordered first), falling back to `w.GetAttribute()` for single-attribute widgets. Emit a top-level texttemplate mapping **only** when an alias is registered (keeps the broad skip). Bump `WidgetDefGeneratorVersion`; the example must supply the required `SeriesName:`. mxbuild-verified: whole chart file = 0 errors after update-widgets. **Diagnose "required property" CE0642/CE4899 by running `mx update-widgets` first (clears CE0463), then check which schema key is missing from the BSON.** Tests: `TestResolveMapping_NamedAttribute`, `TestGenerateDefJSON_PieChartNamedProperties`. Widget-level DESCRIBE of `seriesName`/datasource is a separate gap. Item 1b | -| A view entity whose attribute is named after a Mendix OQL keyword — most often a date-part word like `Quarter`/`Month`/`Year` — passes `mxcli check`/`exec` but fails **MxBuild CE0174** "The 'Quarter' part is incomplete or incorrect. You could use here: … OPEN_QUOTE, or IDENTIFIER". A `Region`/`Period` column is fine | OQL reads the bare word as the keyword, not the attribute. **This row previously said mxcli cannot escape it and that `s."Quarter"` is a parse error — both are wrong**, and the error text quoted in the row disproves the first (it lists `OPEN_QUOTE` among what is valid there) | `mdl/executor/oql_type_inference.go` (`ValidateOQLSyntax`, `oqlReservedWords`, **MDL032**, **MDL072**); `mdl/executor/validate_oql_reserved_names.go` (**MDL071**) | **Quote it in a SOURCE position** — `select s."Quarter" …`, `from Module."Year" as s` — measured at 0 errors on 11.13.0, with the quotes passed through verbatim (`RawQuery` via `extractOriginalText`). The **alias** is the one position OQL will not take a quote in, for any name at all, so a view entity's own attribute — whose name IS its alias — has to be renamed; that is the only case that does. Read the two CE0174 texts side by side before concluding anything about quoting: the source position lists `ASTERISK, AT_SIGN, OPEN_QUOTE, or IDENTIFIER` and the alias position lists only `IDENTIFIER`. MDL071 warns at CREATE (where a rename is still cheap), MDL032 inside a view's OQL, MDL072 on the quoted-alias spelling. | -| A view entity with a **derived string column** — `cast(x as string)`, a string-returning `CASE`, or a string expression — passes `mxcli check`/`exec` but fails **MxBuild CE6770** "View Entity is out of sync with the OQL Query" whenever the declared attribute length is anything other than **200** (e.g. `string(30)`, `string(50)`, unlimited). The OQL itself runs fine, so it looks like a serialization gap but is a **platform rule**: Mendix normalizes a derived string column to the default length String(200); a plain pass-through column (`c.Name as Name`) instead inherits its source length | Two parts. (1) The rule: only *derived* string columns are forced to 200 — a bare attribute ref infers `TypeUnknown` and is skipped, so any concretely-typed String the static inferrer sees is derived → 200. (2) The checker was **dead**: `inferTypeStatic`/`inferTypeFromExpression`/`inferCaseType` uppercased the expr (`upper := ToUpper(...)`) then compared against **lowercase** literals (`HasPrefix(upper, "cast(")`, `"count("`, `"case"`, `== "true"` …), so every prefix check failed and only `DATEPART(` (uppercase literal) ever matched — CAST/CASE/COUNT/SUM/AVG/MIN/MAX/LENGTH inference all returned Unknown. Sibling of Bug 9b's `extractSelectClause` case bug | `mdl/executor/oql_type_inference.go` (`inferTypeStatic` CAST/CASE + case-fixed prefixes, `castTargetType`, `derivedStringLength=200`, `ValidateOQLTypes` string-length normalize, `typesStrictlyCompatible` length compare, `inferCaseType`, `inferTypeFromExpression`) — **MDL031** | Infer `cast(expr AS string)` and string CASE as `String(200)`; in `ValidateOQLTypes` normalize any inferred `TypeString` length to 200 (derived); `typesStrictlyCompatible` compares String **length** (not just kind), so `string(30)` vs `String(200)` is flagged with `Fix: change to 'X: String(200)'`. **Fix the case-comparison bug** (lowercase→UPPERCASE literals) so the checker actually runs. **Guard against false positives**: `SUM(` over an un-inferable inner (a bare attribute ref → Unknown) returns **Unknown**, not a guessed Decimal — otherwise `sum(s.Units)` declared `integer` false-fires (caught on `34-chart-widget-examples.mdl`). mxbuild-confirmed on 11.6.6: `string(30)`→CE6770, `string(200)`→0 errors. Tests: `TestValidateOQLTypesDerivedString`, `TestValidateOQLTypesNoFalsePositive`; bug-test `mdl-examples/bug-tests/view-entity-derived-string-length.mdl` | -| A view entity with a **pass-through string column** — a bare source-attribute reference like `select c.Name as CategoryName` — declared with a length **different from the source attribute** (most often an unbounded `string` against a `string(100)` source) passes `mxcli check` but fails **MxBuild CE6770** "View Entity out of sync". This is the counterpart of the *derived*-column rule (which forces 200): a pass-through column inherits the source length **exactly** | The references-mode validator (`validateViewEntityTypes`) compared with `typesCompatible`, whose String rule only guards **truncation** (`declared.Length >= inferred.Length`), so an unbounded `string` (Length 0) or a wider `string(200)` against a `string(100)` source slipped through. Only *derived* columns (via `ValidateOQLTypes`/`typesStrictlyCompatible`, syntax mode) were length-exact | `mdl/executor/oql_type_inference.go` (`passthroughStringLengthMismatch`, called in `validateViewEntityTypes` before the generic `typesCompatible`) | For a pass-through column (`col.SourceAttr != ""`, set only for a bare `alias.attr` ref — aggregates use a throwaway col so it stays empty) require **exact** String length match; emit the source entity/attr + inherited length in the message. **References-mode only** (needs the domain model to resolve the source attribute's length), so the repro is not a `.fail.mdl` (the syntax-only `make check-mdl` harness can't see it). Test `TestPassthroughStringLengthMismatch`; example `mdl-examples/bug-tests/ledger-36-view-passthrough-length.mdl`; mxbuild-confirmed on 11.12.1 (`string`→CE6770, `string(100)`→clean). Ledger finding #36 | -| `DESCRIBE` of a view entity on the **default (modelsdk)** engine omits the `as (… OQL …)` clause entirely (legacy renders it) — the describe→exec round-trip silently drops the query, leaving a queryless view. Reproduces on any Mendix **11.x** project | On Mendix 11.0+ the OQL is stored **only** in the separate `DomainModels$ViewEntitySourceDocument`; the inline `OqlViewEntitySource.Oql` field was removed (see the writer's `if !pv.IsAtLeast(11,0)` gate in `serializeOqlViewEntitySource`). The modelsdk read (`entityFromGen`: `out.OqlQuery = src.Oql()`) only read the now-empty inline field and never followed the `SourceDocumentRef`. Legacy was correct — `loadViewEntityOqlQueries` in `sdk/mpr/reader_documents.go` already joins the source docs | `mdl/backend/modelsdk/domainmodel.go` (`populateViewEntityOql`, wired into `ListDomainModels` + `GetDomainModel`) | After building the domain models, list `ViewEntitySourceDocument` units (`mprread.ListUnitsWithContainer[*genDm.ViewEntitySourceDocument]`), map `moduleName.docName → Oql()`, and fill `OqlQuery` for any view entity with an empty `OqlQuery` + a `SourceDocumentRef` (mirrors the legacy join; guarded by a cheap pre-check so non-view projects pay nothing). When a view-entity read differs between engines on 11.x, suspect a field the platform moved out-of-line into a source document. Test: `TestGetDomainModel_ViewEntityOqlFromSourceDocument` (round-trips via both read paths) | -| An `actionbutton`/`linkbutton` `icon:` property is silently dropped on write (flagged `MDL-WIDGET07` "property `icon` … will be silently dropped") — no way to give a button an icon, so the button renders without one. `linkbutton` (Link render mode) itself already worked | The button builder read Caption/Action/ButtonStyle but never the `icon` property, and the `Forms$ActionButton` codec default nulls the `Icon` field. Nothing built the Mendix icon element | `sdk/pages/pages_widgets_action.go` (`Icon` struct + `IconTypeIconCollection`) + `mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildButtonV3` reads `icon`) + `mdl/backend/modelsdk/widget_write.go` (`iconToGen`, wired into the `ActionButton` case) + describe (`cmd_pages_describe_parse.go` `extractIconRef`, `cmd_pages_describe_output.go` emit) + `validate_widgets.go` (`staticWidgetKnownProps` += `Icon`) | Build the modern **icon-collection** icon: `icon: 'Atlas_Core.Atlas_Filled.pencil'` → `Forms$IconCollectionIcon{Image: QN}` (verified against a Studio-Pro button — storage `$Type` is `Forms$`, not `Pages$`; no typed gen struct, so build it via `newElem`/`addStr`). `g.SetIcon(...)` overrides the null-Icon default. DESCRIBE reconstructs `Icon:` from the element's `Image`; add `Icon` to the known-props list so MDL-WIDGET07 stops warning. A non-existent icon name is (correctly) rejected by MxBuild with **CE1613** "selected custom icon … no longer exists". Tests: `TestBuildButtonV3_Icon`, `TestOutputWidgetMDLV3_ButtonIcon`; bug-test `mdl-examples/bug-tests/602-button-icon.mdl`. Issue #602 | -| `DESCRIBE WORKFLOW` on the **default (modelsdk)** engine renders some activities as non-round-trippable comments — `-- [Workflows$StartWorkflowActivity]`, `-- [Workflows$JumpToActivity]`, `-- [Workflows$WaitForTimerActivity]`, `-- [Workflows$WaitForNotificationActivity]` — so `describe → exec` drops those activities and you can't learn their syntax from an existing workflow. User tasks, decisions, splits, call-microflow/workflow round-trip fine (legacy round-trips all of them) | The describe **formatter** (`formatWorkflowActivities`) already has typed cases for jump/wait/start/end, but the modelsdk **reader** never produced them: these activities have no `genWf` struct (written via generic `newElem`), so `workflowActivityFromGen`'s type switch fell through to `GenericWorkflowActivity`, which the formatter renders as `-- [$Type]`. Legacy's `parser_workflow.go` had dedicated cases, so legacy was correct — modelsdk-only gap | `mdl/backend/modelsdk/workflow_read.go` (`workflowActivityFromGen` default → new `workflowSimpleActivityFromGen`) | Recognise the untyped simple activities by `el.TypeName()` and rebuild the semantic struct, reading fields from raw BSON via `genWf.RawFieldString` (`TargetActivity` for jump, `Delay` for the timer) — mirrors the legacy parser. Start/end become typed so the formatter **skips** them (implicit in MDL); jump/wait become executable statements. When an engine emits `-- [$Type]` in describe, the *formatter* usually already handles the type — the gap is the *reader* decoding it to `GenericWorkflowActivity`. Test: `TestWorkflowSimpleActivities_ReconstructedTyped`; round-trip validated describe→drop→exec→`docker check` = 0 workflow errors. Bug 11b (11a = the missing `write-workflows.md` authoring skill) | -| A built-in widget action (`actionbutton`/`container`/`linkbutton` `Action: microflow M.Flow(Param: $x)`) **silently drops the argument** on the **default (modelsdk)** engine — `describe` shows `Action: microflow M.Flow` (no args), the flow runs with no parameter, and a required-param flow never fires so the button/row no-ops at runtime. `mxcli check`/`docker check`/`docker build` all pass (0 errors) — silent break. Legacy persists it (the workaround) | `clientActionToGen`'s `MicroflowClientAction` case built the `Forms$MicroflowSettings` from the microflow **name only** (`microflowSettingsToGen(x.MicroflowName)`), never serializing `x.ParameterMappings` — the executor builds them correctly into the model, the writer threw them away. (NOT the same as `custom-widgets.md`'s "action mapping too complex for auto-mapping" — that's about *pluggable-widget action-typed properties* in def.json generation, a different path.) | `mdl/backend/modelsdk/widget_write.go` (`microflowSettingsToGen`) | Give `microflowSettingsToGen` the mappings param; emit one `genPg.MicroflowParameterMapping` each — `SetParameterQualifiedName(".")` (BY_NAME) + `SetExpression(variable-or-expression)` — mirroring the legacy `writer_widgets_action.go` BSON. The client-action caller passes `x.ParameterMappings`; the microflow-datasource callers pass `nil`. Verified: default-engine describe now keeps `(Param: $x)`; a type-correct page = 0 mxbuild errors (both engines give the *same* CE0117 on a type-mismatched arg — that's a model error, not the writer). Tests: `TestClientActionToGen_MicroflowParameterMappings`; bug-test `mdl-examples/bug-tests/bug1-widget-action-param-mapping.mdl`. Nanoflow widget actions were a separate gap (the whole `NanoflowClientAction` case was missing) — now fixed in Bug 2 below. Bug 1 | -| A microflow `declare $x Module.Entity [= $obj];` (object/entity-typed local variable) passes `mxcli check` but mxbuild rejects it with **CE0053** "Selected type is not allowed" (+ CE0038 "Value required", + CE7247 on a following `set`) — a silent trap. The declare-entity attribute qualifier is a red herring: the whole construct is invalid, bare or initialized | Mendix's Create Variable activity (what `declare` maps to) only holds **primitive** types; there is no object/list Create Variable form (same restriction as MDL040 for lists). Objects must come from a parameter, a `retrieve … limit 1`, a `create` object, or a loop iterator. The check was missing; the microflow parser records a bare `Module.X` declare type as the ambiguous `TypeEnumeration` (`EnumRef`, `ExplicitEnum=false`), an explicit `Enumeration(Module.X)` sets `ExplicitEnum=true` | `mdl/executor/validate_microflow.go` (`walkBody` `*ast.DeclareStmt` case, next to the MDL040 list check) | Add **MDL043**: flag `Type.Kind == TypeEntity \|\| (TypeEnumeration && EnumRef != nil && !ExplicitEnum)`; error points to parameter/retrieve/create/loop and notes "if it's an enum, write `Enumeration(...)`". Reliable connection-free (no backend) because bare object names resolve to a distinct AST shape from explicit enums. Also fix the skills that wrongly taught `declare $x Module.Entity;` / `declare $x as …` as valid (`write-microflows.md`, `cheatsheet-variables.md`, `cheatsheet-errors.md`, `check-syntax.md`, `patterns-crud.md`, `patterns-data-processing.md`, `business-events.md`, `resolve-forward-references.md`, `migrate-k2-nintex.md`, `README.md`). Test: `TestValidateMicroflow_DeclareObjectIsRejected`; bug-test `mdl-examples/bug-tests/declare-object-variable-rejected.fail.mdl`. mxbuild-confirmed (11.12.0) | -| No way to change an existing **enumeration value's caption** in place — `alter enumeration` had only `ADD`/`RENAME`/`DROP VALUE` + `SET COMMENT`; `RENAME VALUE X TO Y` changes the *name*, not the caption. The only route was drop + recreate, which fails while the enum is referenced by an attribute | Missing grammar action + AST op + executor case — a full-stack gap, not a code bug | `mdl/grammar/domains/MDLDomainModel.g4` (`alterEnumerationAction`) → `mdl/ast/ast_enumeration.go` (`AlterEnumOp`) → `mdl/visitor/visitor_enumeration.go` (`ExitAlterEnumerationAction`) → `mdl/executor/cmd_enumerations.go` (`execAlterEnumeration`) | Add `MODIFY VALUE IDENTIFIER CAPTION STRING_LITERAL` (reuses existing `MODIFY`/`CAPTION` tokens, so no lexer change); add `AlterEnumModifyCaption` op reusing the `Caption` field; executor finds the value by name and replaces only the `en_US` translation (preserves the value's ID + other locales). Re-captions in place, so it works while referenced (mxbuild 0 errors) — no drop needed. Value names in `alter` must be plain identifiers (a reserved-word-named value like `Created` can't be targeted — pre-existing, shared by ADD/RENAME/DROP). Tests: `TestAlterEnumeration_ModifyValueCaption` (visitor) + `TestAlterEnumeration_ModifyValueCaption_Mock` (executor); example in `01-domain-model-examples.mdl` | -| A page with a native **LISTVIEW** over a `database from` (XPath) datasource passes `mx check` but **crashes the browser client at runtime** and redirects to login: `TypeError: Cannot read properties of undefined (reading 'length')` at `processResult` → `retrieveByXPath`. Pluggable Gallery/DataGrid2 database sources are fine | The serialized `Forms$ListViewXPathSource` omitted the arrays the client reads `.length` of. **Codec (default):** `Forms$ListViewSearch` emitted without its `SearchRefs` list — the encoder drops an empty, never-`Set` PartList unless the `$Type` is in `RegisterTypeDefaults` (GridSortBar.SortItems was registered; ListViewSearch was not). **Legacy:** wrote a bogus `Forms$ListViewSort` + a `Paths` key (renamed `SearchRefs` in 7.11.0) and no `Forms$GridSortBar`. NOT a `SortItems` marker issue — an empty `[2]` compiles fine when search is off; the crash is the absent `SearchRefs`. Diagnose by building a Deploy target and reading the compiled `deployment/web/pages/.js` (the client model) + `mxcli bson dump --type page` on the source | `mdl/backend/modelsdk/widget_write.go` (`init` RegisterTypeDefaults) + `sdk/mpr/writer_widgets_display.go` (`serializeListViewDataSource`, `emptyListViewXPathSource`) | Codec: `RegisterTypeDefaults("Forms$ListViewSearch", {MandatoryLists: []string{"SearchRefs"}})` (emits empty marker-3 list). Legacy: emit `Forms$GridSortBar`/`SortItems` (mirror `SerializeCustomWidgetDataSource`) + `Forms$ListViewSearch`/`SearchRefs` + `ForceFullObjects`; drop the bogus `Sort`/`Paths`. Tests: `TestListViewSourceToGen_SearchRefsEmitted` (codec, encode-level), `TestSerializeListViewDataSource_Database` + `TestEmptyListViewXPathSource_Shape` (legacy). Repro `mdl-examples/bug-tests/listview-database-source-searchrefs.mdl` | -| Editing `themesource/**/main.scss` (or `theme/web/main.scss`) while `mxcli run --local` serves on `:8080` keeps showing **old styles** — reads exactly like a stale compiled-CSS cache. `rm -rf theme-cache/ .mendix-cache/ deployment/` "fixes" it only because a restart came with it | THREE distinct causes, none a CSS cache: (1) **no `--watch` = no watcher at all** — `mxbuild --serve` only rebuilds on a `/build` request (startup, or a watch tick), so a save changes nothing; (2) **stale process silently adopted** — a leftover serve/runtime on the ports answers the startup readiness probes (`waitReady`/`waitAdminReady` only check "port answers"), so a new run attaches to the OLD process and its own child is torn down by `defer`; a backgrounded `run --local` whose wrapping shell exited non-zero dies while its serve+runtime keep serving; (3) the theme source was **watched by nothing** — the `--watch` signal was model-only (`.mpr`+`mprcontents/`). The incremental theme step itself is FINE: one `/build` after an scss **content** edit does rewrite `theme-cache/web/theme.compiled.css` (verified), so there is no cache to clear | `cmd/mxcli/docker/runlocal.go` — `checkTargetPortsFree` (guard), `themeSourceMTime`/`sourceMTime` (watch signal), `watchAndApply` (generation log) | (2) Refuse to boot when `:8080/:8090/:6543` already answer, with an actionable message (never auto-kill — user's call). (3) Add `theme/`+`themesource/` scss/css/js to the `--watch` mtime-poll signal (`sourceMTime` = max(model, theme)); poll-based so it's container-safe (unlike the rollup chokidar/inotify web-client watcher). Log a build-generation counter (`build #2`) so "did it take?" is answerable. Docs: `docs-site/src/tools/run-local.md` + skill `run-local.md` — "SCSS needs a rebuild (`--watch` or clean restart), never a cache-clear; kill the old serve/runtime first". Tests `TestThemeSourceMTime_WatchesThemeAndThemesource`, `TestCheckTargetPortsFree` | -| Re-running a domain script that is 90% already-applied applies **none** of the remaining 10%: `alter entity … add attribute X` errors `attribute 'X' already exists` and, because `exec` halts on the first error, everything after it is skipped. No idempotent add/drop and no continue-past-errors | Two gaps: (1) `ADD ATTRIBUTE` / `DROP ATTRIBUTE` had no `IF NOT EXISTS` / `IF EXISTS` guard, so a re-apply was a hard error; (2) `ExecuteProgram` returns on the first statement error | grammar `mdl/grammar/domains/MDLDomainModel.g4` (`ifNotExists`/`ifExists`, `alterEntityAction`) + AST `mdl/ast/ast_entity.go` (`IfNotExists`/`IfExists`) + visitor `mdl/visitor/visitor_entity.go` (`ExitAlterEntityAction`) + executor `mdl/executor/cmd_entities.go` (add/drop guards) + `mdl/executor/executor.go` (`ExecuteProgramContinueOnError`) + `cmd/mxcli/cmd_exec.go` (`--continue-on-error`) | Add `IF NOT EXISTS`/`IF EXISTS` to the grammar (regen), carry the flag on the AST, and in the executor turn the already-exists / not-found error into a skip-with-notice when the guard is set. Separately add `mxcli exec --continue-on-error`: attempts every statement, prints each failure as `statement N: …`, exits non-zero if any failed (never masks a real error; `exit`/`quit` still stop the run). Bug-test `mdl-examples/bug-tests/f10-idempotent-alter-entity.mdl`. Findings #10 | -| `alter entity M.E add attribute X: autonumber;` (no seed) or `... add attribute Created: AutoCreatedDate;` (renamed AutoX) passes `mxcli check` "Check passed!" but fails the build (CE7247) / silently discards the name — while the SAME attribute in `create entity` is correctly flagged (MDL023 / MDL022) | The per-attribute checks (MDL021/022/023) only ran on `CreateEntityStmt`; the ALTER ENTITY ADD ATTRIBUTE path had no validation at all, so an attribute added later escaped every rule | `mdl/executor/cmd_enumerations.go` (`ValidateAlterEntity`, `validateEntityAttribute`) wired from `cmd/mxcli/cmd_check.go` + `cmd/mxcli/lsp_diagnostics.go` | Extract the CREATE loop body into `validateEntityAttribute(attr, persistent, entityName)`; add `ValidateAlterEntity(stmt)` that runs it on `AlterEntityAddAttribute`. The entity kind isn't known from ALTER, so the persistent-only MDL020 is skipped; the kind-independent MDL021/022/023 all run. Bug-test `mdl-examples/bug-tests/f6-autonumber-seed-alter.fail.mdl`. Findings #6 (alter path) | -| `create or modify entity M.E ( )` on an entity that already has more attributes **silently drops** every attribute not re-listed (36→2 attrs seen in practice), then widgets/microflows still bound to them fail the build with CE1613 — and the "already exists" error that leads users here recommends the destructive `create or modify` for a partial edit | `create or modify` rebuilds the entity from the statement alone and REPLACEs the stored one, so any omitted attribute is deleted with no warning; the `NewAlreadyExistsMsg` hint pointed at `create or modify` without distinguishing "replace whole" from "add one" | `mdl/executor/cmd_entities.go` (`droppedEntityMembers`, the warn block before `UpdateEntity`, and the `execCreateEntity` already-exists message) | Warn-only (non-blocking, the user asked to modify): before `UpdateEntity`, diff existing vs replacement members (`droppedEntityMembers` — named attrs case-insensitive + the four audit flags) and print what's dropped + point at `alter entity … add attribute` for incremental edits. Fix the already-exists message to recommend `alter entity` for a member change and reserve `create or modify` for a full replace. Bug-test `mdl-examples/bug-tests/f24-create-or-modify-dataloss.mdl`. Findings #24 | -| A microflow expression calls a function that doesn't exist (e.g. `randomInt(9)` — in some docs but not a Mendix built-in): parses, passes `mxcli check`, then fails the build with CE0117 "Error(s) in expression". Also: a Decimal-returning function (`random()`, `secondsBetween`, the duration `*Between` family) assigned to an Integer/Long variable fails CE0117, but `mxcli check` only caught bare `div` | (1) The func checker only validated arity for *known* functions; an unknown call name was ignored. (2) `checkNumericAssignment` used `SourceIsArithmeticDecimal`, which fires only on arithmetic roots, so a Decimal *function* result slipped through. Also the `*Between` duration funcs were mistyped Integer in `funcTable` | `mdl/exprcheck/unknown_funcs.go` (`UnknownFunctionCalls`, `SourceRejectedForIntegerTarget`, `nearestFunc`) + `mdl/exprcheck/func_checker.go` (`funcTable` between-date return kinds) + `mdl/executor/validate_microflow.go` (`checkExprFunctions` → MDL044; `checkNumericAssignment` now uses `SourceRejectedForIntegerTarget`) | Walk each expression for `CallExpr` names not in `funcTable` (which lists *every* built-in — a bare `name(...)` is always a built-in call) → **MDL044** with a Levenshtein/prefix "did you mean" hint. Extend the Decimal-into-Integer check to Decimal-returning non-rounding functions → **MDL041**. Correct `secondsBetween`/`minutesBetween`/`hoursBetween`/`daysBetween`/`weeksBetween` to Decimal (calendar variants stay Integer, millisecondsBetween stays Long). When adding a new Mendix built-in, add it to `funcTable` or MDL044 will false-positive. Bug-tests `f1-unknown-expression-function.fail.mdl`, `f2-decimal-func-into-integer.fail.mdl`. Findings #1, #2 | -| `index name on (cols)` inside `create entity` (or `alter entity add index name on (cols)`) fails with `extraneous input 'on' expecting '('` — the SQL-like form docs/users expect. The bare `index name (cols)` worked | `indexDefinition` had no `ON` token: `IDENTIFIER? LPAREN indexAttributeList RPAREN` | `mdl/grammar/domains/MDLDomainModel.g4` (`indexDefinition`) | Make `ON` optional: `IDENTIFIER? ON? LPAREN indexAttributeList RPAREN`, regen grammar. `buildIndex` reads columns from `IndexAttributeList` only, so ON can't be mistaken for a column and no visitor change is needed. Covers both CREATE (entityOption) and ALTER ADD INDEX (shared rule). Bug-test `mdl-examples/bug-tests/f4-entity-index-on.mdl`. Findings #4 | -| `retrieve … where [Seq = $Game/MoveSeq + 1]` fails with a bare `mismatched input '+' expecting ']'` — no hint that Mendix XPath can't compute values (this is a Mendix limitation, not an mxcli bug) | Mendix XPath constraints take a literal/token/variable/path on the value side, never an arithmetic expression; the parse error named the token but not the cause | `mdl/visitor/visitor.go` (`enhanceErrorMessage`, `looksLikeXPathArithmetic`/`xpathArithmeticRe`) | Do NOT add grammar support (mxbuild would still reject the XPath). Add an error hint keyed on `mismatched input '<+|*|div|mod>' expecting ']'` (`expecting ']'` only occurs inside a `[…]` constraint) explaining the limitation and the workaround: compute into a variable first, then compare. Also documented in `xpath-constraints.md`. Bug-test `mdl-examples/bug-tests/f8-xpath-arithmetic.fail.mdl`. Findings #8 | -| Design properties are written free-form: a `ColorPicker`/`ToggleButtonGroup` value serializes as a plain option (wrong `$Type` for Studio Pro's Appearance tab), and a typo'd key/value (they're case-sensitive) passes `mxcli check`. Also `show design properties ` reports "No design properties found for widget type container" for a valid widget | Root bug: `resolveDesignPropsKey` upper-cased the MDL keyword but the lookup map is **lowercase-keyed**, so `container`→`DivContainer` never resolved — leaving `resolveDesignPropertyValueType` dead code and the theme registry unused on the write/validate paths | `mdl/executor/theme_reader.go` (`resolveDesignPropsKey` case fix) + `mdl/executor/cmd_pages_builder_v3.go` (`astDesignPropToValue` takes theme props) + `mdl/executor/validate_design_properties.go` (new, MDL-WIDGET11/12) wired from `cmd/mxcli/cmd_check.go` + `cmd/mxcli/lsp_diagnostics.go` (cached `themeRegistry`) | Fix `resolveDesignPropsKey` to lower-case the lookup. On write, resolve each flat value's type from the registry **by matching the value against the property's declared options** (see the CE6084 correction below — the control type alone does NOT decide it). On check (`-p` only, when themesource defines properties), walk page/snippet/alter-page widget trees and warn: **MDL-WIDGET11** unknown key (case-sensitivity hint / valid-key list), **MDL-WIDGET12** invalid value (lists allowed values). Warnings, not errors — a newer theme may add keys/values (forward-compat, per `page-styling-support.md:402`). Skip compound (registry doesn't model sub-props) and widgets with no type-specific metadata (pluggable). Bug-test `mdl-examples/bug-tests/typed-design-properties.mdl` | -| Follow-up regression from the row above: after typed design properties merged, `mx check` fails **CE6084** "Expected design property _Flex container_ / _Column gap_ / _Align items Y_ … to be of type **Toggle button group**, but found **Custom**" on any page using a flat `ToggleButtonGroup` value (Atlas flex/spacing/typography, e.g. `'Column gap': 'Medium'`). Broke `TestMxCheck_DoctypeScripts` on `12-styling`, `15c-fragment-bindings`, `31-pluggable-datagrid-gallery-v010` (both engines) — green on unit tests, red only in `make test-integration` | `resolveDesignPropertyValueType` mapped `ToggleButtonGroup`→`custom` by control type. But a ToggleButtonGroup selection picks one of a **fixed option set**, so Studio Pro stores it as an **Option** — a `Custom` value type mismatches the declaration. Only a ColorPicker's **off-list** value (a free-form hex) is genuinely Custom. The value type is decided by the **value**, not the control | `mdl/executor/cmd_pages_builder_v3.go` (`resolveDesignPropertyValueType`, now takes the value and reuses `themeOptionAllowed`) | Make it value-aware: value ∈ declared options → `option` (Dropdown, ToggleButtonGroup, predefined ColorPicker swatch alike); off-list **and** `ColorPicker` → `custom`; else `option`; no metadata → `option`. Verified: the three doctype examples pass `mx check` = 0 errors on both engines. Test `TestAstDesignPropToValue_Typed` extended with the `Column gap: Medium` + ColorPicker swatch/hex cases. **Diagnosis pattern**: a value-type/BSON-`$Type` mapping keyed on a *declared control type* is a trap — verify it against `mx check`, never assert it from the type name alone (this is exactly how the original bug slipped in). **Process lesson**: this shipped red because `make test-integration` (mx-check doctype roundtrips) was not run before merge — run it, not just unit tests, for any page/widget-serialization change | -| The **nightly** matrix (Mendix 10.24 / 11.6 / 11.12) fails only on **10.24**: `TestMxCheck_DoctypeScripts/15c-fragment-bindings-examples` → `Execution error: failed to build page: building block not found: Atlas_Web_Content.List_Cards` (both engines). 11.6/11.12 pass; unit tests + push-test (single-version) pass. Looks like a "design property on 10.24" issue but isn't | `15c` demonstrates `use building block Atlas_Web_Content.List_Cards`, an Atlas UI building block that ships in **11.x but is absent from the 10.x Atlas** (the example comment wrongly said "present in every standard Mendix app"). The example had **no `-- @version:` gate**, so on a 10.24 project the whole file ran and mxbuild couldn't resolve the block. (12-styling gates its design-property section at line 186; 31 gates the whole file at line 1 — both already skip on 10.24) | `mdl-examples/doctype-tests/15c-fragment-bindings-examples.mdl` | Add `-- @version: 11.0+` immediately before the `create page … P002_Rebound_Block` block (its last statement) so `filterByVersion` skips only the building-block demo on 10.x; the fragment-binding statements above stay ungated and keep 10.24 coverage. Verified: 15c passes on **10.24** (10 lines skipped, 0 errors) and still runs+passes the section on **11.6.3** (0 errors). **Diagnosis pattern**: a nightly-only, version-specific doctype failure = an example using a construct (building block, widget, syntax) that doesn't exist in the oldest matrix version and lacks a `-- @version:` gate; reproduce locally with `MX_BINARY=~/.mxcli/mxbuild//modeler/mx go test -tags integration -run TestMxCheck_DoctypeScripts/` | -| Same nightly pattern, **CE6083** this time: `TestMxCheck_DoctypeScripts/15b-fragment-slots-examples` fails only on **10.24** (both engines) — `[CE6083] "Design property Card style is not supported by your theme"` at every `cardWrap` container. `Card style` is an **Atlas v3 design property (11.x); the 10.x Atlas theme doesn't define it** | The shared `define fragment Card` used `designproperties: ['Card style': on]` and is instantiated by every page in the file, so a `-- @version:` gate would have to gate the whole file (killing 10.24 coverage of the slot feature the example is actually about). Unlike 15c's building block, the design property was **incidental** to the example | `mdl-examples/doctype-tests/15b-fragment-slots-examples.mdl` | **Drop the incidental v3 design property**, keep `class: 'card'` (Atlas card styling works on every version) — the example demonstrates content *slots*, not design properties (those live in 12-styling, gated 11.0+). Verified: 15b passes on 10.24 **and** 11.6.3 (0 errors, both engines). **Gate vs remove rule**: if the version-specific construct IS the point of a self-contained section → `-- @version:` gate it (15c); if it's incidental and in a shared/expanded definition → remove it and use a cross-version equivalent (15b). **Proactive sweep** after any such fix: `grep -lE 'designproperties|use building block' mdl-examples/doctype-tests/*.mdl` and confirm each usage is either gated or version-safe (note the doctype test skips `*.test.mdl`/`*.tests.mdl`) | -| `mxcli run --local`: when a page action throws, the browser shows the generic Mendix error dialog and there is nothing to correlate it against — the runtime's own stdout/stderr (server stack trace, microflow `LOG` output) is swallowed, so a server-side bug can't be told apart from a client one | The runtime JVM was spawned with `cmd.Stdout=log; cmd.Stderr=log` where `log` is an in-memory `syncBuffer` surfaced only on a *startup* failure; during normal operation it goes nowhere on disk | `cmd/mxcli/docker/localboot.go` (`spawnAndConfigure`, `openRuntimeLog`, `LocalRuntime.logFile`, `LocalRuntimeOptions.RuntimeLogPath`) + `cmd/mxcli/docker/runlocal.go` (default `/.mxcli/runtime.log`) + `cmd/mxcli/cmd_run.go` (`--runtime-log`) | Tee the JVM's stdout+stderr to `/.mxcli/runtime.log` via `io.MultiWriter(log, file)` (the in-memory buffer still backs startup-error reporting). Append across restarts with a `=== runtime start … ===` marker; close the handle on Stop/reopen. Default on; `--runtime-log ` relocates, `-` disables. Print the path at boot. Test `TestOpenRuntimeLog`. Findings #25 | -| Follow-up to the above (#25 re-test): `run --local` writes `runtime.log` but it stays **nearly empty** — the JVM tee captures startup/JVM output only; **application** logs (microflow `LOG`, server-side exception stack traces) never reach stdout, so a page-action error still can't be diagnosed | A standalone runtime (launched via `runtimelauncher.jar`) attaches **no log subscriber** by default — unlike a Studio Pro / m2ee run, which calls `create_log_subscriber` **after** start. Mendix application logs flow to log *subscribers*, not stdout, so with none attached they go nowhere | `cmd/mxcli/docker/runtime_controller.go` (`RuntimeController.LogSubscriberFile`/`Stdout`, `attachFileLogSubscriber`, called at the end of `Start`) + `cmd/mxcli/docker/localboot.go` (`StartLocalRuntime` sets `ctrl.LogSubscriberFile` to the abs runtime-log path) | After a successful `start` (and on every restart's `Start`, since each fresh JVM has no subscriber), call the `create_log_subscriber` admin action with `{type:"file", name:"mxcli-run-local", autosubscribe:"INFO", filename:, max_size:1GiB, max_rotate:0}`. **`max_rotate:0` is load-bearing**: the JVM stdout tee holds an fd on the same file, and a rotate-rename would detach it. Best-effort (a logging failure must not fail an up runtime — warn to Stdout instead). Pass an **absolute** path (the runtime's cwd is `/runtime`, not mxcli's). Tests `TestStart_AttachesLogSubscriber`, `TestStart_NoLogSubscriberWhenUnset`, `TestStart_LogSubscriberFailureNonFatal`. Findings #25 (round 2) | -| `create or modify persistent entity` (even a byte-for-byte identical re-run) NULLs every column value on the next runtime DB sync — rows survive, values gone. Reports success (`Modified entity`), `mx check` = 0 errors; the loss only surfaces when something reads the data. Diagnose via the DB-sync count in `runtime.log` ("Executing N database synchronization command(s)") on an unchanged model | `execCreateEntity` minted a FRESH attribute ID for EVERY attribute each run, even unchanged ones. Mendix's DB synchronizer keys off attribute identity — a new ID reads as "attribute departed + new attribute added", so it drops and re-adds the column. Only the entity's own ID was preserved; ALTER ENTITY (the safe path) mutates loaded attributes in place, keeping IDs | `mdl/executor/cmd_entities.go` (`execCreateEntity`) | On CREATE OR MODIFY of an existing entity, build a name→ID map from `existingEntity.Attributes` and reuse the existing ID for retained attributes (only new attributes get a fresh ID); `attrNameToID` propagates the reused ID into validation rules + indexes. Also steer the "already exists in project" hint (`validate_duplicates.go`) toward `alter entity … add attribute` for entities. Test `TestCreateOrModifyEntity_PreservesAttributeIDs`; repro `mdl-examples/bug-tests/create-or-modify-preserves-attribute-ids.mdl`. Findings #13 | -| `alter page P { set class = 'x' on }` (lowercase property) hard-errors on EVERY built-in widget: `property "class" not found (widget has no pluggable Object)` — but `create page` accepts the same lowercase prop, and these aren't pluggable widgets | The MPR page mutator dispatched first-class props via a case-SENSITIVE switch on canonical casing ("Class"/"Caption"/"DynamicClasses"); a lowercase name missed every case and fell through to the pluggable-Object path. `create page` was fine (WidgetV3.GetStringProp is case-insensitive); the sibling MCP mutator already lowercased | `mdl/backend/pagemutator/mutator.go` (`setRawWidgetPropertyMut`) | `switch strings.ToLower(propName)` with lowercase case labels; keep the pluggable fallback (default) on the original casing (template keys are case-sensitive). Test `TestSetWidgetProperty_LowercaseFirstClassProps`; repro `mdl-examples/bug-tests/alter-page-lowercase-set-on-builtin.mdl`. Findings #1 | -| `call javascript action` (e.g. NanoflowCommons.OpenURL) passes `check --references` + `exec` but persists as `-- Empty action`; `mx check` fails CE0008 "No action defined." + CE0109 on the output variable. Affects microflows AND nanoflows | The default `modelsdk` engine's `microflowActionToGen` had no case for `*microflows.JavaScriptActionCallAction` → returned nil → ActionActivity written with no Action. Read path + legacy engine both handled it (hence check/exec looked green); nanoflows reuse the same converter | `mdl/backend/modelsdk/microflow_write.go` (`microflowActionToGen`) | Add the `JavaScriptActionCallAction` case, built directly to mirror the legacy serializer (storage keys JavaScriptAction/OutputVariableName; JS parameter mappings use the `ParameterValue` key, not the Java-style `Value`). Test `TestMicroflowActionToGen_JavaScriptActionCall`; repro `mdl-examples/bug-tests/javascript-action-call-persist.mdl`. Findings #11 | -| A widget EXPRESSION property (`dynamicclasses`/`visibleif`/`editableif`) that walks an association passes `mxcli check --references` but fails `mx check` with CE0117 "Error(s) in expression." Easy to trip: a data binding (`contentparams`) on the SAME widget can traverse the same association legitimately | No check inspected expression-typed widget property values for association steps (MDL-WIDGET04/07/etc. check placeholders/keys, not expression contents). Mendix client-side expressions cannot follow associations — only data bindings can | `mdl/executor/validate_widgets.go` (`validateWidgetExpressionAssociations`, called from `validateStaticWidget`) | New MDL-WIDGET13: for `DynamicClasses`/`VisibleIf`/`EditableIf`, regex `exprAssociationStepRe` (`/Ident.Ident/`) flags a module-qualified step between slashes; a plain attribute (`$obj/Slug`) or enum literal (`Mod.Enum.Val`, no leading slash) doesn't match. Fix for the user: denormalise the attribute onto the bound entity (calculated attribute) or use a data binding. Test `TestValidateWidgetExpressionAssociations`. Findings #4 | -| Checker HINTS send the author the wrong way: MDL001 (nested loop) recommends `retrieve $Match from $List where … limit 1` (a parse error — can't filter a list variable); MDL044 flags `count()` in an expression with "Did you mean 'round()'?" (count is an aggregate, not a typo); MDL044's hint cites `mxcli syntax expressions` (no such topic) | Message-only defects in the linter | `mdl/executor/validate_microflow.go` (MDL001 message ~216; `checkExprFunctions` ~283; `mendixAggregateFuncs`) | MDL001 → recommend `$Match = FIND($List, )` (in-memory O(N); also fix CLAUDE.md idiom #2); MDL044 → for `count/sum/average/minimum/maximum` emit "aggregate activity, not an expression function — assign to a variable first: `$n = count($List);`" instead of a did-you-mean; point the generic hint at `mxcli syntax microflow`. Findings #7/#8/#14c | -| A microflow-CALL output variable reused across a fallback chain (`$S = call M.Inner(...); if … then $S = call M.Inner(...) end if;`) — the natural "try A else try B" — fails the build CE0111 "Duplicate variable name" | Each `$Var = call microflow/create/retrieve …` is a *fresh* variable creation; reusing the name (even inside an if) is a duplicate. `check --references` runs the flowBuilder validation (`validateOutputVariable`) which catches it — bare `check` (no project) does not | `mdl/executor/cmd_microflows_builder_validate.go` (`validateOutputVariable`, `validateScopedStatements`) | Already caught by the shared create-output-var check (same fix as the retrieve/create case); locked in by `TestValidateDuplicateMicroflowCallOutputVar` (same-scope + nested-in-if). Fix for the user: one variable per call, then a plain `set` picks the winner (documented in write-microflows.md). Finding #5 (2nd trigger) | -| `textbox` silently drops `placeholder` and `onchange` (MDL-WIDGET07 warns "unrecognized property … dropped"); after `create page` + `DESCRIBE`, both are gone — the live-search box degrades to a button, the field loses its hint text | The `pages.TextBox` model already had `Placeholder`/`OnChangeAction` fields, and the modelsdk (default) engine's `widgetToGen` already serialized them — but (a) the grammar had no `onchange:`/`placeholder:` property, (b) `buildTextBoxV3` never populated the model, (c) the legacy `serializeTextBox` hardcoded empty, (d) they weren't in `staticWidgetKnownProps` | grammar `mdl/grammar/domains/MDLPage.g4` (`widgetPropertyV3`); visitor `mdl/visitor/visitor_page_v3.go`; `mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildTextBoxV3`); `sdk/mpr/writer_widgets_input.go` (`serializeTextBox`) + `writer_widgets.go` (`serializePlaceholderTemplate`); `validate_widgets.go` known-props | Add `ONCHANGE COLON actionExprV3` + `PLACEHOLDER COLON STRING_LITERAL` (tokens already existed); visitor → `Properties["OnChange"]`/`["Placeholder"]`; `GetOnChange()`/`GetPlaceholder()` accessors; builder sets `tb.Placeholder` (a `*model.Text`) + `tb.OnChangeAction` (via `buildClientActionV3`, reusing the button path); legacy serializer emits `serializePlaceholderTemplate(tb.Placeholder)` + `serializeClientAction(tb.OnChangeAction)`; add `OnChange`/`Placeholder` to `staticWidgetKnownProps`. Tests `TestSerializeTextBox_PlaceholderAndOnChange`; repro `mdl-examples/bug-tests/textbox-placeholder-onchange.mdl`. Finding #9 | -| `call javascript action` / `call java action` with a wrong-cased or misspelled PARAMETER name passes `check --references` (the action itself resolves) but writes a dangling reference that fails the build with CE1613 "The selected … parameter … no longer exists". E.g. `NanoflowCommons.OpenURL(url = …)` when the parameter is `Url` | The reference checker resolved only the action NAME (`buildJava*ActionQualifiedNames` returns names, discarding params); the call's `CallArgument` names were never compared to the action's declared parameters | `mdl/executor/validate.go` (`flowRefCollector` → `codeActionCallRef`, `validateCodeActionParams`) | Carry the call's `argNames` on the collector; after the name-exists check, `ReadJavaScriptActionByName`/`ReadJavaActionByName` → `.Parameters[].Name`, diff case-sensitively; casing-only mismatch → did-you-mean. Skip `System.*` (runtime-provided) and degrade to no-error when the backend reports no params. References-mode only (needs `-p`). Tests `TestValidateCodeActionParams`. RSS-reader follow-up finding | -| `DESCRIBE PAGE` omits a textbox's `placeholder`/`onchange` even though they are written correctly (present in the .mxunit, live app works) — so a DESCRIBE round-trip is not a reliable way to confirm the write landed | The describe read path (`parseRawWidget`) only read `LabelTemplate` + `AttributeRef` for a textbox; `PlaceholderTemplate` and `OnChangeAction` were never read back into `rawWidget` | `mdl/executor/cmd_pages_describe_parse.go` (`extractPlaceholderText`), `cmd_pages_describe.go` (`rawWidget` fields), `cmd_pages_describe_output.go` (`renderClientActionMDL`/`extractOnChangeAction` + TextBox emit) | Add `Placeholder`/`OnChange` to `rawWidget`; read `PlaceholderTemplate` via `extractTextFromTemplate` (same as label) and `OnChangeAction` via a key-agnostic `renderClientActionMDL` (refactored out of `extractButtonAction`, since OnChangeAction is the same client-action type under a different key); emit `Placeholder:`/`OnChange:` in the TextBox case. Single describe path serves both engines (reads raw BSON via `GetRawUnit`). Test `TestParseRawWidget_TextBoxPlaceholderAndOnChange`. RSS-reader follow-up (verification note on #9) | -| `DESCRIBE PAGE` of a `dynamictext` bound to a NON-STRING attribute (Integer/DateTime/…) emits `ContentParams: [{1} = toString($currentObject/Attr)]`; re-applying that output fails the build with CE1613 "attribute '…toString($currentObject/Attr)' no longer exists" — the rendered expression is treated as an attribute NAME. A string binding round-trips fine (bare attribute name) | The write side converts a non-String attribute binding to `toString($currentObject/Attr)` (`resolveTemplateAttributePathFull`), stored as a ClientTemplateParameter `Expression`; the describe reader emitted the Expression verbatim instead of reversing the transform | `mdl/executor/cmd_pages_describe_output.go` (`unwrapToStringAttrParam` in `extractClientTemplateParameters`) | When a ContentParam Expression is exactly `toString($currentObject/)` or `toString($param/)` (the auto-generated forms), emit the bare `` / `$param.attr`; the write side re-derives the toString on the next apply, so it round-trips. Any other expression (extra text, hand-written toString) is left untouched. Tests `TestUnwrapToStringAttrParam`, `TestParseRawWidget_DynamicTextNonStringAttribute`. RSS-reader follow-up finding | -| A `contentparams`/`captionparams` value bound to a client EXPRESSION (`[{1} = formatDateTime($obj/LastImport, 'd MMM yyyy')]`) passes `mxcli check` but Studio Pro rejects the page with CE1613 "attribute … no longer exists" — the whole expression is stored as a bogus attribute name. A plain attribute path (`$obj/Attr`) or quoted literal (`'text'`) works | **mxcli** treats a template-parameter slot as a data binding: `buildClientTemplateParams` stores any unquoted value as an attribute path (`resolveTemplateAttributePathFull`), so an expression becomes a bogus attribute name. No check inspected the value for expression syntax. NOTE the original row said a template parameter *is* a data binding "not an expression" — that is a claim about MENDIX and it is **false**: Studio Pro's Edit Template Parameter dialog offers `Parameter type: Value | Expression`, with its own editor, variable list and wizard, and `Pages$ClientTemplateParameter` carries `Expression` beside `AttributeRef` and `SourceVariable`. The limit is mxcli's | `mdl/executor/validate_widgets.go` (`validateTemplateParamExpressions`, called from `validateStaticWidget`) | New **MDL-WIDGET14**: for each `ContentParams`/`CaptionParams` value, skip quoted string literals, then regex `templateParamExprRe` (`Ident(` function call or an arithmetic/comparison operator) flags an expression; an attribute path never matches. Fix for the user: set the parameter's type to Expression in Studio Pro, or bind an attribute path / quoted literal in MDL — do NOT send them to build a calculated attribute for something the platform already offers (the message used to, which is unnecessary modelling). Test `TestValidateTemplateParamExpressions`; bug-test `mdl-examples/bug-tests/contentparam-expression-rejected.fail.mdl`. Ledger finding #26 | -| A microflow expression using `dateTime(2026, $Month, $Day)` (a variable/computed arg) passes `mxcli check` but fails the build with CE0117 — Mendix builds `dateTime()`/`dateTimeUTC()` from hardcoded numeric constants only | No check inspected date-construction arguments; the function name resolves so the unknown-function check (MDL044) is silent | `mdl/executor/validate_microflow.go` (`checkDateTimeLiterals`/`exprHasNonLiteralDateTime`, MDL046) | New **MDL046**: walk the expression for a `FunctionCallExpr` named `dateTime`/`dateTimeUTC` with any non-`LiteralExpr` argument. Fix for the user: step off a literal anchor date — `addDays(addMonths(dateTime(2026,1,1), $Month-1), $Day-1)` (addDays/addMonths take variables). Test `TestValidateMicroflow_DateTimeLiterals`; repro `mdl-examples/bug-tests/ledger-21-datetime-literals.fail.mdl`. Ledger finding #21 | -| A retrieve constraint `where [Ledger.Transaction_Category = empty]` (an ASSOCIATION `= empty`) passes `mxcli check` but fails the build with CE0161 "Error(s) in XPath constraint" — XPath `= empty` tests attribute nullability, not association nullability | No check inspected constraint strings for an association compared to `empty`; a bare attribute `= empty` IS valid, so a blanket ban would false-positive | `mdl/executor/validate_microflow.go` (`checkXPathAssociationEmpty`/`xpathAssocEmptyRe`, MDL047) | New **MDL047**: on a `RetrieveStmt.Where` (via `expressionToXPath`), regex a **module-qualified** name (one dot) directly `= empty`, with a leading boundary class that excludes `/` (so `Assoc/Attr = empty` — a valid attribute-over-association test — is NOT flagged) and `.`/word (so a 3-part enum literal tail isn't grabbed). Fix for the user: `[not(Assoc/Module.Target)]`. **Also covers page/widget datasource where-clauses** (`validateDatasourceXPathAssociationEmpty` on `DataSourceV3.Where`, shared regex via `xpathAssociationEmptyMatches`) — the ledger project hit it there first. Tests `TestValidateMicroflow_XPathAssociationEmpty`, `TestValidateDatasourceXPathAssociationEmpty`; repros `ledger-25-xpath-assoc-empty.fail.mdl` (retrieve) + `ledger-25-page-datasource-assoc-empty.fail.mdl` (datagrid). Ledger finding #25 | -| Two sibling `dynamictext` widgets in a container render as `€ 310Last import: 7/24/2026` — concatenated with no separator, regardless of each one's RenderMode | A Mendix DynamicText is always an inline ``; RenderMode does not make it block-level. Not a build error — a silent layout surprise, so a non-fatal advisory fits | `mdl/executor/validate_widgets.go` (`validateConsecutiveDynamicText`, called from `validateWidgetTreeIn`) | New **MDL-WIDGET15** (info): scan each sibling list; emit once when a run of ≥2 adjacent **inline** dynamictexts occurs. **Only H1–H6 are block-level** (`headingRenderModeRe`) — `Text`/unset AND `Paragraph` both render as an inline `` (verified on Mendix 11.12.1 + Atlas), so `inlineDynamicText` excludes only headings. A heading+subtitle pair is NOT flagged; a Paragraph+Paragraph pair IS (it fuses). Fix for the user: merge into one dynamictext, wrap each in a container, or use a **heading** RenderMode — NOT Paragraph. Info severity so it never fails the build. Test `TestValidateConsecutiveDynamicText`; repro `mdl-examples/bug-tests/ledger-27-consecutive-dynamictext.mdl`. **Lesson: verify Mendix render behavior empirically — `Paragraph` sounds block-level but isn't; don't infer `display` from a name.** Ledger findings #27/#29 | -| `create association … from/to ` passes `mxcli check --references` and `mxcli exec` creates it, but `mx check` rejects it with **CE6771** "It is not possible to create associations to/from View Entities." Both directions are invalid | Associations to/from a view entity are statically impossible, but the reference checker only validated that the endpoint modules exist — it never inspected whether an endpoint was a view entity | `mdl/executor/validate.go` (CreateAssociationStmt case) + `mdl/executor/cmd_entities.go` (`isViewEntity`) | Resolve both endpoints via `findEntity`; if either `isViewEntity` (Source `DomainModels$OqlViewEntitySource`, or OqlQuery/SourceDocumentRef set), reject with CE6771 and point at a non-persistent entity carrying a real reference. Same-script endpoints are skipped (validated on their own statement). **References-mode only** (needs the domain model). Tests `TestIsViewEntity`; example `mdl-examples/bug-tests/ledger-41-view-entity-association.mdl`. Verified on Mendix 11.12.1. Ledger finding #41 | -| A `create or modify entity` that omits (drops) an INDEXED attribute leaves the entity's index orphaned — its column references a GUID that no longer exists. `mxcli` prints only the attribute-drop warning; `mx check` then **CRASHES loading the project** with `System.AggregateException` "The given key '' was not present in the dictionary" (DESCRIBE shows a dangling `index ()`) | Two parts. (1) The executor rebuilt the entity from the statement (no index clause → empty `entity.Indexes`), so the existing index wasn't reconciled against the surviving attributes. (2) The write path: `entityToGen` produced an **empty, untouched** Indexes PartList, which the codec treats as "clean" and passes the raw (orphaned) index through — an empty typed list does NOT override raw bytes for an existing element | `mdl/executor/cmd_entities.go` (`reconcileDroppedIndexes`, before `UpdateEntity`) + `mdl/backend/modelsdk/domainmodel_alter.go` (`UpdateEntity` empty-index dirtying) | (1) When the statement lists no indexes, carry existing indexes forward, pruning columns for dropped attributes (attr IDs survive by name via #13) and dropping empty indexes — a **partial** drop (`index (A,B)` → drop B → `index (A)`) works via this non-empty list. (2) For the **all-removed** case, force the empty Indexes list dirty (`AddIndexes(NewIndex())` + `RemoveIndexes(0)` — `PartList.Remove` calls `markDirty`) so the codec re-emits it as empty, clearing the raw orphan. **Codec insight: an untouched empty PartList is `clean` → raw passthrough; only a *dirtied* list (even if empty) overrides raw for an existing element.** Tests `TestReconcileDroppedIndexes`; example `mdl-examples/bug-tests/ledger-39-drop-indexed-attribute.mdl`. **Verified end-to-end: `mx check` → 0 errors on Mendix 11.12.1** (previously crashed). Ledger finding #39 | -| `retrieve … where [id = $Var]` (constraining on the object id) passes `mxcli check` but fails the build with **CE0161** "Error(s) in XPath constraint" — whether `$Var` is String or Long. Mendix XPath has no id operator reachable from a microflow expression | No check inspected retrieve constraints for an id comparison; `id` is a reserved member so it resolves loosely | `mdl/executor/validate_microflow.go` (`checkXPathIdConstraint`/`xpathIdConstraintRe`, MDL048) | New **MDL048**: on `RetrieveStmt.Where`, regex a bare `id` (word-boundaried, case-insensitive) before a comparison, **capturing the operand**, and flag only when the operand is a VALUE — a `$`-var whose kind is a primitive (in `varKinds`; objects aren't) or a string/number literal. **Comparing `id` to an OBJECT variable (`[id != $obj]`, the valid "exclude self" pattern) is NOT flagged** — verified valid on mx check; an over-broad `\bid\b\s*[=…]` regex false-positived `16-xpath-examples.mdl`'s exclude-self example. Fix for the user: a marketplace GUID action (GetObjectByGuid), **or** expose the id as a String on a view entity (`cast(id as string) as ObjectId`) and constrain on that String column. Test `TestValidateMicroflow_XPathIdConstraint`; repro `mdl-examples/bug-tests/ledger-42-retrieve-by-id.fail.mdl`. **Lesson: `[id = $x]` splits on the operand type — value → CE0161, object → valid; a checker that ignores the operand mislabels the valid case.** Ledger finding #42 | -| A call argument bound to an association-object path (`call M.Consume(B = $Edit/M.Edit_Budget)` — the object reached over an association) passes `mxcli check` but fails the build with **CE0117** "Error(s) in expression." An attribute value over the same association (`$Edit/M.Edit_Budget/Name`) is fine | Mendix does not treat an association path as a value — it must be materialized (`retrieve`) first. No check inspected call arguments for an association-object path | `mdl/executor/validate_microflow.go` (`checkAssociationObjectArgs`/`exprIsAssociationObjectPath`, MDL049; wired for CallMicroflowStmt + CallNanoflowStmt) | New **MDL049**: an argument whose value is an `AttributePathExpr` whose FINAL segment is module-qualified (a `.` → an association, yielding an object) is flagged; a final bare segment (an attribute) is not. Fix for the user: `retrieve $x from $Edit/M.Edit_Budget;` then pass `$x`. Test `TestValidateMicroflow_AssociationObjectArg`; repro `mdl-examples/bug-tests/ledger-44-assoc-path-as-value.fail.mdl`. Ledger findings #43/#44 | -| ~~MDL050 "format function + association navigation"~~ **REMOVED — it was a false positive.** MDL050 flagged `formatDateTime($obj/Mod.Assoc/Date, …)` and `formatDecimal(…) + $obj/Mod.Assoc/Attr` as CE0117. Re-verification against `mx check` on 11.12.1 (once the real #48 root cause — the dropped association target-entity step — was fixed) showed the premise was wrong on BOTH cases | The earlier "format-function + association" correlation was an artifact of TWO unrelated bugs, neither of which is about format functions: (1) **association navigation dropped its target-entity step** → CE0117 for ANY `$obj/Assoc/Attr` in an expression (see the "#48 root cause" row above), which happened to include the formatDateTime example; (2) **`formatDecimal($x, 2)` fails CE0117 on a PLAIN local decimal** with no association at all — a `formatDecimal`-signature bug, not an association issue. With (1) fixed, `formatDateTime($obj/Assoc/Date, …)` builds clean → MDL050 rejected valid code | `mdl/executor/validate_microflow.go` (removed `checkFormatWithAssociation`/`exprHasRenderFunc`/`exprHasAssociationNav`/`renderFuncsIncompatibleWithAssoc`) | Deleted the check, its call sites, its test (`TestValidateMicroflow_FormatWithAssociation`), and its bug-test. **Lesson (reinforced): even after "reproducing the failing neighbours", a rule can still be mis-premised if the neighbours share a DIFFERENT hidden bug — here the missing entity step. Verify a check is still valid after every related write-path fix; a correlation-based rule is fragile.** The remaining real issue — `formatDecimal($x, precision)` failing CE0117 regardless of associations — is a **separate open finding** (likely a wrong function signature/arity; investigate against `mx` and fix in the executor or flag via the function checker). Ledger finding #48 (root cause corrected; MDL050 retired) | -| **Attribute-over-association navigation in a microflow EXPRESSION** (`$T/Mod.Assoc/Attr` in a Change Variable / if-condition / return) passes `mxcli check` but `mx check` fails with **CE0117** "Error(s) in expression". Trips hardest when the module or entity name ends in a digit (`L48.`, `Account2.`) or when a decimal literal co-occurs (`$T/Mod.Assoc/Bal + 2.0`). Studio Pro supports this pattern | Mendix expression syntax needs the **target-entity step** in the path (`$T/Mod.Assoc/Mod.Entity/Attr`); the builder inserts it via `resolveAssociationPaths`→`resolvePathSegments`. Two bugs skipped that insertion: (1) `shouldPreserveExpressionSource` treated a `.` **preceded by a digit** as a decimal literal, so a name segment ending in a digit froze the whole expression's source and bypassed resolution (source is written verbatim); (2) a **legitimately** source-frozen expression (real decimal / whitespace) also bypassed resolution, so association nav + decimal in one expression broke even for non-digit modules | `mdl/visitor/visitor_microflow_statements.go` (`shouldPreserveExpressionSource` + `dotIsQualifiedNameSeparator`/`isIdentByte`); `mdl/executor/cmd_microflows_builder.go` (`resolveAssociationPaths` SourceExpr arm + `collectAttributePaths`) | (1) A `.` is a decimal point only when its adjacent digit run is a standalone number — NOT when the preceding token contains a letter/underscore (a qualified-name segment). `dotIsQualifiedNameSeparator` walks back over ident chars; a letter/`_` means name, not decimal. (2) For a source-preserved `SourceExpr`, still rewrite association paths **inside the raw source**: for each `AttributePathExpr`, `expressionToString(orig)` vs `expressionToString(resolve(orig))`; if they differ, `strings.ReplaceAll` in the source. Keeps decimal/whitespace fidelity AND inserts the entity step. Tests: `TestShouldPreserveExpressionSource_Decimals` (digit-module cases); example `mdl-examples/bug-tests/ledger-48-assoc-nav-digit-module.mdl`. **Verified: `mx check` → 0 errors on 11.12.1** for plain nav, if-then-else enum compare, and decimal-mixed. **Lesson: a heuristic that keys on a single character (`.`→decimal) collides with other grammar (qualified names); and "preserve source" and "resolve semantics" are in tension — a frozen source still needs its association paths fixed.** Ledger finding #48 (root cause) | -| An action property on a pluggable widget — e.g. `onClick: microflow …` on a DataGrid2 — passes `mxcli check --references` AND `mx check`, builds and runs, but is **silently discarded**: `DESCRIBE PAGE` omits it and the model round-trips minus the action, with no error or warning | The .def.json **generator** never emitted an `action` operation for `type="action"` properties (across 42 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 | `mdl/executor/widget_defs.go` (`GenerateDefJSON` `case "action"` + `actionSourceForKey`); `mdl/executor/widget_engine.go` (`resolveMapping` `case "OnChange"`; `WidgetDefGeneratorVersion` bump 12→13) | Emit an `action` `PropertyMapping` for the action slots MDL can author: `onClick`→source `OnClick` (reads the widget's `Action` property, i.e. the `onClick:`/`Action:` alias) and `onChange`→source `OnChange`. The engine's **existing** `applyOperation "action"` → `builder.SetAction` path (nil-guarded) then serializes the `ClientAction` with its parameter mapping. Non-authorable action slots (DataGrid2 `onSelectionChange`/`onConfigurationChange`) return `""` from `actionSourceForKey` → no mapping (no MDL surface yet). Bump the generator version so existing projects regenerate. Tests `TestGenerateDefJSON_ActionMapping`; example `mdl-examples/bug-tests/ledger-67-pluggable-widget-action.mdl`. **Verified end-to-end: `mx check` → 0 errors on 11.12.1**, and the DataGrid2 persists the onClick microflow action with its `Thing: $currentObject` mapping (the raw page unit carries `L67.OnClick` + `L67.OnClick.Thing`). Ledger finding #67 (write path) | -| **General guard for the #67 class:** a real widget property that 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 #67 for any unmapped property type (`expression`, `icon`, a future type, an action slot with no MDL surface) | The `WidgetDefinition.KnownProperties` field + the `MDL-WIDGET06` "recognized but not persisted" path 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 (via an alias like `Action`) passed silently | `mdl/executor/widget_defs.go` (`GenerateDefJSON` — populate `KnownProperties`); `WidgetDefGeneratorVersion` bump 13→14 | Compute `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 `knownUnmappedProperties`/MDL-WIDGET06 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.** Test `TestGenerateDefJSON_KnownPropertiesUnmapped`. **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.** Ledger finding #67 (general guard) | -| **DESCRIBE read gap (follow-up to #67):** after the write fix, `DESCRIBE PAGE` still omitted a DataGrid2's `onClick` action — a describe round-trip silently lost it | The datagrid2 describe parse/output paths read datasource + columns + paging but never read the widget-level action; the param-aware `renderClientActionMDL` reader (used for button/onchange) was never called for a pluggable widget's action | `mdl/executor/cmd_pages_describe_pluggable.go` (`customWidgetPropertyActionMap`), `cmd_pages_describe.go` (`rawWidget.OnClick`), `cmd_pages_describe_parse.go` (datagrid2 branch), `cmd_pages_describe_output.go` (datagrid2 emit) | New `customWidgetPropertyActionMap` returns the raw `Forms$*ClientAction` map for a CustomWidget property (NoAction → nil); the read is wired in **two** describe paths — the `datagrid2` branch AND the **generic `pluggablewidget '…'` branch** (`!isKnownCustomWidgetType`, where **CustomChart — the finding's actual widget — is described**). Each renders it via `renderClientActionMDL` (param-aware) into `rawWidget.OnClick`; the output emits `onClick: ` (the generic branch's guard also fires on `w.OnClick != ""` so an action-only widget still gets its `pluggablewidget` header). **Verified end-to-end on BOTH DataGrid2 and CustomChart: describe → re-exec → `mx check` 0 errors, round-trip preserves the full `onClick: microflow …(param: $currentObject)` including the parameter mapping** (CustomChart quotes the param name, `"Data":`, which re-parses fine). Test `TestCustomWidgetPropertyActionMap`. Ledger finding #67 (DESCRIBE read gap closed for datagrid2 + generic pluggable/CustomChart) | -| Two loops in the same microflow reusing the same iterator name (`loop $R in … end loop; loop $R in … end loop`) pass `mxcli check` but `mx check` fails 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. No check tracked loop iterator names across a microflow | `mdl/executor/validate_microflow.go` (`checkDuplicateLoopVariables`, MDL052) | New **MDL052**: walk the microflow body (recursing through if/case/while/loop bodies) collecting `LoopStmt.LoopVariable` names; flag any name used by a second loop. Catches sequential AND nested reuse (a nested loop reusing an outer iterator is also CE0111). Distinct iterators, a single loop, and the same name across DIFFERENT microflows are fine. Fix for the user: give each loop a distinct iterator (`$R`, `$C`, `$M`). Test `TestValidateMicroflow_DuplicateLoopVariable`; repro `mdl-examples/bug-tests/ledger-64-duplicate-loop-variable.fail.mdl`. Ledger finding #64 | -| A `break` nested inside an `if`/`case` within a loop passes `mxcli check` but 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. A break placed **directly** in the loop body serializes fine | The flow builder (`addBreakEvent`) creates the Break event but the sequence flow connecting it from inside a conditional dangles — a write-path (flow-graph serialization) bug, still open. `break` directly in the loop body wires correctly | `mdl/executor/validate_microflow.go` (`loopBodyHasConditionalBreak`/`stmtsContainBreak`, MDL051) — **interim check**, pending the serialization fix | New **MDL051**: on a `LoopStmt`, scan its body for a `break` inside an if/case/inheritance-split (not descending into nested loops — a nested loop traps its own break); a direct-child break is not flagged. Since the pattern currently produces a *crash*, a check-time rejection is a strict improvement. Fix for the user: a guard variable (`declare $Done Boolean = false; … if not($Done) then … set $Done = true`) — **verified clean on mx check**. Test `TestValidateMicroflow_ConditionalBreak`; repro `mdl-examples/bug-tests/ledger-52-break-in-conditional.fail.mdl`. Verified on 11.12.1: conditional break → KeyNotFound crash, direct break + guard-var workaround → 0 errors. **The real fix is the break-in-conditional flow serialization (write path) — MDL051 is the interim guard.** Ledger finding #52 | -| `set $Match = contains($Hay, $Needle)` over two **String** values was serialized as a **List operation** activity 'Contains', so string `contains(haystack, needle)` was unusable: a pre-declared Boolean `$Match` collided (**CE0111** "Duplicate variable name"), and a String input to a list op is otherwise rejected (CE0023/CE0097). `contains` in an IF condition worked (stayed an expression) | `contains` is overloaded — a LIST operation `contains(list, object)` **and** a STRING function `contains(haystack, needle)` — but the visitor unconditionally rewrote `set $x = contains(...)` into a `ListOperationStmt`, **dropping literal arguments** (`extractVariableName` returns "" for a literal). A list operation CREATES its output variable; the string function assigns to a pre-declared one | `mdl/visitor/visitor_microflow_statements.go` (`buildSetStatement` CONTAINS case + `isPlainVariableArg`) + `mdl/executor/cmd_microflows_builder_actions.go` (`addListOperationAction`) + `mdl/executor/cmd_microflows_builder_validate.go` (ListOperationStmt case) | **Write-path fix, two parts.** (1) Visitor: a literal/computed argument means the string function unambiguously → fall through to `MfSetStmt` (a Change Variable value expression); both-plain-variables stays a `ListOperationStmt` (ambiguous, kind unknown at parse). (2) Flow builder: when `declaredVars[InputVariable] == "String"`, emit a **Change Variable** action carrying `contains($In, $Second)` instead of a `ContainsOperation`; the validate pre-pass mirrors this (validate the target IS declared, don't flag CE0111 duplicate). Genuine list form (list var + object var) unchanged. Tests `TestContainsOverloadParsing` (visitor), `TestBuildContains_StringVsList` (builder); example `mdl-examples/bug-tests/ledger-53-string-contains.mdl`. **Verified: `mx check` → 0 errors on 11.12.1** for literal-arg, two-String-var, and genuine-list forms. Ledger finding #53 | -| `set $At = find($Raw, '"id":"')` — the STRING function `find(haystack, needle)` (substring index) — was serialized as a List operation activity 'Find by expression', so a pre-declared `$At` collided (**CE0111** "Duplicate variable name 'At'"). String `find()` was unusable. Same overload trap as #53 (contains) | `find` is overloaded — the LIST operation `find(list, condition)` (filter by a boolean condition) and the STRING function `find(haystack, needle)` → index — but the visitor unconditionally rewrote `set $x = find(...)` into a `ListOperationStmt` (list find), and a list op CREATES its output variable | `mdl/visitor/visitor_microflow_statements.go` (`buildSetStatement` FIND case + `isStringLiteralArg`) + `mdl/executor/cmd_microflows_builder_actions.go` (`addListOperationAction`) + `mdl/executor/cmd_microflows_builder_validate.go` (ListOperationStmt case) | **Write-path fix, mirrors #53.** (1) Visitor: a **string-literal** second argument (you never filter a list by a bare string literal) means the string function → fall through to `MfSetStmt`; a boolean condition or both-plain-variables stays a `ListOperationStmt`. (2) Flow builder + validate pre-pass: when `declaredVars[InputVariable] == "String"`, emit a **Change Variable** carrying `find($in, )` (arg1 is stored in the stmt's `Condition`) instead of a list FindOperation, and validate the target as a pre-declared SET (no CE0111). The contains and find String-input handling now share one branch. Tests `TestFindOverloadParsing` (visitor), `TestBuildContains_StringVsList` find cases (builder); example `mdl-examples/bug-tests/ledger-63-string-find.mdl`. **Verified: `mx check` → 0 errors on 11.12.1** for literal-arg, two-String-var, and genuine-list-condition forms. Ledger finding #63 | -| A datagrid **column** with an explicit empty caption (`Caption: ''`) passes `mxcli check` but `mx check` rejects the page with **CE0463** "The definition of this widget has changed" on the Data grid 2 (error points at the widget version, not the caption). Omitting the caption, or a non-empty string, both build clean | The pluggable widget engine's column-header fallback treated a **present-but-empty** header property as "has header" and skipped the attribute-name default — so `Caption: ''` emitted an empty header (which Studio Pro rejects) while an **absent** caption got the fallback. The keyword datagrid path already handled it (`if caption == "" { caption = col.Attribute }` in `datagrid_column.go`) | `mdl/executor/widget_engine.go` (`applyColumnHeaderFallback`) | **Write-path 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 (not appended — that would duplicate the `header` prop). A header WITH params (`Caption: '{1}'`) is left untouched. Result: `Caption: ''` now behaves like omitting it. **Round 2 (custom-content columns):** the first fix left a column with **no bound attribute** (an action/custom-content column) untouched — it had nothing to derive a header from, so an empty OR absent caption still tripped CE0463 (a custom-content column requires a non-empty header). Fixed by falling back to the **column's own name** when there's no attribute, and **gating the whole fallback on the item template having a `header` slot** (`mapping.ItemProperties`) so header-less object-list items (chart series, accordion groups) are never given a spurious header. `applyColumnHeaderFallback(spec, columnName, hasHeaderSlot)`. Test `TestApplyColumnHeaderFallback` (cases 1–8); examples `ledger-54-empty-column-caption.mdl` + custom-content verified via exec. **Verified: `mx check` → 0 errors on 11.12.1 for attribute columns AND custom-content columns (empty + absent caption)** (previously CE0463). Ledger finding #54 (custom-content columns) | -| Follow-up to round 2 (#25 re-test): the subscriber is registered but `runtime.log` **still** holds only the 4 JVM-banner lines — a probe microflow's `log info/warning/error` and a forced runtime exception produce **zero** lines. Isolation test: boot+subscriber → 0 probe lines; then call `start_logging` → 6 lines | A standalone runtime boots with logging **not started**, so a registered subscriber sits **inert** — nothing is delivered until the logging subsystem is activated. `create_log_subscriber` alone is necessary but not sufficient | `cmd/mxcli/docker/runtime_controller.go` (`configureRuntimeLogging` — renamed from `attachFileLogSubscriber`; now also calls `start_logging`) | After `create_log_subscriber`, call `CallM2EE(c.opts, "start_logging", nil)` in the same step (order: create subscriber → start_logging). Treat an "already started" response as success (`start` re-runs on the DB-update retry / restart paths on a still-running JVM). Still best-effort (warn to Stdout, never fail an up runtime). Test `TestStart_StartLoggingAlreadyStartedIsSuccess`; `TestStart_AttachesLogSubscriber` now asserts the `[start, create_log_subscriber, start_logging]` sequence. **Diagnosis pattern**: when a registered sink receives nothing, check whether the subsystem that feeds it is even *running* — registration ≠ activation. Findings #25 (round 3) | -| `mxcli new --version X` prints "Resolving MxBuild X..." and then produces a project at a **different** Mendix version — silently. Every later step (init, mxbuild, runtime, `run --local`) follows the wrong version | `ResolveMxForNewProject` delegated to `ResolveMxForVersion`, whose last resort is `AnyCachedMxPath()` — *any* cached mx, of any version. That fallback is fine when the project already exists and its version is a preference; for `new` the requested version **is** the output, because `mx create-project` stamps the project with the version of the binary that ran it | `cmd/mxcli/docker/check.go` (`localMxForVersion`, `ResolveMxForNewProject`) + `cmd/mxcli/cmd_new.go` (postcondition) | Resolve **exactly** the requested version for `new` (exact Studio Pro install → exact versioned install path → exact download cache; **not** PATH, which carries no version guarantee), and download otherwise. Then check the postcondition: reopen the created `.mpr`, compare `ProductVersion` to `--version`, and fail loudly on a mismatch — resolution bugs are invisible without it. **Generalisable**: when a flag names the version/identity of the artifact being produced, a "close enough" local substitute is never valid, and the produced artifact should be verified against the request rather than the resolution trusted. Found while reproducing #812 in a browser — cost a full project rebuild before it was noticed | -| A **DataView** property parses, passes `mxcli check`, and has no effect — `FormOrientation: Vertical` (#762) or `showFooter: true` (#813). `FormOrientation` works under `--engine legacy` | Two different causes that look identical. (a) `FormOrientation` has no BSON field: Studio Pro's radio **is** `LabelWidth` (0=Vertical, 3=Horizontal default). Only the legacy writer translated it; the modelsdk writer emitted `LabelWidth` solely when set explicitly, so the orientation was read into the model and dropped — the #812 shape, a model field no active-engine writer reads. (b) `ShowFooter` was only ever set implicitly by a `footer { … }` block; the property sat in the validator allow-list, so it parsed and was discarded | `sdk/pages/pages_widgets_data.go` (`ResolvedLabelWidth`), `mdl/backend/modelsdk/widget_write.go`, `sdk/mpr/writer_widgets_display.go`, `mdl/executor/cmd_pages_builder_v3_widgets.go` | Put the derivation **on the model** (`ResolvedLabelWidth`) so both writers share one definition instead of one owning it, and emit `LabelWidth` unconditionally. For the property, read it explicitly and let it win over the implicit block in both directions. **Trap**: `WidgetV3.GetBoolProp` is case-SENSITIVE and accepts only a real `bool`, unlike `GetStringProp` — so `showFooter: true` read as `false` even after the key was found. Coerce from the looked-up value and refuse a nonsense one instead of defaulting to false. Repro `mdl-examples/bug-tests/762-813-dataview-properties.mdl`. Issues #762, #813 | -| Every mxcli-authored page carries a container nobody asked for — a `Forms$DivContainer` named `conditionalVisibilityWidget` wrapping the page's top-level widgets. Creating a single button yields a button **and** a container | The builder wrapped each non-empty layout placeholder, because `pages.LayoutCallArgument` declared a **single** `Widget` field while the BSON `Forms$FormCallArgument` carries a **`Widgets` array**. The wrapper existed only to squeeze N widgets through a 1-widget field — never a BSON requirement | `sdk/pages/pages_parameters.go` (`LayoutCallArgument.Widgets`), `mdl/executor/cmd_pages_builder_v3.go`, `sdk/mpr/writer_pages.go`, `mdl/backend/modelsdk/page_write.go`, `mdl/backend/mcp/page.go` | Make the field a list and place widgets directly. **Check the claim against Mendix's own output before believing a comment**: ours said the wrapper is what "mxcli (and Studio Pro) adds", but `Administration.Account_Overview` in a `mx create-project` app has *two* top-level widgets in one placeholder and zero wrappers — same reasoned-by-analogy error as #812/#295. Corroborating signal that a construct is wrong: DESCRIBE already unwrapped it as a "phantom CONTAINER" and the catalog skipped it as "transparent" — three places working around something that should not be created. **Keep those readers**: projects authored before the fix still contain wrappers. Repro `mdl-examples/bug-tests/760-no-placeholder-wrapper.mdl`. Issue #760 | -| `CREATE CONFIGURATION` (or any `ALTER SETTINGS`) reports success and `mx check` passes, but Studio Pro throws `System.InvalidOperationException: Sequence contains no matching element` at `MprProperty.cs:25` when the changed unit is opened (e.g. from the version-control status grid). Silently, the same write also resets **HttpPortNumber/ServerPortNumber to 0** on every *existing* configuration | Three storage-name/enum defects in the settings write, all invisible to mxbuild (its deserializer tolerates unknown properties; Studio Pro resolves each stored property against the type's property list and throws when there is no match). (1) `createConfiguration` hardcoded `DatabaseType: "HSQLDB"` — the enum member is `Hsqldb`. (2) The gen `Configuration` binds the ports as `RuntimePortNumber`/`AdminPortNumber` (SDK names) while Studio Pro stores `HttpPortNumber`/`ServerPortNumber`, so the read returned 0 and the overlay wrote that 0 back. (3) Mendix renamed the runtime Java version property between 11.6 (`JavaVersion` = `"Java21"`) and 11.12 (`JavaMajorVersion` = `"21"`); mxcli wrote the 11.6 name unconditionally, leaving `JavaMajorVersion` stale and adding a property 11.12 does not define | `mdl/executor/cmd_settings.go` (`settingsDatabaseType`, `createConfiguration` defaults), `mdl/backend/modelsdk/settings_read.go` (`rawInt`, `javaVersionOf`), `mdl/settingsoverlay/settingsoverlay.go` (`JavaVersionKey`/`SetJavaVersion`, `newServerConfiguration`), `sdk/mpr/parser_settings.go` + `writer_settings.go` | Canonicalise enum-valued settings against `generated/metamodel` and reject the rest (executor **and** `mxcli check`, via a `settingsKind*` entry so the drift guard covers it). Read version-renamed properties off the stored document and write them back to the key they came from — **never invent a key the document does not already have** (the same reasoning removed the hardcoded `Tracing: nil` from the no-sibling fallback: 11.12 spells it `OpenTelemetry`). **Diagnose without Studio Pro**: dump the `Settings$ProjectSettings` unit before and after the command and diff key-by-key against the project `mx create-project` produced — the write must be purely additive. A "no matching element" *property* lookup means a key Mendix does not know; an enum member mismatch is a different exception. Repro: `create configuration 'X'` on an 11.12 project. Issue #759 | -| Any `ALTER SETTINGS` / `CREATE CONFIGURATION` corrupts a **private** constant override: the stored `Settings$PrivateValue` comes back carrying `"Value": ""`. Studio Pro then throws `System.InvalidOperationException: Sequence contains no matching element` at `MprProperty.cs:25` on open. `describe settings` separately renders the override as `value ''`, so replaying describe's own output converts it to a *shared* empty override | A constant override's value is either a `Settings$SharedValue` (carries `Value`, lives in the shared model) or a `Settings$PrivateValue` — a **marker type with no properties at all**, meaning the value is on the developer's workstation and deliberately out of version control. The overlay assumed SharedValue and wrote `cv.Value` (always `""` for a private override) into whichever node it found; the read type-asserted to `*SharedValue`, failed, and returned `""` with no way to distinguish private from empty | `mdl/settingsoverlay/settingsoverlay.go` (`constantValue`, `PrivateValueType`), `mdl/backend/modelsdk/settings_read.go` (`isPrivateConstantValue`), `sdk/mpr/parser_settings.go` (`parseConstantValue`), `mdl/executor/cmd_settings.go` (`describeSettings`, `alterSettingsConstant`) | Carry the distinction in the model (`model.ConstantValue.IsPrivate`) and **preserve, never author**: leave a PrivateValue node byte-identical, have `describe` emit a comment instead of a re-executable statement, and refuse an `alter settings constant` that would flip private→shared (drop is still allowed — it discards the whole override, which is what was asked). **Generalisable**: a polymorphic child whose variants differ in *arity* (one carries a value, one is a bare marker) cannot be overlaid by field assignment — branch on `$Type` first. Blast radius is wider than it looks: configurations are shared in version control, so one developer's unrelated edit corrupts every developer's private overrides and pushes the result. Found from a user describing their workflow, not from a filed issue | +## Finding a prior fix + +The findings live in `findings/*.jsonl`, one JSON object per line, sharded by +area. They are **data, not reading material**: 630 findings at ~1.7 KB each do +not fit in a context window, and the table they used to live in had grown past +the point where GitHub's web editor would open it. + +Grep is the fast path — the shard name narrows it, and every line is +self-contained: + +```bash +grep -il 'CE0463' .claude/skills/fix-issue/findings/*.jsonl # which areas +grep -h 'CE0463' .claude/skills/fix-issue/findings/mdl-executor.jsonl | jq . +``` + +For anything with a shape to it, the records have fields — `area`, `symptom`, +`cause`, `file`, `insight`, plus `refs` / `ce` / `rules` where the text carried +them — and DuckDB reads the files in place, no import step: + +```bash +duckdb -c "select symptom, file from '.claude/skills/fix-issue/findings/*.jsonl' + where list_contains(ce, 'CE0463')" +``` + +A finding whose original row could not be split into four columns keeps its +`raw` line instead; `symptom` and friends are then absent, so filter on `raw` as +well when a query must cover everything. + +`docs-wiki/bug-patterns/` is the layer above this: the *classes* of failure, +small enough to read. Start there, come here for the instance. + +## Recording a fix + +Append one line to the shard for the area you touched — a new shard is fine if +none fits. Keep it one line: `merge=union` in `.gitattributes` resolves two +concurrent appends by keeping both, which is correct for a file of independent +records and is not correct for prose. + +```bash +cat >> .claude/skills/fix-issue/findings/mdl-executor.jsonl <<'JSON' +{"area":"mdl/executor","date":"2026-08-31","symptom":"...","cause":"...","file":"...","insight":"...","refs":["#123"]} +JSON +make check-findings +``` + +`check-findings` prints one line saying how far `docs-wiki/bug-patterns/` has +fallen behind; `make digest-status` breaks it down by area. **If the class of +failure keeps recurring, sync its pattern page** (`/mxcli-dev:wiki-sync +bug-patterns/.md`). Nothing else will ask: the digest is on demand, and +every page in it was written on one day in May while the findings went on +accumulating — 607 of the 631 arrived afterwards. Neither number is a target to +drive to zero; they are there so the decision is made deliberately rather than +by default. + +Write the **insight**, not the changelog: what would have made this cheaper to +find, what measurement settled it, and which plausible-sounding wrong turn to +skip. Position carries no meaning — these are looked up by matching a symptom, +never read in order. + --- @@ -343,57 +160,6 @@ cases for these three BSON types — they fell to `default: return nil`. | A `create json structure … snippet` produces a structure whose **import mappings silently import zero objects** — the REST call succeeds, the mapping looks right, and no data arrives. No validation error, `mx check` passes. Dumping the stored BSON shows every element at `MinOccurs=0, MaxOccurs=0` | Mendix reads `MaxOccurs=0` **literally as "never occurs"**, not as "unspecified". The snippet→element builder hardcoded `0/0` at all nine construction sites; `cmd_import_mappings.go` then copies MinOccurs/MaxOccurs straight onto the mapping elements, so the dead bound propagates from the structure into every mapping bound to it | `mdl/types/json_utils.go` (`BuildJsonElementsFromSnippet` + `buildElementFromRawObject` / `buildElementFromRawRootArray` / `buildElementFromRawArray` / `buildValueElement`) | Derive occurrences from the JSON shape: root `1..1`, Object/Value `0..1`, Array `0..1` with its **item** child `0..*` (`MaxOccurs = -1`), primitive-array Wrapper `0..*`. Added the named constant `occursUnbounded` so `-1` is not a bare literal. **Generalisable — the shape to look for**: when a tree comes out uniformly wrong except for *one* node, that node proves the writer is capable of the correct value and localises the bug to the construction sites that hardcode it — here the nested object-array item was already `0..-1` while the root-array item beside it was `0..0`, i.e. the same construct written two ways in two builders. **Trap**: `0` looks like a harmless default for a numeric field, so this reads as correct in review and survives every checker; only a BSON dump or a runtime import reveals it. **Follow-up that the first cut missed**: Mendix cross-validates every *mapping* element's occurrence against its bound *schema* element and reports **CE5015** ("Attribute 'MaxOccurs' does not match schema element") on a mismatch. Import writers already propagated occurrences; all **three** export writers (`modelsdk/mpr/serialize_mappings.go`, `sdk/mpr/writer_export_mapping.go`, `mdl/backend/modelsdk/mapping_write.go` — the last is the one the default engine actually uses) hardcoded `MaxOccurs: 0` on value elements, so raising the schema broke every export mapping. **Generalisable — the shape to look for**: changing a value that another document is validated *against* is never a one-sided edit; grep every writer that emits the same key, and expect more than one engine to have its own copy. **Verification trap that caused the miss**: the repro created JSON structures but no mapping bound to one, so it passed `mx check` while the integration suite went red — when a fix changes a field that other documents reference, the repro must instantiate a *referencing* document, not just the changed one. Repro `mdl-examples/bug-tests/841-json-structure-occurrences.mdl` (now includes an import+export mapping); verified end-to-end (`mx check` 11.13.0 0 errors on the repro and on both doctype scripts that failed CI). Issue #841 | | A workflow **`DECISION`** whose expression uses the documented lowercase `$workflowContext` fails `mx check` with **`[error] [CE0117] "Error(s) in expression." at Decision 'Decision'`**, while the *same spelling* in a `CALL MICROFLOW … WITH` clause works. `mxcli check` and `mxcli exec` both report success | The context parameter is named `WorkflowContext` and Mendix expressions are case-sensitive on 11.9+, so `$workflowContext` is an undefined variable. `normalizeWorkflowContextExpr` existed and was well-tested, but was only *applied* in `autoBindCallMicroflow` (the FINDINGS #39 fix) — `buildExclusiveSplit` stored `n.Expression` verbatim. The working WITH clause is what disguised it: the user reasonably concludes the spelling is fine | `mdl/executor/cmd_workflows_write.go` (`buildExclusiveSplit`, and the sibling `buildWaitForTimer` whose delay may reference a context date attribute) | Run the authored expression through the existing `normalizeWorkflowContextExpr` at every site that accepts a user expression — there are three in the workflow writer, and only the parameter-mapping one was covered. **Generalisable — the shape to look for**: a *normalizer that exists and is unit-tested* is not evidence it is *called*; grep the call sites, not the helper. When one input spelling works and an identical one fails, compare the two code paths before questioning the data. **Also fix the docs that teach the broken form** — `.claude/skills/mendix/write-workflows.md` showed lowercase in its DECISION example and is synced into user projects by `mxcli init` via `cmd/mxcli/skills/`, so the bug propagated to every generated project. Repro `mdl-examples/bug-tests/845-workflow-decision-context-casing.mdl`; verified end-to-end (`mx check` 11.13.0: 1 error → 0). Issue #845 | -| `mxcli check … --references` reports **"All references valid" / "Check passed!"** for a script whose GRANT names a module role from a different module than the document; `exec` then fails with **CE0148** — after the preceding statements have already been applied, leaving the project half-modified | The guard (`checkDocumentAccessRolesSameModule`) existed and was wired into all five exec paths, but **no validate path ever called it**. mxcli does not run a script in a single transaction, so a failure that only surfaces at exec time is exactly what a pre-flight check exists to prevent | `mdl/executor/validate_grant_roles.go` (`ValidateGrantRoles`, MDL-GRANT01), `mdl/executor/cmd_security_defaults.go` (`validateCrossModuleGrant`), wired in `cmd/mxcli/cmd_check.go` | Reuse the existing exec-time guard from the **no-project** violations pass, covering all five document-access grants (microflow, nanoflow, page, OData service, published REST service). Take the document's module from the **statement's own qualified name**, not the resolved document — same comparison, and it works before the document exists (it is often created earlier in the same script). **Put it in the no-project pass, not under `--references`**: the check compares two names already in the script, so requiring `-p` withholds an answer mxcli can always give, and a plain `mxcli check` now catches it. **Generalisable — the shape to look for**: when exec rejects something a checker accepts, the bug is usually not a missing rule but a rule wired into only one of the two paths — grep the guard's callers before writing a new one. Repro `mdl-examples/bug-tests/836-check-cross-module-grant.fail.mdl`; verified end-to-end (check exits 1 naming the statement and CE0148, project left untouched; same-module variant still passes and gives 0 errors under mx check). Issue #836 | - -| `DESCRIBE MICROFLOW` emits **`on error rollback`** on activities authored with no error-handling clause at all, growing the diff on every round-trip. No checker flags it — `"Rollback"` is structurally valid, so `mx check` and every mxcli validator pass | `Rollback` is what `convertErrorHandlingType(nil)` stores for an activity with no clause **and** what the parser falls back to when `ErrorHandlingType` is absent from the BSON. The stored value therefore cannot distinguish an authored clause from the default, and read-back guessed "authored" | `mdl/executor/cmd_microflows_show_helpers.go` (`formatErrorHandlingSuffix`) | Drop the `Rollback` case so it falls through to no suffix. **The asymmetry is the whole argument**: omitting it is lossless (re-executing stores `Rollback` again, so the model is unchanged), while emitting it is lossy in the direction that matters — it puts a clause in the user's script that they never wrote. `Continue` / `Custom` / `CustomWithoutRollback` are never defaults, so they still round-trip. **Generalisable — the shape to look for**: when a formatter renders an enum whose zero/fallback value is also a legal authored value, read-back cannot invert the write; render only the values that are *never* defaults. Ask "what does the parser fall back to?" before trusting a stored enum to mean the author chose it. Repro `mdl-examples/bug-tests/840-describe-invents-on-error-rollback.mdl`; verified end-to-end (describe → exec → describe byte-identical, `mx check` 11.13.0 0 errors). Issue #840 | - - -| `mxcli check` **segfaults** (`SIGSEGV` in `visitor.(*Builder).ExitSqlDisconnect`) on the one-line script `SQL DISCONNECT source;` — which is the exact line in mxcli's own `mxcli syntax sql` example. Other aliases (`mydb`, `src`) are fine | Two defects. **Grammar**: every `sqlStatement` alternative took a bare `IDENTIFIER` for the alias/driver/table, but `source` lexes as `SOURCE_KW` (`MDLLexer.g4:317`), so no alternative matched. `IMPORT FROM identifierOrKeyword` in the same file already accepted a keyword alias, so `import from source …` worked while `sql source …` did not. **Robustness**: ANTLR error-recovers and still walks the tree, so the listener received a context whose `IDENTIFIER()` was nil and `.GetText()` crashed the process — sibling handlers guarded their children (`if len(ids) < 2 { return }`), this one did not | `mdl/grammar/domains/MDLSettings.g4` (`sqlStatement` → `identifierOrKeyword`), `mdl/visitor/visitor_sql.go` (`sqlWords` helper + guards at all five handlers) | Use `identifierOrKeyword` for every user-chosen word, and read children through a helper that tolerates a missing node. **Generalisable — the shape to look for**: a listener that dereferences `ctx.X()` without a nil check is a crash waiting for the first input that fails to parse *that rule* — ANTLR does not stop the walk on a syntax error. Grep for `ctx\.[A-Z][A-Za-z]*()\.GetText()` and treat each as unguarded. **Second shape**: when a bare `IDENTIFIER` names something a user picks (alias, table, column, prefix), it silently forbids every keyword; `identifierOrKeyword` is nearly always what was meant, and the inconsistency shows up as "works in one statement, not the neighbouring one". **Watch the index shift** when converting: the alias joins the `AllIdentifierOrKeyword()` list, so downstream `[0]`/`[1:]` offsets move (this bit `ExitSqlGenerateConnector`, where getting it wrong would generate a connector into a module named after the connection). **How it was found — and the trap it hid**: `TestExamplesParse` (new) feeds every `mxcli syntax` example through the parser; the panic **aborted the test binary**, masking ten further failures in entries sorted alphabetically after `sql`. A crash in a table-driven test is not one failure, it is an unknown number. Repro `mdl-examples/bug-tests/sql-keyword-alias-crash.mdl` | - -| `mxcli syntax ` teaches MDL that does not parse — `Binds:` (removed in favour of `Attribute:`, and hard-rejected by the parser), workflow decision outcomes without the `->` arrow, `ALTER WORKFLOW … SET DUE DATE = '…'` (no `=`), `INSERT AFTER ` (operands reversed), `IMAGE 'name'` (identifier, not string), a `BEFORE` that does not exist. Nothing failed, because nothing checked | The registry (`cmd/mxcli/syntax/features_*.go`) is hand-maintained while the grammar moves underneath it, and every existing test checked *structure* — fields populated, aliases resolve, see-also targets exist — never whether the documented MDL is real. 26 of 120 entries had a non-parsing example | `cmd/mxcli/syntax/example_parses_test.go` (new guard), plus corrections across `features_page.go`, `features_workflow.go`, `features_integration.go`, `features_microflow.go`, `features_misc.go` | Parse every `Example` and fail the build if it does not. Examples come in several legitimate shapes, so each blank-line-separated **block** is tried as a statement, a microflow activity, a page widget, a workflow activity, and a retrieve clause — a failure means it parses as none of them. Only `Example` is checked; `Syntax` carries metasyntax (`[OR MODIFY]`, ``) by design. **Generalisable — the shape to look for**: documentation that is *data in the binary* can be executed against the real parser, which turns "the docs drifted" from a review problem into a test failure. The same trick applies to any embedded example corpus (skills, `--help` text, README snippets extracted at build time). **Why it matters more than it looks**: the registry is the first surface an agent consults, so a gap there does not read as "undocumented", it reads as "unsupported" — a contact-management app built with mxcli worked around three features that already existed, including replacing a `SAVE_CHANGES CLOSE_PAGE` button with a bespoke microflow because only the two halves were listed separately. **Close the opt-out** — a guard that skips empty input is a guard you can silence by emptying the field; `TestFeatureFieldsPopulated` already rejects a blank `Example`, and the new guard additionally fails an example that is all comments | -| SCSS written to **`themesource//web/main.scss` never reaches the app** — no error, no warning, the build succeeds and the rules are simply absent from `theme-cache/web/theme.compiled.css`. Looks exactly like an SCSS cache problem, so the usual reflex (`rm -rf theme-cache/`) wastes the session | A theme source folder is only compiled when `` matches a **real module in the model**. mxbuild walks the model's modules and pulls each one's `themesource//web/main.scss`; it never globs the `themesource/` directory, so an invented folder (`themesource/my_theme/`) is silently skipped. Verified on 11.13: a probe rule in `themesource/myfirstmodule/` compiled, the identical rule in `themesource/mxcli_theme/` did not | `cmd/mxcli/theme/theme.go` (package doc records the compile order); the target paths live in `cmd/mxcli/theme/assets//files/` | Put app-level styling in **`theme/web/`**, not in an invented theme source folder: `theme/web/main.scss` is compiled **last** — after Atlas Core *and* after every module theme source — so a partial imported from it overrides any Atlas rule without `!important`. Use a module's theme source only when the styling genuinely belongs to that module (it exports with the `.mpk`). **Generalisable — the shape to look for**: when CSS "doesn't apply", first prove the file is *compiled at all* (grep a unique probe selector in `theme-cache/web/theme.compiled.css`) before debugging specificity or caches — absent and overridden look identical in the browser. Note also that `theme/web/custom-variables.scss` is imported once **per module**, so it must hold declarations only; a rule there is emitted N times | - -| A **`CREATE JAVASCRIPT ACTION`** succeeds, `mxcli check` passes and the build is clean, but calling the action in the running app throws **`JavaScript action was not implemented`** and the nanoflow aborts | mxcli wrote the source to `javascriptsource//actions/` using the module's own casing. Mendix reads a **lowercased** directory — a blank Mendix 11 app ships `javascriptsource/nanoflowcommons/`, `/datawidgets/`, `/webactions/` for modules named `NanoflowCommons`, `DataWidgets`, `WebActions`. Finding no source at the path it reads, mxbuild generates a stub whose body is `throw new Error("JavaScript action was not implemented")` and bundles that. Only reproduces on a **case-sensitive filesystem**, which is why it survived: on macOS and Windows the two spellings are the same directory | `mdl/backend/modelsdk/javascript_write.go` and `sdk/mpr/writer_javascriptactions.go` (`jsActionSourceDir`) | `strings.ToLower(moduleName)` in both writers — the comment in each previously asserted the opposite ("unlike javasource, which is lowercased"), so the belief was documented, not tested. **Generalisable — the shape to look for**: when generated *source files* pair with model units, the model unit is not evidence the file is found; the filesystem path is a separate contract, and a case-only mismatch is invisible on the developer's own machine. Check against the directories a blank project already ships rather than against what the code says. Nothing short of running the app catches it: parse, check and build all pass. Test `mdl/backend/modelsdk/javascript_write_dir_test.go`; verified end-to-end (button click flips the theme instead of throwing) | -| A `create rest client` operation reports success, but `describe rest client` omits `Query:`, `Parameters:` and `Headers:` and always prints `Response: none`. BSON shows the query parameters/headers stored **correctly** while `ResponseHandling` is `Rest$NoResponseHandling` — the response mapping is gone. `mx check` passes (0 errors), so nothing anywhere complains | **Two unrelated defects with one symptom.** *Write*: `model.RestClientOperation` documents `BodyType`/`ResponseType` as UPPER-case tokens and every consumer compares against that spelling, but the MDL executor stored the visitor's lower-case source text — so `op.ResponseType == "MAPPING"` never matched and the mapping fell through to the else-branch, which legitimately writes `NoResponseHandling`. *Read*: `restOperationFromGen` populated only Name/HttpMethod/Path/Timeout and type-asserted `*genRest.RestParameter` for **both** parameter lists, while the writer emits `Rest$OperationParameter` and `Rest$QueryParameter` — two different gen types, so both assertions failed silently | `mdl/executor/cmd_rest_clients.go` (`buildRestClientOperation` normalization + `checkInlineMappingBody`), `mdl/backend/modelsdk/integration_read.go` (`restOperationFromGen` + response/body/mapping-tree readers), `mdl/backend/modelsdk/consumed_rest_write.go` and `modelsdk/mpr/serialize_web_services.go` (`EqualFold`), `mdl/executor/validate_rest_mapping.go` (MDL-REST01) | Normalize with `strings.ToUpper` at the one place the AST becomes the semantic model, and make the two serializer comparisons `EqualFold` so the landmine is not left armed for the next producer. **Generalisable — the shape to look for**: a case-sensitive comparison against a *documented-but-unenforced* string constant, where the non-matching branch is a **legitimate** outcome. Nothing errors, because "no response handling" is a real thing an operation can have — the else-branch launders a producer/consumer mismatch into a plausible-looking model. Grep every comparison against the constant, expect one per engine, and check whether the false branch is silent. **Second shape**: a reader that type-asserts one concrete type for two lists the writer builds from two *different* types; the assertion fails to `ok=false` and `continue`s, so a stub reader is indistinguishable from an empty document. The pre-existing round-trip test even created a query parameter — but only asserted the operation *count*, never that the parameter survived. **Third, separate half (#843's headline)**: `Response: mapping Mod.IMM_X` names an import mapping **document**, which Mendix cannot reference — `Rest$RestOperationResponseHandling` has exactly two implementations, inline and none. The clause parsed, contributed no entries, and was written as "none". Now refused at exec *and* `mxcli check` (MDL-REST01, no project needed). **Note** `Rest$QueryParameter` stores no DataType at all, so the MDL type is decorative and `describe` re-emits every query parameter as `String` — do not "fix" that by inventing the authored type back (see the #840 row). Repros `mdl-examples/bug-tests/843-rest-response-mapping.mdl` + `843-rest-response-mapping-no-body.fail.mdl`; verified end-to-end (`mx check` 11.13.0 0 errors, BSON now `Rest$ImplicitMappingResponseHandling` with a full `ImportMappings$ObjectMappingElement` tree, describe → exec → describe byte-identical). Issue #843 | -| A widget datasource bound to a **parameterized** microflow/nanoflow (`datasource: microflow Mod.MF(Name: $x)`) fails with **CE1571** "No argument has been selected for parameter 'X'". `mxcli check` and `exec` both report success; `describe page` shows the microflow but no arguments | The grammar parsed the arguments into `DataSourceV3.Args`, but the builder never read them and `pages.MicroflowSource` had **no field to hold them**, so they were dropped between AST and model. The writer's `microflowSettingsToGen(d.Microflow, nil)` passed a literal `nil` at all three datasource sites, with a comment asserting datasources never carry mappings — true when only actions could take arguments, stale once the grammar accepted them on a datasource | `sdk/pages/pages_datasources.go` (`MicroflowSource`/`NanoflowSource` gain `ParameterMappings`), `mdl/executor/cmd_pages_builder_v3.go` (`flowArgsToParameterMappings`, shared with the action path), `mdl/backend/modelsdk/widget_write.go` (three sites pass `d.ParameterMappings`) | Reuse the action path's `$`-variable-vs-expression rule rather than writing a second one — the two must agree or the same argument binds through different BSON fields depending on where it appears. **Generalisable — the shape to look for**: a hardcoded `nil` argument whose comment explains *why* the data can't exist is a dated assumption; when the grammar grows a construct, grep for the `nil`s that were correct before it. **Also check the model type can hold the value at all** — here the AST parsed it and the writer would have written it, but the struct in between had no field, so nothing was "dropped" by any single line. **Remaining gap**: `describe page` does not yet emit the mappings, so a describe → exec round-trip still loses them (read-back only; the write path is correct). Repro `mdl-examples/bug-tests/835-datagrid-microflow-datasource-params.mdl`; verified end-to-end (`mx check` 11.13.0: CE1571 → 0 errors). Issue #835 | -| Quoted identifiers inside a `create import mapping { }` / `create export mapping { }` body produce **CE1613** "The selected entity `'Mod."Entity"'` no longer exists" on every reference, while the same quoting works everywhere else. `mxcli exec` reports success with no warning | The mapping-body builders read entity/association names with `ctx.QualifiedName().GetText()`, which returns **raw parse text including the quotes**, instead of `buildQualifiedName` (which strips them via `identifierOrKeywordText`, as the rest of the visitor does) | `mdl/visitor/visitor_import_export_mapping.go` (`buildImportRootElement`, `buildImportChild`, `buildExportRootElement`, `buildExportChild` — five sites: root entity, nested association + entity, value-transform converter) | Route every qualified name through `buildQualifiedName(...).String()`. **The error message names the bug**: `'Mod."Entity".RouteId'` mixes a *quoted* entity with an *unquoted* attribute, because the attribute half already went through `identifierOrKeywordText` — when a stored name is half-stripped, one of the two readers is raw. **Generalisable — the shape to look for**: `GetText()` on a parser context is almost always wrong for a name; it is the raw source slice, so any lexical decoration (quotes, whitespace) survives into the model. Grep for `.GetText()` next to `QualifiedName` whenever a reference looks right in the script but not in the .mpr. Repro `mdl-examples/bug-tests/842-mapping-quoted-identifiers.mdl`; verified end-to-end (`mx check` 11.13.0: 3 errors → 0). Issue #842 | -| `ALTER PAGE … ON ` targeting a DataGrid2 column: (a) the authored MDL column name (`column colFoo`) fails with a bare `widget "colFoo" not found`, and (b) a name shared by two columns (duplicate captions) **silently mutates the first** and reports `Altered page`, leaving the second unreachable | DataGrid2 columns carry **no stored name** in the Mendix model (their WidgetObject has no `Name` property), so the authored name is dropped on write and mxcli addresses a column by a *derived* name (attribute leaf, else caption, else `col{N}`). The bare-name resolver `findInWidgetArray` returned on the **first** derived-name match, so duplicates collided silently; the miss path emitted a generic "widget not found" with no hint that columns use a derived name | `mdl/backend/pagemutator/mutator.go` — the column loop in `findInWidgetArray` (now counts matches → `bsonWidgetResult.matchCount`), `findBsonColumn` (returns `(result, error)`), and the mutation entry points `SetWidgetProperty`/`DropWidget`/`InsertWidget`/`ReplaceWidget` (reject `matchCount > 1`, use `widgetNotFoundError` which lists derived names) | Persisting the authored name is **not possible** — columns have no name slot, and inventing one is the Studio-Pro-won't-open hazard (ADR-0005 guard-don't-drop). So implement the finding's two fallbacks: reject an ambiguous `ON ` with an actionable error instead of silently taking the first (a *data hazard*, not just a wart), and on a miss list the addressable derived names + explain the derived-name model (run DESCRIBE PAGE). **Generalisable**: a resolver that returns the first of N matches hides ambiguity — count matches and reject >1 at every *mutating* entry point, not just the read path. Repro `mdl-examples/bug-tests/ledger-78-datagrid-column-addressing.mdl`; A/B: pre-fix binary reports `Altered page` for the duplicate-caption `ON "Amount"`, fixed errors with "ambiguous". Ledger #78 | -| `loop { if then break; }` where the `if` is the **last** statement builds a Decision with only its `true` outgoing flow (→ break). `mxcli check` passes but `mx check` reports **CE0079** "the 'false' condition value should be configured in properties for an outgoing sequence flow", and the microflow won't deploy. (Distinct from the earlier #791 crash — that was a dropped Break/Continue *event*; this is a missing *flow*.) `continue` and break-not-last behaved likewise | The loop-body flow builder (`addLoopStatement`) is a simplified copy of `buildFlowGraph` that connected body statements with a plain `newHorizontalFlow` and **never honoured the deferred `nextFlowCase`** a merge-less split leaves for its FALSE branch. So the split's false case was dropped: mid-body it wired the next statement with no case; as the last statement it wired nothing at all | `mdl/executor/cmd_microflows_builder_control.go` (`addLoopStatement` body loop) | Mirror `buildFlowGraph`: track `pendingCase` between body statements and apply it to the connecting flow; then, for a leftover `pendingCase` at the end of the loop body (a decision whose non-terminal branch falls off the end), synthesize a **ContinueEvent** and wire the split's false flow to it — the valid Mendix representation of "didn't break/return → next iteration". **Trap**: the check-time acceptance test (`TestValidateMicroflow_ConditionalBreakAccepted`) only asserted MDL051 doesn't fire — it never ran `mx check` on the *output*, so the CE0079 microflow shipped green. Assert the produced BSON, not just that check accepts the source. Repro `mdl-examples/bug-tests/ledger-52-break-in-conditional.mdl`; verified raw `mx check` 0 errors (was 1× CE0079) and the split now carries both a true→Break and a false→Continue flow. Ledger #52 | - -| A workflow containing a standalone `annotation '...'` writes a project Mendix **cannot load**: `System.InvalidOperationException: Type ...Workflows.Model.Annotation does not contain a constructor with a parameter of type ...Workflows.Model.Flow`. Not a build error — Studio Pro will not open the project and `mx check` dies before validating anything. `mxcli check` passed and `exec` succeeded | mxcli writes the annotation into the workflow's **activity flow**. Mendix loads that list by constructing every child with a `Flow` parent, and no annotation type takes one: `Workflows$Annotation` carries only `Description` (it attaches to a Flow) and `Workflows$FloatingAnnotation` (the canvas sticky note, which has exactly the `RelativeMiddlePoint`/`Size` fields mxcli was already writing) is not a flow element either. **Placement is the defect, not the storage name** — swapping the `$Type` to FloatingAnnotation reproduces the identical error with the new type name | `mdl/executor/validate_workflow.go` (new `MDL-WF04`), `mdl/executor/cmd_workflows_write.go` (`execCreateWorkflow` guard + `hasStandaloneWorkflowAnnotation`), skill `.claude/skills/mendix/write-workflows.md` (which had documented the construct) | **Refuse rather than emit an unopenable unit** — at check time *and* at exec time, because a user who skips `check` otherwise still loses the whole project. The correct container is not determinable from the gen model (no struct owns a `FloatingAnnotation` list) and CLAUDE.md's rule applies: when the BSON shape is unknown, get a Studio Pro reference rather than guess. **Generalisable**: verify a storage-name hypothesis by *swapping only the name* — if the error is byte-identical with the new type, the bug is where the element is attached, not what it is called. Repro `mdl-examples/bug-tests/it-15-workflow-annotation-refused.fail.mdl`; A/B: pre-fix binary writes it and `mx check` cannot load the project, fixed binary refuses and the project checks 0 errors. issuetracker #15 **Follow-up (CI):** three `-tags integration` round-trip tests asserted this construct *works* and went red on the guard. They exercised mxcli's own write → read → describe → re-execute loop, which a structurally invalid document survives — the loop never loaded the project in Mendix, so it proved nothing about validity. Re-settled by stubbing the guard and running real `mx check`: the project fails at "Loading the mpr file". The tests were pinning the defect, and now assert the refusal (`TestCreateWorkflow_StandaloneAnnotationRefused`). **Second instance in one PR** of a green test codifying a bug (see the `jump to` row) — when a pre-existing test contradicts a new guard, re-derive the ground truth from the layer the symptom lives in before believing either. | - - -| A page datasource navigating an association writes `DestinationEntity: ""`, and the project becomes **unloadable**: `An error occurred when trying to set the 'DestinationEntity' property of a Entity ref step ... ---> System.ArgumentNullException` at `EntityRefStep.set_DestinationEntityId`. Studio Pro will not open it and `mx check` dies before validating anything. `mxcli check` and `exec` both succeed | `resolveAssociationDestination` resolves both ends via `entityQNByID`, which only sees the **project's own** domain models — an association ending in a **System** entity (`from W.Issue to System.Workflow`) yields `""` for that side. The context then matched neither end, and the fallback `return childEntity` returned the empty one. An empty by-name reference is not "absent", it is a reference Mendix resolves to null | `mdl/executor/cmd_pages_builder_v3.go` (`resolveAssociationDestination` fallbacks + a hard guard in the `association` case of `buildDataSourceV3`) | Prefer whichever end actually resolved and is not the context; then **refuse** an unresolved destination rather than write it, pointing at the explicit `Assoc/Module.Entity` form (verified to build 0 errors — it is the construct the reporter had abandoned). **Narrower than reported**: the finding blamed *nesting*, but a one-step probe with the same association reproduces it identically — nesting was incidental. Always re-derive the trigger with the smallest case before fixing the reported shape. **Generalisable**: a resolver that returns `""` on failure will silently produce a null by-name reference; make the write path refuse empty rather than trusting the resolver. Repro `mdl-examples/bug-tests/it-14-assoc-destination-entity.mdl`; A/B: pre-fix binary leaves the project unopenable, fixed binary refuses and the project checks 0 errors. issuetracker #14 | -| `describe workflow` renders a plain `jump to Review;` as `jump to Review comment 'Review';` — a comment clause the author never wrote, which then round-trips back into the model as a real caption | `buildJumpTo` defaults the activity's `Caption` to the **target name**, and the DESCRIBE emitter echoed `Caption` unconditionally (falling back to the activity `Name` when empty). Both are derived values carrying no authored information | `mdl/executor/cmd_workflows.go` (`JumpToActivity` case in `formatWorkflowActivities`) | Emit `comment '...'` only when the caption is genuinely authored — non-empty **and** different from both the target name and the activity name. **Watch for tests that codify the bug**: `TestFormatJumpTo_CaptionCommentFormat` had a "name fallback when caption empty" case asserting the phantom comment, and two issue-619 quoting tests were incidentally coupled to it; a green suite was pinning the defect in place. Tests `mdl/executor/cmd_workflows_describe_test.go`, `mdl/executor/issue619_emitter_quoting_test.go`. issuetracker #16 | -| A workflow `decision ''` referencing the context passes `mxcli check`, executes, then the build fails `[error] [CE0117] "Error(s) in expression." at Decision 'Decision'`. `$WorkflowContext/X` (exact casing) works; `$workflowContext/X` — the spelling this repo's own skill documented — and `$Ctx/X` from the author's `parameter $Ctx:` header both fail | mxcli always stores the context parameter as `WorkflowContext` and Mendix expressions are **case-sensitive**. `normalizeWorkflowContextExpr` already existed but was wired into `autoBindCallMicroflow` only, so `with (...)` mappings were normalized while a decision's condition was written through verbatim. Separately, the header's declared variable name was parsed into `ast.CreateWorkflowStmt.ParameterVar` and then **never consumed** — a field populated but read nowhere, so `$Ctx` resolved to nothing | `mdl/executor/cmd_workflows_write.go` (`contextExprNormalizer` + threading it through `autoBindActivitiesInFlow`), `mdl/executor/cmd_alter_workflow.go`, skill `.claude/skills/mendix/write-workflows.md` (whose examples were the failing spelling) | One normalizer applied to **every** expression an author can write in a workflow — decision conditions, user task due dates and XPath targeting, wait-for-timer delays, call-microflow mappings — rather than a second point fix. The declared name is aliased onto the stored one (whole-word, so `$CtxItem` is not mangled) instead of documenting it as meaningless. **Generalisable**: grep for an AST field that is written by the visitor and read nowhere — that is a silently-discarded user intent, not dead code. Repro `mdl-examples/bug-tests/it-17-workflow-context-expression.mdl`; A/B: pre-fix binary writes it and `mx check` reports CE0117, fixed binary checks 0 errors. issuetracker #17 | -| A page widget bound through an association — `Attribute: Issue_Assignee/Name` — passes `mxcli check`, executes, then fails `[error] [CE1613] "The selected attribute 'IT.Issue.Issue_Assignee/Name' no longer exists."`. The error text is the raw MDL path glued onto the context entity. Same-module paths (`Issue_Project/Code`) work | A domain model keeps associations in **two** lists. `Associations` holds intra-module ones (both ends BY_ID); an association targeting another module is a `DomainModels$CrossAssociation` in **`CrossAssociations`**, where only the local end is BY_ID and the remote end is the BY_NAME `ChildRef`. `associationEndpoints` searched only the first list, so every cross-module hop returned ok=false and the writer fell back to a flat attribute path instead of an `AttributeRef` with an `IndirectEntityRef` of steps | `mdl/executor/cmd_pages_builder_v3.go` (`associationEndpoints`, `resolveAssociationDestination`), `mdl/executor/widget_engine.go` + `cmd_pages_builder_input.go` (`resolveAssociationPathIn`, `storedSystemMemberName`) | **Scope correction — the reported trigger was wrong.** The finding blamed the *System module*; a plain second app module reproduces it identically, so the trigger is cross-module. Fixing "System" alone would have left the commoner case broken — always re-derive the trigger with a neutral variant before fixing the reported one. Two sibling defects in the same finding: (a) a ComboBox's `Association:` was qualified with the module of its **own option list**, because the `DataSource:` mapping runs first and moves `pageBuilder.entityContext` — an association belongs to the *containing* entity, so it now resolves against the context saved at `Build` entry (`outerEntityContext`); (b) `CreatedDate: AutoCreatedDate` is the spelling mxcli **requires** when declaring an audit member, but the member is stored as `createdDate`, so binding the name you just declared failed — `storedSystemMemberName` now maps declared→stored. **Generalisable**: when a resolver reads one collection off a model object, check whether the model splits that concept across two (intra- vs cross-module, own vs inherited). Repro `mdl-examples/bug-tests/it-19-cross-module-attribute-path.mdl`; A/B on Mendix 11.12.1: pre-fix binary → 4 × CE1613, fixed binary → 0 errors. issuetracker #19 | -| GRANT rejects members Mendix does recognise — `entity M.Label has no member(s) Issue_Label` for the non-owning end of an `OWNER Both` reference set, and `has no member(s) createdDate, changedDate` for audit members — and a `read * / write *` rule that looks complete still fails `[error] [CE0066] "Entity access is out of date."`, so partial coverage is worse than none | Two unrelated gaps in "what counts as a member". (a) `OWNER Both` makes an association a member of **both** ends, but the writer emitted the MemberAccess only for the FROM entity (`ParentID`) **and** `ReconcileMemberAccesses` independently applied the same FROM-only rule — so it stripped the entry back out on the next write even if the executor had added it. Two places had to agree. (b) Audit members are entity **flags** (`HasCreatedDate`/`HasChangedDate`), not entries in `entity.Attributes`, so the member walk never yielded them | `mdl/executor/cmd_security_write.go` (`execGrantEntityAccess`, `storedAuditMembers`, `otherModuleBothOwnerAssociations`), `mdl/backend/modelsdk/domainmodel_security_write.go` (`ReconcileMemberAccesses`) | **Ask mxbuild what it wants instead of inferring symmetry.** Emitting a MemberAccess for `createdDate` seemed like the obvious fix for (b) — mxbuild **rejects** it with CE0066, and an entity storing audit members checks clean with no entry. So audit members are accepted as names but per-member rights on them are **refused with the reason** rather than silently dropped; only the `OWNER Both` association actually needed a new entry. **When a symptom has two spellings (a rejection and a build error), check whether they are one bug or two** — here they were two, and fixing them the same way would have introduced a new CE0066. **Generalisable**: a writer and a reconciler that both compute "the expected member set" are one invariant in two places; changing one alone is silently undone. Repro `mdl-examples/bug-tests/it-20-grant-member-coverage.mdl` (+ `it-20-grant-audit-member-rights.fail.mdl`); A/B on Mendix 11.12.1, same module same project: pre-fix binary → CE0066 + the bogus rejection, fixed binary → 0 errors. Controlled: the identical model with `OWNER Default` checks clean pre-fix, so the owner mode is the trigger. issuetracker #20 | -| An unquoted negative number in an XPath constraint fails to parse: `where [Amount > -7]` → `Parse error: extraneous input '7' expecting {',', ')'}`. Reported as "negative numeric literals truncate (`-7` becomes `-`)" | `xpathWord` — the name-part rule inside XPath — is a **negated token set** that did not exclude `MINUS`, so the sign was consumed as a name word and the digits were left stranded (hence the truncation appearance). The lexer deliberately keeps `-` out of `NUMBER_LITERAL` (a leading sign there mis-tokenises `$x -2`), leaving negation to the parser; the general grammar has `unaryExpression` for this and the XPath grammar simply never got the equivalent | `mdl/grammar/domains/MDLPage.g4` (`xpathValueExpr` gains `MINUS xpathValueExpr`; `MINUS` added to the `xpathWord` exclusion set), `mdl/visitor/visitor_xpath.go` (`buildXPathValueExpr`), `mdl/visitor/visitor_page_v3.go` (`xpathExprToString` emits `-7`, not `- 7`) | **The grammar fix alone is worse than the bug.** With the parser accepting `-7` but the XPath AST builder having no case for the new alternative, the constraint parses and silently serializes to `[Amount > ]` — a dropped operand instead of a loud parse error. Caught only because the visitor has a round-trip helper; the microflow write path uses `GetText()` and looked fine. **When adding a grammar alternative, check every consumer of that rule, not just the one your repro exercises.** **Scope correction**: the finding's own example (`addDays([%CurrentDateTime%], -7)`) still fails — `addDays` is a *microflow expression* function, not an XPath one, and it fails `CE0161` with a POSITIVE argument too, so the sign was never its problem. Repro `mdl-examples/bug-tests/it-18-xpath-negative-literal.mdl`; A/B on Mendix 11.12.1: pre-fix the script does not parse, fixed binary writes it and `mx check` reports 0 errors. issuetracker #18 | - -| Text painted by an **Atlas topbar widget is invisible in a dark theme** — the language selector measures ~1.13:1 contrast, glyph pixels spanning 4 luminance values out of 255. A theme override exists and *names the right element*, so it looks handled | Two separate mistakes stacked. (1) **Specificity**: Atlas's own rule is `.navbar-brand .widget-language-selector .current-language-text` at (0,3,0); a bare `.current-language-text` at (0,1,0) never wins, and only appears to on layouts that do not nest the selector under `.navbar-brand`. (2) **Wrong value**: `color: inherit` inherits *body ink*, which is dark, while the rail is dark in both palettes — so even at the winning specificity it measures 1.00:1. Atlas paints from `--bg-color-secondary` with a `#fff` fallback because it assumes a dark rail | `cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-atlas-map.scss` (the "Atlas fixups" block) | Re-declare **Atlas's own selector shape** so the weights match and source order decides, and resolve the colour through the rail token (`var(--mxt-rail-ink-active, var(--mxt-rail-ink))`) rather than `inherit`. List the bare and the `.navbar-brand`-nested selectors together — each is matched at its own specificity, so one rule covers both layouts. **Generalisable — the shape to look for**: a guard that names the right element is not evidence it applies. Read the *winning* declaration (`CSS.getMatchedStylesForNode` in DevTools, or the computed value) instead of the one you wrote. **Measure contrast, not colour**: reading `getComputedStyle(el).color` once and seeing a plausible value proves nothing — compute the WCAG ratio against the first non-transparent ancestor background, which is what turns "looks fine" into 1.13 vs 19.47. Reported from the RssReader test build; tests in `cmd/mxcli/theme/theme_test.go`, verified in a browser at 17.79:1 light / 19.47:1 dark | - -| A themed app is on-palette everywhere except a few widget details — the **Data Grid 2 pager caption is invisible** (1.02:1 on a dark ground), row-select checkboxes stay stock Mendix blue, popovers cast light-mode shadows. Re-pointing tokens changes nothing, and the same widget's other parts (the pager *buttons*) are fine | The theme source shipped by the **widget modules** (`themesource/datawidgets`, `atlas_web_content`) styles some things with Sass variables and literals — `datawidgets/web/variables.scss:18` is `$pagination-caption-color: #0a1325`. Sass resolves those at compile time, before any custom property exists, so the value is baked into `theme.compiled.css` and no `--mxt-*` can reach it. The parts that *do* work resolve `var(--gray-darker, …)` through Atlas: same bar, two mechanisms | `cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-widgets.scss` (the shared widget layer, imported after the theme partial) | Add a CSS rule per baked declaration, resolving through a token so both palettes follow. **The obvious fix does not work**: each module's `main.scss` imports `theme/web/custom-variables` before its own `!default` vars, so `$pagination-caption-color: var(--mxt-ink-muted)` there *would* win — but (1) the names collide with Atlas Core's, which feeds them to Sass colour functions (`atlas_core/web/_variables.scss:20` computes `mix($brand-primary, #e7e7e9, 10%)`; handing `mix()` a `var()` is a compile error) and (2) the worst offenders are not behind a variable at all — `_three-state-checkbox.scss` writes `#264ae5` and `rgba(#264ae5, 0.4)` directly. **Generalisable — the shape to look for**: read the **compiled CSS, not the SCSS**, when deciding what to override. The sources are full of `var(--token, #fallback)` declarations that already resolve correctly; in one measured app `#264ae5` appeared in 46 declarations and **24 were harmless fallbacks**, so grepping the source would have produced twice the rules for no benefit. Reported from the Formula1 test build (§33); verified in a browser: pager caption 1.02:1 → 6.99:1 console dark, 6.39:1 light, 6.78/5.93 on signal | -| A loop's variable used after `end loop;` passes `mxcli check`, then `mx check` fails `[error] [CE0108] "Variable 'item' is defined but not in scope at this location."` at the referencing activity. Applies to the **iterator** and to anything the body introduces (a `retrieve`, a `$X = create …`, a call output) | Nothing tracked loop-variable *visibility*. MDL052 already covered the sibling rule — names are unique across the whole microflow (CE0111) — and the wording of that rule ("scoped to the WHOLE microflow") reads as if the variable is readable flow-wide. Uniqueness and visibility are different: the name is reserved everywhere, readable only inside the loop body | `mdl/executor/validate_microflow_loop_scope.go` (new `MDL053`, wired from `microflowValidator.validate`), skill `.claude/skills/mendix/write-microflows.md` | Map each loop-scoped name to the loop whose **own** body introduces it (nested loops keep their own names), then walk the flow with the set of enclosing loops and flag any reference from outside the owner. **A name claimed by two loops is dropped, not reported** — that is the MDL052/CE0111 case, and without the guard the MDL052 negative example started failing for the wrong reason: the first loop's own use of `$R` was blamed on the second loop's claim. **Generalisable**: a rule keyed by variable *name* needs an ambiguity escape hatch whenever another rule exists precisely because names can collide. Both flavours verified against mxbuild 11.12.1 (2 × CE0108 in one probe). Repro `mdl-examples/bug-tests/loop-variable-out-of-scope.fail.mdl`; tests `mdl/executor/validate_microflow_loop_scope_test.go`. Found while working the sudoku findings, but **not** one of them — the numbered finding it was filed under is an app bug in that project, not an mxcli defect | -| `mxcli oql` silently omits a whole column: a `select A, B …` renders only `A`, and the JSON output has no `B` key at all — no error, no empty column. Reproduces whenever the value of `B` is **null in the first row** | The runtime omits a null-valued column from a row's JSON object entirely, and `parseOQLFeedback` took the column list from `extractColumnOrder(rows[0])` — one row, chosen for its key *order*. Every later row was then projected onto that short list, so the column vanished from the result rather than showing NULLs | `cmd/mxcli/docker/oql.go` (`parseOQLFeedback`, `mergeColumnOrder`, `hasOnlyKnownKeys`) | Union the keys of **all** rows, inserting each new key directly after the last key already known rather than appending — merging `[A, C]` with `[A, B, C]` must give `[A, B, C]`, not `[A, C, B]`, or a column that is null early in the result set jumps to the end of the table. The per-row re-scan is skipped when a row carries no unseen key, so the uniform case still costs one length check. **Generalisable**: any "take the shape from the first element" over a sparse encoding is a silent-wrong-answer bug, not a formatting bug — the output looks complete. Tests `TestParseOQLFeedback_ColumnUnionAcrossRows`, `TestMergeColumnOrder`; proven by stubbing the union back to first-row-only and watching the reported symptom return. sudoku #39, first half | -| `mxcli test` cannot run at all in a container without a Docker daemon — parsing, runner generation, model injection and the **entire mxbuild build** succeed natively, then `docker up` fails with "failed to connect to the docker API at unix:///var/run/docker.sock". So microflow tests are unavailable in exactly the environment mxcli targets (Claude Code web containers ship `/usr/bin/docker` with no daemon) | Only one step of the run was containerised — start the runtime and read its log — but it was wired directly to `docker compose`, with no seam for another way to run the app. `run --local` had all three pieces already (boot a standalone runtime, tee its log, restore) | `cmd/mxcli/docker/localapp.go` (new `StartLocalApp`), `cmd/mxcli/testrunner/runner_local.go` (new), `cmd/mxcli/testrunner/runner.go` (docker path extracted to `runDockerAndCapture`), `cmd/mxcli/cmd_test_run.go` + `main.go` (`--local`) | Give the run a seam — `runLocalAndCapture` / `runDockerAndCapture` — so both modes share parse/inject/parse-results/cleanup and differ only in how the app is started. **Two traps, both found by running it rather than reasoning about it.** (a) The runner reports via an **after-startup** microflow, so its LOG output happens DURING the start action, before the runtime's log subscriber attaches; registering the subscriber early is not possible (the runtime answers `LoggingException` pre-start). What actually carries the output is the JVM console tee, live from spawn — verified by A/B running with the early attach removed. (b) A **failing** test makes the runner return false, which makes the after-startup action fail, which makes `start` return an error — the first version reported that as a broken run and dumped a stack trace instead of the test report. A failed boot whose log shows a verdict is a normal outcome. Local runs use their own ports (8081/8091) and a `_test` database so a `run --local` dev loop can keep serving. Verified end-to-end in a daemon-less container: 2 passed; then 1 passed / 1 failed with exit 1, project restored. sudoku #41 | -| A solution's apps are served on one host with different ports, so they share a cookie jar (cookies key on host name and **ignore the port**) — logging into one silently replaces the other's `XASSESSIONID`. Giving each app a host name in **App Settings -> Configurations -> Application root URL** appears to do nothing under `run --local` | `runtimeConfigParams` builds the entire boot `update_configuration` payload from `LocalRuntimeOptions`, and `ApplicationRootUrl` was only ever populated from a `--hub` registration. The model's own value had no path into the payload — and since the admin action REPLACES rather than merges, nothing else could supply it either | `cmd/mxcli/docker/runlocal.go` (`configuredApplicationRootURL`, `applicationRootURLFrom`, `customHostRootURL`) | Read the setting off the project at boot and use it when no hub URL was assigned (hub wins — that URL is the one actually serving the app). **The trap is that a blank Mendix app already ships `ApplicationRootUrl = http://localhost:8080/`**, so "is set" does not mean "was chosen": honouring every value would change behaviour for every existing project and, under `--app-port`, advertise a port the app is not serving on. Only a **non-loopback host** is passed through, and a port that disagrees with `--app-port` warns. Serving under the host name needs no flag at all — the runtime accepts any `Host` and the client uses relative URLs (verified: 200 via `/etc/hosts`, nip.io and localtest.me); the setting matters only for the **absolute** URLs Mendix generates. **Generalisable**: before defaulting from a model setting, check what a blank project already has in it — a non-empty default makes "fall back to the model" a behaviour change, not a no-op. Verified end-to-end on 11.12.1: with `backend.local` configured, boot prints `Application root URL from configuration "Default"` and the app answers 200 on both the host name and the listen address. Tests `TestApplicationRootURLFrom`, `TestCustomHostRootURL` | -| A page bound to an attribute the entity **inherits** (`Person extends Administration.Account`, page binds `FullName`) passes `mxcli check --references` AND `mxcli lint`, then the real MxBuild fails `[error] [CE1613] "The selected attribute 'TaskBoard.Person.FullName' no longer exists."` The message reads as a deletion; it never existed there | Mendix stores a page's attribute reference against the entity that **declares** the attribute. `resolveAttributePath` qualified a bare name with `pb.entityContext` unconditionally, so a specialization that merely inherits the attribute got a dangling reference. Two independent resolvers had the same bug: the direct binding, and the final attribute of an association path (`resolveAssociationAttributePath`) | `mdl/executor/cmd_pages_builder_input.go` (`declaringEntityFor`, `entityAttributeOwners`), `mdl/executor/cmd_pages_builder_v3.go` (final attribute of the association path) | Walk the generalization chain and qualify with the first entity that declares the name. **Fix both resolvers** — a probe that only tested the direct case would have shipped half a fix; the reporter's own table already showed the associated case failing, and it did still fail after the first patch. Unknown names keep today's context qualification rather than being re-pointed, and a cyclic chain terminates. **Watch the new dependency**: attribute resolution now consults the domain models, and `getDomainModels` panics on a nil backend — several unit tests build a `pageBuilder` with neither backend nor cache, so the lookup bails out early instead. A/B on Mendix 11.12.1: pre-fix binary → CE1613 for the inherited column and 0 errors for the own column; fixed binary → 0 errors for both shapes. Repro `mdl-examples/bug-tests/todo-12-inherited-attribute-on-page.mdl`; tests `mdl/executor/cmd_pages_builder_inheritance_test.go`. mxcli-todo #12 | -| The SessionStart hook `mxcli init` writes cannot survive an idle reap: it is guarded on `test -x ./mxcli`, and `.gitignore` excludes that binary (~85 MB) on purpose. The container is reclaimed, the repo re-cloned without it, the guard fails and the hook no-ops through `\|\| true` — the next session has no mxcli, no MxBuild cache, no database, and no message saying so | The hook inlined the whole bring-up in one shell line, so "binary missing" could only be expressed as "skip". A hook line cannot reasonably do OS/arch detection and a download; something committed has to | `cmd/mxcli/init_hook.go` (`bootstrapScriptTemplate`, `writeBootstrapScript`, `sessionStartHookCommand`, `sessionStartHookMarkers`) | Emit a committed `.claude/bootstrap-mxcli.sh` that resolves OS/arch, fetches the binary when absent (`MXCLI_TAG` to pin), then runs the setup; the hook becomes `sh .claude/bootstrap-mxcli.sh \|\| true`. **Changing the hook command breaks dedupe**, which matched on the old command string — so `addSessionStartHook` now recognises *any* known marker and **rewrites the entry in place**, migrating an old project instead of leaving it with two hooks that both run. **Generalisable**: a guard whose condition is something you deliberately do not commit is a silent no-op waiting for a fresh clone — make the guard able to satisfy itself. Verified by reproducing the reap: moved `./mxcli` out of the project, ran the hook command verbatim, watched it re-download (88 MB, new mtime) and finish with "Setup complete … database ready". Tests `TestAddSessionStartHook_MigratesLegacyCommand`, `TestEnsureSessionStartHook_WritesFile`. mxcli-todo #2 | -| `mxcli exec -p app.mpr - <<'EOF' … EOF` fails with `Error reading file: open -: no such file or directory` — `-` is taken literally as a filename, so MDL cannot be piped or written as a heredoc and every ad-hoc script needs a temp file first | `exec` (and `check`) called `os.ReadFile(path)` directly, with no case for the conventional stdin spelling | `cmd/mxcli/mdlsource.go` (new `readMDLSource`, `mdlSourceLabel`), `cmd/mxcli/cmd_exec.go`, `cmd/mxcli/cmd_check.go` | One helper both commands share, so `check` gained the same spelling rather than only the reported one; `check` reports the source as `` instead of a bare `-`. Verified live: a heredoc through `exec` and a pipe through `check` both run. Tests `cmd/mxcli/mdlsource_test.go`. mxcli-todo #5 | -| `mxcli syntax` documents spellings the parser rejects, so an agent following the reference writes MDL that fails — `TEXTBOX … (Binds: Attr)` ("'Binds:' is no longer supported, use 'Attribute:' instead") and `DataSource: MICROFLOW Module.MF()` (a zero-arg microflow datasource takes NO parens, unlike RETRIEVE/CALL) | Nothing checks the `syntax` corpus against the parser. `make check-skill-mdl` validates MDL blocks in the skills and the docs site, but the `Syntax`/`Example` strings in `cmd/mxcli/syntax/*.go` are not covered, so a retired spelling can sit there indefinitely | `cmd/mxcli/syntax/features_page.go` (10 × `Binds:` → `Attribute:`, the datasource parens), `cmd/mxcli/syntax/retired_spellings_test.go` (new guard) | Fix the text **and** pin it: a table-driven test fails if a retired spelling reappears in any topic's Syntax or Example. It is a spelling guard rather than a parse — the snippets are fragments (a DATAVIEW body, a property line) that do not stand alone as statements, so they cannot just be fed to the parser. Proven by reintroducing `Binds:` and watching the test name the topic and field. **A third claim in the same report did not reproduce**: `CONTAINER (OnClick: SHOW_PAGE M.P(Param: $currentObject))` parses fine on current main, so only the two verified ones were changed. mxcli-todo #8 | -| `mxcli test … --local` (or any other `StartLocalApp` caller) fails with MxBuild's `the project file path should be an absolute path`, followed by a page of Windows sample requests, whenever `-p` is given a **relative** path | `ServeServer.Build` forwarded `ProjectFilePath` verbatim. `mxcli run` had learned to absolutize at the CLI layer (findings #17), but that fix lived in `cmd_run.go`, not in the code that talks to MxBuild — so the next caller re-hit it | `cmd/mxcli/docker/mxserve.go` (`ServeServer.Build`) | Absolutize `req.ProjectFilePath` in `Build` itself, the single place that talks to MxBuild, so no future caller can miss it; also resolve `LocalAppOptions.ProjectPath` in `applyDefaults` so `DeployDir` and the runtime log path are not derived from a relative value. Test by pointing a `ServeServer` at an `httptest` fake and asserting on the request body — the CLI-layer fix cannot be tested that way, which is part of why it did not generalise | -| `mxcli test --attach` fails with `reload_model failed: Authentication failed.` — after the test microflows have already been injected into the project | The M2EE admin API and the test endpoint are **different secrets**. `attach` built its `RuntimeController` with `M2EEOptions{Token: hs.Token}` — the endpoint token — instead of the runtime's admin password | `cmd/mxcli/testrunner/runner_attach.go` (`attach`), `cmd/mxcli/testrunner/handshake.go` (`Handshake`) | Carry `AdminPass` in the handshake alongside `Token` and pass that to `M2EEOptions`. The hosting `run --local` publishes it via `docker.LocalAppInfo` (the resolved value, not the package default, so a `--admin-pass` override still works). Whenever one process drives another's M2EE API, check which credential is being passed — `defaultLocalAdminPass` and any app-level token are unrelated | -| `alter page … set Editable = [expr]` (or `set Visible`) writes a project Studio Pro refuses to open: `StorageLoadException: Conditional editability settings has an invalid value '' for property Attribute`. `mxcli check` ✓ and `mx check` ✓ — neither inspects the stored value. The identical settings written by `create page` load fine | The ALTER path builds the `Forms$Conditional{Visibility,Editability}Settings` node by hand and wrote `Attribute: null`. `Attribute` is a **BY_NAME** `AttributeIdentifier`, so its unset value is the empty string, not null — exactly what the CREATE path already encodes via `codec.RegisterTypeDefaults(..., EmptyStringFields: []string{"Attribute"})`, whose comment records this same StorageLoadException from #627. Only the hand-built ALTER node missed it | `mdl/backend/pagemutator/mutator.go` (`setWidgetConditionalSettingMut`) | Write `{Key: "Attribute", Value: ""}`, not `nil`. **General rule: when one path hand-builds BSON that another path builds through the codec, diff the two encodings rather than eyeballing the hand-built one** — `mxcli bson dump --type page --object M.P` on a CREATE-authored and an ALTER-authored widget makes the divergence a one-line diff (key sets and values were otherwise identical). `SourceVariable` stays `nil`: it is BY_ID, where null *is* the absent value, so "null is wrong" is per-field, not a blanket rule. Test `TestSetWidgetConditionalSetting_AttributeIsEmptyString`; repro `mdl-examples/bug-tests/851-alter-page-conditional-attribute.mdl`. Issue #851 | -| A widget conditional using a function whose name is also an MDL lexer keyword — `visible: [trim($currentObject/Slug) != '']`, `[length(…) > 0]`, `empty`/`count`/`find` — **silently drops the whole property**; `mxcli check` ✓, `mx check` ✓, and the widget renders unconditionally visible. `toUpperCase`/`isMatch`/`contains` in the same position work | `xpathFunctionName` (MDLPage.g4) enumerated only `IDENTIFIER \| HYPHENATED_ID \| NOT \| TRUE \| FALSE \| CONTAINS`, so `trim(` never matched `xpathFunctionCall`. The enclosing `[...]` then failed to parse as an `xpathConstraint` and matched the generic `propertyValueV3` alternative instead, so the visitor set `Visible` (an array) rather than `VisibleIf`, and the builder's `else if pages.StaticVisibleExpression(...)` — which reads only bool/string — never fired | `mdl/grammar/domains/MDLPage.g4` (`xpathFunctionName`) + `mdl/executor/validate_widgets.go` (`validateConsumableConditional`) | Define `xpathFunctionName : xpathWord \| NOT` — `xpathWord` is a negated token set, so it self-maintains as the lexer gains keywords; an enumerated list reacquires this bug with the next promoted function name. Safe because `xpathFunctionCall` requires a following LPAREN and no `xpathStepValue` may be followed by one, so bare `empty` still parses as a path word. `NOT` is spelled out (xpathWord excludes it). **Also add the general guard**: MDL-WIDGET19 errors when `Visible`/`Editable` holds a value that is neither routed to `VisibleIf`/`EditableIf` nor a bool/string — that is the residue signature of any conditional the visitor could not build, so the next one fails loudly instead of vanishing. `make grammar` regenerates the parser (not committed). **Verify in a browser, not at `mx check`** — a dropped property is still a valid model, so `mx check` reports 0 errors before AND after; the symptom only exists at render time (see `verify-in-runtime.md`). The repro script carries a `Bug852.Verify` page for this: `Slug` is three spaces, so `trim()` changes the outcome and a dropped `Visible` renders (Mendix defaults to visible). Pre-fix all 5 markers render; post-fix only the 3 that should. **One rule, two contexts**: `xpathConstraint` serves both `Visible:`/`Editable:` (a Mendix *client expression* — trim/length/toUpperCase/find) and a datasource `where` (real *XPath* — contains/starts-with/ends-with/string-length/not, `length()` = list length, aggregates Java-only, and `empty`/`NULL` are KEYWORDS not calls). The sets differ, so the grammar must not enumerate either; mxbuild adjudicates. Regression-test the XPath side when touching this rule — `[Name = empty]`, `[Name = NULL]`, `not()`, `contains()`, `starts-with()`, `string-length()` all still parse and `mx check` clean. Tests `TestConditionalVisibility_KeywordFunctionNames`, `TestValidateStaticWidget_UnconsumableConditional`; repro `mdl-examples/bug-tests/852-conditional-keyword-functions.mdl`. Issue #852 | -| `download file $Doc;` is accepted by `mxcli check` and `mxcli exec` ("Created microflow") but the activity lands with **no action at all** — `describe` renders `-- Empty action` and `mx check` fails `[CE0008] "No action defined."`. Same for `download file $Doc show in browser;` | `microflowActionToGen` (the modelsdk write path) had no `*microflows.DownloadFileAction` case, so it hit `default: return nil` and the enclosing ActionActivity was serialized with a nil Action. Grammar, visitor, flow builder, read path and DESCRIBE formatter were all already in place, so the statement passed every stage that reports anything and vanished at the one that does not | `mdl/backend/modelsdk/microflow_write.go` (`microflowActionToGen`) | Add the case, setting `FileDocumentVariableName`, `ShowFileInBrowser` and `ErrorHandlingType` (Rollback default). **The storage key is `ShowFileInBrowser`, not `ShowInBrowser`** — the gen setter binds the right one; legacy's `parseDownloadFileAction` reads the wrong key. **Test at the round trip, not the reader**: a reader-only test starts from BSON the writer never had to produce, so `TestActionFromGen_DownloadFile` was green throughout. `roundTripMicroflow` (model→gen→codec→model) is the harness; assert the ActionActivity's `Action` is non-nil, which is the CE0008 shape itself. This is the same silent-drop mechanism as the `microflowObjectToGen` default branch (#791) — when auditing, diff the write switch's cases against `sdk/mpr/writer_microflow_actions.go`. Test `TestMicroflowRoundTrip_DownloadFile`; repro `mdl-examples/bug-tests/850-download-file-action.mdl`. Issue #850 | -| A decision written with **uppercase** keywords — `IF $T/Status != M.Status.Done AND $T/CompletedOn != empty` — passes `mxcli check` and then fails the build with `[error] [CE0117] "Error(s) in expression."`, quoting the expression back with `AND` still uppercase. The same condition written with `=` builds fine, which makes it look like `!=` cannot be an operand of `AND` | Mendix requires its word operators lowercase. A rebuilt `BinaryExpr` gets `strings.ToLower(e.Operator)`; a condition kept as an `ast.SourceExpr` (original text **plus** the parsed tree) returned `e.Source` verbatim and skipped it. The `=` form parses to a BinaryExpr and the `!=` form to a SourceExpr — hence the operator-shaped illusion | `mdl/executor/cmd_microflows_helpers.go` (`normalizeMendixOperatorCase`, applied in the `SourceExpr` branch) | Lowercase `and/or/not/div/mod` in preserved source, leaving everything else byte-identical: a scanner that tracks single-quoted literals (with `''` escapes) and skips any word preceded by `.`, `/` or `$`, so `'AND'`, `M.Enum.And`, `$Task/Mod` and `$Android` are untouched. **The reporter's own probe table is the cautionary bit** — nine builds established a rule ("any `!=` inside `AND` fails") that was real in every observation and wrong about the cause, because every failing probe was uppercase and the control was not. When a table's rule tracks a token, check what ELSE differs between the rows. Reproduced and fixed against mxbuild 11.12.1: stored `AND` → CE0117, stored `and` → 0 errors. Repro `mdl-examples/bug-tests/todo-14b-uppercase-and-operator.mdl`; tests `TestNormalizeMendixOperatorCase`. mxcli-todo #14b | -| `ALTER ENTITY X ADD EVENT HANDLER …` errors when the handler exists and `DROP EVENT HANDLER …` errors when it does not, so a script containing either cannot be re-run — and a defensive drop-then-add fails on whichever half does not match. `ADD ATTRIBUTE` has `IF NOT EXISTS`; event handlers had no equivalent | The idempotency guards added for attributes (findings #10) were never extended to the event-handler clauses, which are the one member with no other re-run route | `mdl/grammar/domains/MDLDomainModel.g4` (`ifNotExists?` / `ifExists?` on the event-handler clauses), `mdl/visitor/visitor_entity.go`, `mdl/executor/cmd_entities.go` | Reuse the existing `ifNotExists`/`ifExists` grammar rules rather than inventing a second spelling, so the guard reads the same everywhere. The error messages now name the flag, so the fix is discoverable from the failure. Verified by running the same script twice: first run drops (skipping, absent) then adds; second run skips both, exit 0, and the project still builds 0 errors. mxcli-todo #18 | -| `CREATE DEMO USER` reports success and `SHOW PROJECT SECURITY` reports `Demo Users Enabled: true`, yet the running app has **zero** accounts — `SELECT Name FROM Administration.Account` returns 0 rows, there is no login page, and none of the row-level XPath rules are enforced | A blank mxcli template ships with **Security Level: Off**, and with it off the runtime creates no accounts at all. The demo users are written to the model correctly; nothing connected the two facts, so the model said yes and the app said nothing | `mdl/executor/cmd_security_write.go` (`warnDemoUsersInert`, called after a successful create) | Say it at the moment the user would otherwise believe it worked, and name the one statement that fixes it (`alter project security level prototype`). A *warning*, not a refusal: authoring demo users before raising the level is legitimate ordering. **Generalisable**: when a write succeeds but a project-level setting makes it inert, the write path is the only place with both facts in hand. Tests `TestWarnDemoUsersInert`. mxcli-todo #15 | -| Wiring a microflow that takes parameters to a **BEFORE CREATE** event handler passes `mxcli check` (and `--references`), and the build then fails `[error] [CE7247] "Microflow should not have parameters" at Event handler of entity …` | Mendix passes no object to a before-create handler — the object does not exist yet — so the handler is called with no arguments. Nothing compared the handler's moment against the microflow's signature; the pairing is only invalid for this one moment/event combination | `mdl/executor/cmd_entities.go` (`checkBeforeCreateHandlerHasNoParameters`, called from `buildEventHandlers`) | Guard where the two paths converge — `buildEventHandlers` is shared by `CREATE ENTITY`'s inline handlers and `ALTER ENTITY ADD EVENT HANDLER`, so one check covers both. It refuses **before the model is written**, and the message carries the build code plus the way out (AFTER CREATE, which does receive the object). **A microflow created earlier in the same script is not readable back yet, so an unreadable microflow is skipped rather than refused** — mxbuild still catches the real case, and failing on the read would break legitimate scripts. Note this is an exec-time guard: `mxcli check` without a project cannot see the microflow's signature at all. A/B on 11.12.1: pre-fix binary writes it and mxbuild reports CE7247; fixed binary refuses, and both AFTER CREATE and a no-parameter BEFORE CREATE still work. Tests `mdl/executor/cmd_entities_before_create_test.go`. mxcli-todo #14a | -| "Contrast is low and not everything uses the dark theme" — and switching theme does not help, because `signal`, `ledger` and `console` all render the same defects. The worst of it is the **login page**: Atlas's stock photograph fills half the viewport on a dark app, and the Sign in button is green while the app's primary button is the brand colour | The login page is served from `theme/web/login.html`, which loads the SAME compiled theme CSS (`{{themecss}}`) — so it IS themeable, but nothing themed it. Two Atlas rules do the damage: `.loginpage-image` layers a brand-tinted gradient over `url("./resources/work-do-more.jpeg")`, and the submit button is `.btn-success`, so it follows the **success** colour rather than the brand. Separately, `--link-color` was mapped straight to the brand, and console's light-variant teal is 3.74:1 on white — under AA for body text | `cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-atlas-map.scss` (login block + `--mxt-link` indirection, all three copies stay byte-identical), `cmd/mxcli/theme/assets/console/.../_mxcli-console.scss` (light `--mxt-link`) | Replace the photo with a token-built gradient and point the login button at the brand; give link TEXT its own `--mxt-link` token defaulting to the brand, because a link needs 4.5:1 as text while a brand used as a button FILL only needs 3:1 plus contrast against its own ink — darkening the brand everywhere would have been the wrong lever. Console light gets `#0f766e` (5.47 / 5.10 / 4.92 against surface / ground / surface-alt). **The project's own `theme/web/logo.png` is deliberately left alone** — it is the app's asset to replace, and hiding it would strip a real logo from apps that have one. **Verification boundary, stated plainly**: verified at the compiled-CSS layer (the overriding `.loginpage-image` is last and carries no photo; the button and link declarations resolve), NOT in a browser — raising the scratch project's security level to serve a login page surfaced pre-existing model errors there that block deploy. The `#999` empty-state label could not be reproduced: every `#999` in the compiled CSS is a Bootstrap default (popover arrows, modal border, print styles), so that one needs the reporter's app. mxcli-todo #19 | - -| A wrong `icon:` reference passes `mxcli check` (including `--references`) and first surfaces as a **build** error: `[error] [CE1613] "The selected custom icon 'Atlas_Core.Atlas_Filled.no-such-icon' no longer exists." at Action button 'btnBad'` | Nothing resolved the reference — the name was written straight through to BSON. Icon-collection lookup existed only for `show`/`describe` (`cmd_iconcollections.go`), never in a validation path | `mdl/executor/validate_icon_refs.go` (`validateIconRefs`), wired into `validateProgram` | Index the project's icon collections once per run and resolve every reference in the program. **Put it in the `--references` pass, not the no-project one** — the collections are documents *in the project* (a blank 11.13 app ships three, ~770 icons), so unlike the #836 grant check there is genuinely nothing to resolve against without `-p`. **Report the two failures differently**: an unknown *icon* in a known collection gets near-match suggestions plus `describe icon collection `; an unknown *collection* gets the list of collections that exist, because that is where the typo usually is. **Generalisable — the shape to look for**: a string property that names a model element but is stored as a plain string is invisible to every reference checker; grep for properties whose value is a qualified name yet whose type is `string`. **Test the silence, not just the noise**: the risk in a new check rule is false positives, so sweep the repo's own examples (all 9 `mdl-examples` scripts using icons) before shipping — a rule that fires on valid input is worse than the gap it closes. **Repro cannot be a `.fail.mdl`**: `make check-mdl` runs `mxcli check` with no `-p`, so a bad-icon script would pass there and be reported as a negative test unexpectedly passing; the repro carries valid icons and the rejection cases live in unit tests. Repro `mdl-examples/bug-tests/icon-reference-validation.mdl`; verified end-to-end (bad reference reported before any write; `mx check` 11.13.0 0 errors on the valid script) | -| `ALTER PAGE … SET Action = microflow M.F ON btn` does not parse — `extraneous input 'M' expecting {DROP, ADD, SET, INSERT, REPLACE, '}'}`. The documented workaround is `REPLACE`, which works but silently drops every property the statement does not restate (ButtonStyle, Class, design properties, tooltip) | `alterPageAssignment` special-cases `DATASOURCE`, `VISIBLE` and `EDITABLE`, then falls through to `identifierOrKeyword EQUALS propertyValueV3` — and `propertyValueV3` has no `microflow ` form, so the value position could not hold an action at all. `CREATE PAGE` has had `ACTION COLON actionExprV3` all along | `mdl/grammar/MDLParser.g4` (`alterPageAssignment` gains `ACTION EQUALS actionExprV3`), `mdl/visitor/visitor_alter_page.go` (`buildAlterPageAssignment`), `mdl/executor/cmd_alter_page.go` (`convertASTAction` + routing), `mdl/backend/mutation.go` + `pagemutator/mutator.go` + `mock/` + `mcp/` (`SetWidgetAction`) | Reuse `actionExprV3` — the same rule CREATE PAGE uses — and build through the **CREATE PAGE builder** (`pb.buildClientActionV3`) rather than a second switch. **Generalisable — the shape to look for**: when `SET` and `REPLACE` (or any narrow/wide pair) can express different vocabularies for the same property, the narrow one is a whitelist that will be extended one bug report at a time — #855 was the identical bug for `DataSource`, filed separately. Delegate instead of enumerating, and the pair cannot drift. **Guard-don't-drop**: refuse `SET Action` on a widget with no `Action` property rather than writing it — Studio Pro resolves every stored property against the type's property list and throws, while mxbuild tolerates the unknown key, so a silent write **builds clean and fails to open**; the build is not a safety net. **Measurement trap**: reverting only the grammar rule does not produce a failing test, it produces a *compile* error (the visitor references `ctx.ActionExprV3()`), so prove causation by reverting grammar+visitor together and re-running the original statement through the CLI. **Do not be misled by CE1571 on a `SHOW_PAGE` action** — authoring the same action through CREATE PAGE reproduces it exactly, so it is the builder's `$currentObject` auto-binding outside a dataview, not this change; always author the control before blaming the new path. Note `OPEN_LINK` still refuses on the modelsdk engine (`LinkClientAction` is unsupported by the codec) — also pre-existing, and CREATE PAGE refuses it identically. Repro `mdl-examples/bug-tests/alter-page-set-action.mdl`; verified end-to-end (Mendix 11.13.0, `mx check` 0 errors, `Class` survives four retargets without being restated) | **Key insight:** `microflows$ListRange` stores offset/limit inside a nested `CustomRange` map — must cast `raw["CustomRange"].(map[string]any)` before @@ -405,360 +171,5 @@ extracting `OffsetExpression`/`LimitExpression`. - [ ] Failing test written before implementation - [ ] `go test ./mdl/executor/... ./sdk/mpr/...` passes -- [ ] New symptom row added to the table above (if not already covered) +- [ ] New finding appended to `findings/.jsonl` (if not already covered), and `make check-findings` passes - [ ] PR title: `fix: ` -| A login that is known to be correct starts failing on `run --local` — the page says only **"Sign in failed"**, as if the password were wrong. `--screenshot-user` silently produces screenshots of the login page instead of the requested page | The local runtime is unlicensed, and an unlicensed runtime caps concurrent sessions. Past the cap it refuses the sign-in and logs `Maximum number of sessions exceeded! (You are currently using a trial license)` to `.mxcli/runtime.log` only; the page reports the refusal identically to a bad password. mxcli's own login helper never noticed either — it filled the form, waited, and saved whatever session it had | `cmd/mxcli/docker/screenshot_login.go` (`loginScript` failure detection, `loginFailureHint`, `readLogTail`), `cmd/mxcli/docker/runlocal.go` (passes `RuntimeLogPath`), `.claude/skills/mendix/run-local.md` | Not an mxcli defect in the model layer — but mxcli held both halves and joined neither. Mendix answers a rejected sign-in by re-rendering the same form, so **the username field still being present after the click is the signal** that login did not complete; on that signal read the tail of the runtime log and name the cap. **Generalisable**: when a browser-driven helper "succeeds" with a degraded result, look for a server-side log that already says why — the page is often the least informative witness. Sessions are released by restarting `run --local`; a browser-driving script should sign out at the end, or the fifth or sixth run is the one that fails and it looks like a regression. Tests `TestLoginFailureHint`, `TestReadLogTail`. mxcli-todo #16 | -| A fresh clone of a project created by `mxcli new` goes dirty the first time anyone builds it: ~50 **tracked** files modified that nobody edited — every `javascriptsource/*/actions/*.js` gains a banner, `import { Big } from "big.js"` and `export async function`, plus the matching `javasource` stubs. In a cloud session with a stop-hook git check it reads as "uncommitted changes" at the end of clean work | The template ships the generated action stubs in a slightly older shape and MxBuild rewrites them all on the first build. `mx check` does **not** — only a build does — so nothing before the first `run --local` could reveal it, which is after the user has already committed | `cmd/mxcli/docker/settle.go` (`SettleGeneratedSources`), `cmd/mxcli/cmd_new.go` (step 5/6, `--skip-build`), `cmd/mxcli/init.go` (`/theme-cache/` in the generated ignore list) | Fix the *timing*, not the content: run the build while the project is still being created, so the settled form lands in the first commit. Do **not** reimplement the rewrite — it is mxbuild's generator and version-specific; run the real thing. Best-effort by contract (no JDK, no mxbuild, failed build → warning, never a failed creation), because a settled tree is a nicety and a usable project is the deliverable. The other half is gitignore: `theme-cache/` is a cache and says so. A/B on 11.12.1, both git-init'd then built: `--skip-build` → 50 dirty files, default → **0**. Tests `cmd/mxcli/docker/settle_test.go`. mxcli-todo #7 | -| `mxcli check` reports `✓ Syntax OK` / `Check passed!`, then `mxcli exec` on the same script fails partway through with `failed to resolve page: page not found: Module.Page` — a button targeting a page the script creates further down. `exec` is not transactional, so the statements before the failure are already written to the .mpr | Page references are resolved in statement order at exec time. `check --references` already had an ordered pass (`validateForwardPageRefs`), but it needs `-p`; plain `check` had nothing, and plain `check` is what gets run | `mdl/executor/validate_page_order.go` (`ValidateScriptPageOrder`, MDL-PAGE01), wired in `cmd/mxcli/cmd_check.go` beside the other project-free validators | The soundness argument is what makes this work without a project: a **plain** CREATE later in the script would fail if the page already existed, so the script itself asserts the page does not exist yet and the earlier reference cannot resolve against the project either. `CREATE OR MODIFY`/`OR REPLACE` assert nothing, so they stay with `--references`, which can look. **Generalisable**: an ordering rule that seems to need project state often does not, once you find the statement that already asserts what you were going to look up. Two things the diagnostic must say and does: a cycle cannot be fixed by ordering (create one page without the linking widget, add it with `ALTER PAGE … INSERT`), and commit before executing a large script. Verified against all `mdl-examples/**/*.mdl` for false positives (0). Tests `mdl/executor/validate_page_order_test.go`, example `mdl-examples/bug-tests/todo-9-forward-page-reference.fail.mdl`. mxcli-todo #9 | -| A sidebar navigation label renders truncated — "All tasks" shows as "All task" — under every theme (`signal`, `ledger`, `console`) and both variants, so switching theme does not help. Measured on the live DOM as `scrollWidth=56` inside `clientWidth=48` | Atlas's **closed** sidebar is an icon rail: `--navsidebar-width-closed: 48px` in Atlas's own `themesource/atlas_core/web/themes/_theme-default.scss`. The label is wider than the rail, and the closed rail expects an *icon*, not text. No mxcli theme sets any navigation width — the themes map colours — which is exactly why every theme reproduces it | `.claude/skills/mendix/theme-styling.md` (documented; no code change) | **A fix was written, verified, and reverted** — record the reason: adding `text-overflow: ellipsis` to the nav item helps only where Atlas also sets `white-space: nowrap`; elsewhere the label wraps to two readable lines, and the rule turns `All / tasks` into `All / t…`. Screenshotted both ways against the real compiled CSS. The answer belongs to the app (give nav items icons — what the rail is for — or keep the sidebar open), not to a theme that would impose it on every app. **Generalisable**: when a reported symptom traces to an upstream layout constant, reproduce the geometry against the real compiled CSS (48px container, real class names, Playwright measurement) — it takes minutes, tells you whose constant it is, and shows when the "obvious" CSS fix is a regression. mxcli-todo #19d | -| `create non-persistent entity X ( A: String not null error '…' )` (or `unique`) passes `mxcli check` AND `mxcli exec`, then the build fails **CE0070** "Validations rules are not allowed on entity 'X', because it is not persistable" | `not null` / `unique` ARE validation rules — Studio Pro models "required" and "uniqueness" as rules on the entity, not as column constraints — so Mendix rejects them on a non-persistable entity. Nothing in mxcli connected the attribute constraint to the entity's persistence kind | `mdl/executor/cmd_enumerations.go` (`validateNPEValidationRules`, called from `ValidateEntity`) | Add **MDL054**, error severity, fired from the CREATE path only. **Establish the construct matrix against mxbuild before writing the rule, not from the issue text** — verified on 11.6.6 that `not null` with a message, `not null` bare, AND `unique` each produce CE0070 while a plain attribute does not, so the bare form (easy to miss, since the reporter only showed the message form) is flagged too. The CREATE path is the only one that can run this: `ALTER ENTITY … ADD ATTRIBUTE` does not carry the persistence kind, the same limitation MDL020 has. Sweep `mdl-examples/` + `scripts/check-skill-mdl.sh` after adding any error-severity rule — a false positive there breaks every user with that shape. Negative test `mdl-examples/bug-tests/832-npe-validation-rules.fail.mdl` (`.fail.mdl` = must fail check, enforced by `make check-mdl`) plus `-ok.mdl` pinning the other edge; tests `TestValidateEntityNPEValidationRules`. Issue #832 | -| `retrieve $L from Mod.Entity where [Attr = $Var/Mod.Assoc/Attr]` passes `mxcli check` AND `mxcli exec`, then the build fails **CE0161** "Error(s) in XPath constraint" | Mendix XPath reaches at most ONE hop off a variable, and nothing checked the hop count | `mdl/executor/validate_microflow.go` (`checkXPathVariableTraversal`, `xpathVarTraversalRe`), called from the `*ast.RetrieveStmt` arm beside MDL047/MDL048 | Add **MDL055**, error severity, matching a `$var`-rooted path with **2+ segments**. **Establish the boundary against mxbuild first — it is narrower than it looks**: `$Var/Attr` VALID, `$Var/Mod.Assoc` VALID (one hop to the associated object), `$Var/Mod.Assoc/Attr` CE0161. A rule keying on "a module-qualified segment follows a variable" would reject the middle form, which is legal; key on hop count instead. Verified on 11.6.6 by building all three and dropping the offender to confirm the other two are clean. **Reject, don't try to serialize** — there is no valid XPath for the two-hop form, so the constraint must be restructured and only the author knows which way. **Verify the suggestion you emit**: both recommended rewrites (`retrieve $Related from $Var/Mod.Assoc;` then constrain on `$Related/Attr`; or invert to `[Mod.Assoc/Mod.Entity = $Var]`) were built and confirmed at 0 errors before the message claimed "both forms build clean". Negative test `mdl-examples/bug-tests/831-xpath-variable-traversal.fail.mdl` + `-ok.mdl`; test `TestXPathVariableTraversal`. Issue #831 | -| `mxcli check` reports a microflow rule (e.g. MDL048 `[id = $StringVar]`) but `mxcli exec` writes the same script silently — a script that skips `check` produces a project the build rejects | Two different validators. The exec path called `ValidateMicroflowBody` (semantic errors); the MDL0xx rule set lives in `ValidateMicroflow`, which was wired only into `cmd/mxcli/cmd_check.go` and the LSP. Same shape as #836, where a guard existed on every exec path but was never reached from validate | `mdl/executor/validate.go` (`validateMicroflowRules`, `execEnforcedMicroflowRules`) called from `mdl/executor/cmd_microflows_create.go` — the handler, mirroring `ValidateEntity` in `cmd_entities.go` (NOT `validateWithContext`, which would double-report under `check --references`) | Promote an **explicit allowlist** of rules verified against a real mxbuild, never the whole set. **Blanket promotion was tried and reverted**: it turns every error-severity rule into a write barrier, and `MDL009` ("enumeration splits require exactly one value per branch") was a **false positive** — a multi-value branch covering every enum value builds at 0 errors on 11.6.6, and the shipped `write-microflows` skill documents that very form (since retired, see the MDL056 row). It also broke an existing repo test whose fixture uses `else` on an enum split. `MDL008` by contrast IS correct (mxbuild: CE0079 per uncovered value + CE0773) — so before promoting any rule, build its construct and read the verdict. Warnings are never promoted. Tests `TestValidateMicroflowRules_ReachedFromExec` and `…_UnverifiedRulesNotPromoted` (the latter fails if the allowlist is widened carelessly). Issue #833 | -| `mxcli check` errors **MDL009** "enumeration splits require exactly one value per branch" on `when Open, Pending then` — but Mendix accepts it, so check rejects valid MDL and contradicts the shipped `write-microflows` skill | The rule asserted the opposite of the platform's behaviour. Nobody had built the construct: a multi-value branch covering every value **plus `(empty)`** builds at 0 errors on 11.6.6 | `mdl/executor/validate_microflow.go` (the `*ast.EnumSplitStmt` arm; `checkEnumSplitEmptyBranch`) | Retire the assertion and replace it with what actually fails: an enum split needs an outgoing flow per condition value, so a missing branch is **CE0079**. **MDL056** checks the `(empty)` branch — universal, verified to hold even on a `not null` enum attribute, so it needs no enumeration lookup and works from the statement alone. Full value coverage (the other half of CE0079) is deliberately NOT implemented: it needs the enum's member list, i.e. resolving the split variable's type against script or project, which `ValidateMicroflow` cannot see — guessing would trade one false positive for another. **Use a NEW rule ID rather than repurposing**, so anything citing the old number still means the old, wrong thing. `MDL008` (no `else`) is correct and stays — mxbuild gives CE0079 per uncovered value **plus** CE0773 on the else flow. Fix the skill in the same change: it documented the invalid `else` form. Tests `TestValidateMicroflow_EnumSplitMultipleValuesAllowed` / `…RequiresEmptyBranch`; repro `mdl-examples/bug-tests/mdl009-enum-split-empty-branch.fail.mdl` + `-ok.mdl` | -| A microflow using `split type` writes a project mxbuild cannot **load**: `KeyNotFoundException: The given key '' was not present in the dictionary` at `StreamingBsonUnitReader.ResolvePostponedProperties`. `mxcli check` ✓ and `mxcli exec` ✓; reproduced on 11.6.6 and 11.13.0 | Two gaps in the modelsdk writer, both the #791 shape. (1) `microflowObjectToGen` had no `*microflows.InheritanceSplit` case → `default: return nil`, so the split was dropped while three sequence flows kept pointing at its `$ID`. (2) `caseValueToGen` had no `InheritanceCase` case → every branch degraded to a bare `Microflows$NoCase`, losing the entity it selects on. Its value-receiver normalisation also omitted the type, so a pointer-only fix would still miss half the calls | `mdl/backend/modelsdk/microflow_write.go` (`microflowObjectToGen`, `caseValueToGen`) — mirror `sdk/mpr/writer_microflow.go` | Add both cases. **Diagnose with the #791 recipe**: `mxcli bson dump --type microflow`, collect every `$ID`, check each key ending in `Pointer` resolves (before: 27 objects / 3 dangling; after: 28 / 0). **Take field lists from the GENERATED type, not from legacy** — legacy writes `ErrorHandlingType` on the split but `initInheritanceSplit` has no such property, i.e. legacy writes a field Mendix does not define. **When adding a case-value type, update the value-receiver normalisation too.** Modelling rules confirmed on both versions while verifying: a type split needs an outgoing flow for every type INCLUDING the base (CE0090), and an `else` does NOT substitute for the base-type case. Tests `TestMicroflowRoundTrip_InheritanceSplit`, `TestCaseValueToGen_InheritanceCase{,ValueReceiver}`; repro `mdl-examples/bug-tests/split-type-dangling-pointer.mdl` | -| The `split type` docs and examples teach a shape that fails the build: `case Spec` + `else`, with no branch for the base entity → **CE0090** "The 'X' value should be configured for an outgoing flow". `mxcli check` passes, so the drift survived; `mdl-examples/bug-tests/365` and `475` both shipped it, and 475's own header claimed "mx check reports 0 errors" | `else` on an inheritance split serializes as `Microflows$NoCase` and IS accepted, so it looks like it covers the remainder — but it does not satisfy type coverage. The base entity needs its own `case` | `.claude/skills/mendix/write-microflows.md` (Type Split section) + `mdl-examples/bug-tests/365-…`, `475-…` | Cover EVERY type including the base; `else` is then redundant. Also give the split somewhere to go: branches converge on a merge continuing to the end event, so a non-void microflow needs a `return` after `end split;` (else MDL003 + **CE0067**). Matrix verified on 11.6.6 AND 11.13.0: `specs+base` 0 errors, `specs+base+else` 0 errors, `specs+else only` CE0090. **When repairing a bug-test fixture, preserve the scenario it pins** — 475 tests "exactly ONE non-split branch continues", so its added base case must TERMINATE; an empty (falling-through) body would make two branches continue and silently retire the regression. Confirmed after the edit that the post-split activity still renders outside both case bodies and the describe→exec roundtrip is mxbuild-clean. Known cosmetic artifact: DESCRIBE emits an empty `else` block that was never authored; it re-parses and builds clean | -| A published OData service created purely from MDL passes `mxcli check` and then fails the build — `[CE0729] "The service name should not be empty."` and `[CE7375] "Attribute ID for entity 'X' must be published and be the key when associations are exposed as an associated object id."` — the second firing even with no associations exposed at all | Two defaults `CREATE ODATA SERVICE` never set. (1) `Name` (the document) and `ServiceName` (the name in the OData metadata document) are different properties and only the first was set; the CONSUMED path had defaulted this for CE0339 all along. (2) `PublishAssociations` defaults to false = "associations as an associated object id", which Mendix only allows when the system `ID` is published as the key — but MDL's `expose (Attr (KEY))` publishes an ordinary attribute | `mdl/executor/cmd_odata.go` (`serviceName` fallback + heal on create-or-modify; `publishAssociationsFor`; `nonPersistablePublishedEntities` warning), `mdl/ast/ast_odata.go` + `mdl/visitor/visitor_odata.go` (`PublishAssociationsSet`) | **Wider than reported**: the finding framed CE7375 as a non-persistable-entity problem. Measured on 11.12.1, the identical service with a PERSISTENT entity and a unique key builds 0 errors with `true` and CE7375 with `false` — so the default broke *every* published service, and non-persistable was just where it could not be worked around. Defaulting to true does not pick a preference; it picks the only value that can build from the MDL people write. Needs tri-state (`PublishAssociationsSet`) so an explicit `false` is still honoured, and `create or modify` no longer flips a stored value the author did not mention. **Why nothing caught it**: `mdl-examples/doctype-tests/10-odata-examples.mdl` sets both properties explicitly, so the repo's own example worked around both defaults. Tests `cmd_odata_service_name_test.go`, `cmd_odata_publish_associations_test.go`; examples `f1-10.1-…`, `f1-10.4-…`. mxcli-formula1 #10.1/#10.4 | -| A typo in an OData property — `ReadMicroflow:` for `ReadMode:`, `ServiceNam:` for `ServiceName:` — passes `mxcli check` and `exec` reports success, but the model does not have the property. Hours can go into wondering why a published resource ignores its read microflow | The grammar accepts any `name: value` pair inside an OData property list, and the visitor's `switch` had no `default` — so an unrecognised name was dropped between parse and AST. The ALTER path has always answered `"unknown OData service property: %s"`; CREATE, PUBLISH ENTITY, the client and the external entity had nothing | `mdl/ast/ast_odata.go` (`UnknownProperties` on four statements), `mdl/visitor/visitor_odata.go` (four `default:` arms), `mdl/executor/validate_odata_properties.go` (`ValidateODataProperties`, MDL-ODATA01), wired in `cmd/mxcli/cmd_check.go` | The visitor is where the name is lost, so the visitor is where it must be recorded — a validator over the AST alone cannot see a key that was already discarded. Carry it as `UnknownProperties` and report at check time, before anything is written. The message names the property AND guesses the intended one (prefix/substring, then one edit), because a bare known-property list still leaves the reader diffing two spellings by eye. **Correction to the report**: `Pagesize:` is *not* silently dropped — the visitor lowercases before matching, so casing is never a typo, and the test pins that. Verified against every `mdl-examples/**/*.mdl` for false positives (0). Tests `mdl/executor/validate_odata_properties_test.go`. mxcli-formula1 suggested issue 8 | -| A read-microflow-backed OData resource must declare a `System.ODataResponse` parameter and compute a count, even when the count is expensive (a full CSV scan) and nobody asked for it — with no MDL to say otherwise. Same for `$skip`/`$top` support | `Countable`, `SkipSupported` and `TopSupported` were written as literal `true` in the BSON writer's `ODataPublish$QueryOptions`; nothing above the writer could express them | `mdl/ast/ast_odata.go` + `mdl/visitor/visitor_odata.go` (`*bool` on `PublishedEntityDef`, `odataBoolPtr`), `model/types.go`, `mdl/executor/cmd_odata.go` (`astEntityDefToModel`), `mdl/backend/modelsdk/odata_write.go` (`boolOrDefault`), `odata_read_detail.go` (`falseOnly`) | Tri-state (`*bool`) is load-bearing: these default to **true**, so "unset" and "false" cannot share a representation or every existing script would silently turn them off. The reader maps a stored `true` back to nil (`falseOnly`) so DESCRIBE prints only what the author wrote instead of three defaults on every resource. Verified end to end on 11.12.1: `Countable: No` + a read microflow with **no** `$Response` parameter builds 0 errors, which is exactly the combination that was impossible before. Tests `cmd_odata_query_options_test.go`, `odata_write_test.go`. mxcli-formula1 #10.3 | -| `describe odata service Module.Api` emits MDL that will not parse — `ReadMode: CallMicroflow:Module.Read` matches no value, `expose (Module.Entity.Attr ...)` where the clause takes a bare member name — and quietly renames the entity set, printing the entity TYPE's exposed name in the `as '…'` position where the entity SET's belongs | Three independent slips in one emit block. The backend stores a microflow-backed mode as `CallMicroflow:` and a member fully qualified; DESCRIBE printed the stored forms verbatim. The set/type name confusion is invisible in a single-word case and only shows when the two differ (Studio Pro's convention is singular type, plural set) | `mdl/executor/cmd_odata.go` (`odataModeToMDL`, `bareMemberName`, entity-set exposed name, `KEY` over `IsPartOfKey`) | Storage form ≠ input form: anywhere DESCRIBE prints a value read back from the backend, ask whether the *parser* accepts that spelling — `mx check` and the linter never see DESCRIBE output, so nothing else can catch it. Proved by round trip rather than by eye: describe → check (parses) → drop → exec the output → describe again → **byte-identical**, and the rebuilt model reports 0 errors from mxbuild. Tests `cmd_odata_describe_roundtrip_test.go`. mxcli-formula1 #10.5 | -| A `create database connection … type 'Redshift'` (or `'SQLServer'`) executes, builds **0 errors**, and the connection does not work. The skill's own table listed both, and omitted the one value that matters for an unsupported driver | mxcli passes the type string straight to BSON (`addStr(e,"DatabaseType",…)`) and **mxbuild does not validate it either** — verified on 11.12.1 — so nothing between the author and the runtime says the type is not real. Studio Pro's picker (read from `modeler/ide-client/database-connector-editor/`, identical on 11.10.0/11.12.1/11.13.0) is MSSQL, MySQL, Oracle, PostgreSQL, Snowflake, **BYOD** ("Other") — no Redshift, no SQLServer | `mdl/executor/validate_database_type.go` (`ValidateDatabaseConnectionType`, MDL-DB01), wired in `cmd/mxcli/cmd_check.go`; `.claude/skills/mendix/database-connections.md` | **A warning, not an error**: the value set is version-specific and mxcli cannot prove a string wrong on a Mendix version it has not seen — but silence is worse when the build is green and the connection is dead. **`BYOD` is the discovery worth keeping**: it forces connection-string config and *skips the driver-presence check*, so any JDBC driver Mendix has no entry for (DuckDB, SQLite, ClickHouse) works by dropping the JAR in `userlib/`. **Generalisable**: when a doc lists enum values, the shipped Studio Pro editor bundle is the authority — grep `id:"…",label:"…"` out of `ide-client/`, and diff across cached versions to see whether the set moved. Tests `validate_database_type_test.go`. mxcli-formula1 #6 | -| `mxcli init` run from a solution root (several app folders, no `.mpr` at the root) reports success and writes tooling that points at `project.mpr` — a file that does not exist. Nobody is told which project it picked, because it did not pick one | `findMprFile` looks only in the target directory; an empty result fell through to a hardcoded `"project.mpr"` default and initialisation continued as if that were a real project | `cmd/mxcli/init.go` (`findMprFilesInSubdirs` + the three-way branch on the candidate count) | Look one level down, then branch on the **count**: 0 → warn that generated paths will be placeholders; 1 → announce the project and initialise **that** directory; 2+ → refuse, list them, and print the exact command naming one. One level only — a Mendix app keeps its `.mpr` at its own root, and walking deeper starts finding deployment copies and backups. Candidates are sorted so the refusal and its suggested command are stable rather than directory-order dependent. **Generalisable**: a "sensible default" that names a file which does not exist is not a default, it is a silent wrong answer — count the candidates and let the count choose the behaviour. Tests `cmd/mxcli/init_discover_test.go`. mxcli-formula1 #3 | -| `alter settings configuration 'Default' …` is the write form, but `describe settings configuration 'Default'` is a **parse error** — and `show settings configurations` summarises the configuration without `ApplicationRootUrl`, so the obvious command for "did my root URL land?" cannot answer it (and renders an empty DatabaseUrl as a bare `, ,`) | The grammar's `DESCRIBE SETTINGS` alternative took no object, so the read form of a write statement simply did not exist; the summary builder listed database/port fields and never gained the root URL when that property was added | `mdl/grammar/domains/MDLCatalog.g4` (`DESCRIBE SETTINGS (CONFIGURATION STRING_LITERAL)?`), `mdl/visitor/visitor_query.go` (reuses `DescribeStmt.Qualifier`), `mdl/executor/cmd_settings.go` (`writeSettingsConfiguration`, `describeSettingsConfiguration`, summary) | **Read forms should mirror write forms** — where MDL has `alter X `, `describe X ` should parse, and reaching for it and getting a parse error teaches the wrong lesson. Factor the emit into one helper so the whole-settings dump and the single-configuration dump cannot drift. An unknown name lists the ones that exist, or the user is guessing. **Watch for**: changing `describeSettings`'s signature broke three existing callers, and the same commit's `IsPartOfKey`→`KEY` change broke an OData round-trip test that only the FULL suite caught — run `go test ./mdl/...`, not just the new test. Tests `cmd_settings_configuration_test.go`. mxcli-formula1 #8 | -| `ALTER MODULE X ADD JAR DEPENDENCY (…)` succeeds, `list jar dependencies` reports it, the build is **green** — and the runtime throws `SQLException: No JDBC driver found in app for URL`. `deployment/build.gradle` has no dependencies block and `find deployment -iname '**'` returns nothing | Not a bad write. Declaring and resolving are **separate steps**: the model records the coordinate, and `mx sync-java-dependencies ` is what downloads it into `vendorlib/`. Studio Pro runs that when you edit Module Settings; nothing headless was running it. Confirmed on 11.12.1 — a full `mxbuild --target=deploy` resolves nothing, and the sync command then fetches the jar | `cmd/mxcli/docker/javadeps.go` (`SyncJavaDependencies`, `UnvendoredJarDependencies`), `cmd/mxcli/cmd_sync_java_deps.go` (`mxcli sync-java-deps [--check]`), `cmd/mxcli/docker/runlocal.go` (vendors before boot), `mdl/executor/cmd_modules.go` (`warnUnvendoredJarDependencies`) | **How to find the missing step**: the reporter's open question was "does mxbuild skip Maven resolution, or does mxcli write it somewhere MxBuild cannot read?" — neither. `strings mx.dll | grep -i dependenc` surfaced `ISyncJavaDependenciesRunner`/`SkipManagedDependencySync`, and `mx --help` listed `sync-java-dependencies`. When a model-level write "works" but the artefact never appears, check whether the **toolset** has a separate command for it before suspecting the write. Wired at three levels so the gap cannot stay silent: the executor says so the moment it writes an unvendored coordinate, `run --local` resolves it before boot, and `--check` exits non-zero as a build gate. Resolution needs network, so every call site is best-effort with an actionable message. Tests `cmd/mxcli/docker/javadeps_test.go`. mxcli-formula1 #12 | -| `$Total = 5;` does not parse — `no viable alternative at input '$Total=5'` — while `DECLARE $Total Integer = 0;` does, and so do `$X = HEAD($List)`, `$X = create M.E (…)` and `$X = execute database query …`. The error names the token, not the missing keyword | Assignment existed only as a **prefix** on specific activity statements (`(VARIABLE EQUALS)?` on CALL/CREATE/RETRIEVE/…), plus a `SET $Var = expression` statement. A plain value therefore required `SET`, which nothing in the error or the surrounding syntax suggested | `mdl/grammar/domains/MDLMicroflow.g4` (`setStatement : SET? …`), `cmd/mxcli/syntax/features_microflow.go` | Make the guessable form work rather than improve the error: `SET` is now optional and both spellings produce the same `MfSetStmt`/`ChangeVariableAction`. **Prove a grammar relaxation causes no regressions with a control binary, not by reading**: `git stash` the `.g4`, `make grammar`, build `bin/mxcli-control`, sweep every `mdl-examples/**/*.mdl` with both — 13 scripts fail, the *same* 13, all pre-existing. ANTLR's adaptive prediction picks the activity-prefixed alternatives over `setStatement` on its own; no ordering change was needed. Executed against a real .mpr, mxbuild reports 0 errors. Tests `mdl/visitor/visitor_microflow_bare_assign_test.go` (bare and keyword forms must agree on the AST, not merely both parse). mxcli-formula1 #13 | -| `mxcli test tests/ -p app/App.mpr` fails with "no such file or directory" for a `tests/` that sits right next to the `.mpr` | Test paths resolved against the process CWD only. Defensible in isolation, but mxcli otherwise encourages naming the project (`-p`) rather than standing in its directory, and project auto-discovery searches outward — so the two conventions collide and the failure looks like a missing directory | `cmd/mxcli/cmd_test_run.go` (`resolveTestPaths`) | Fall back to project-relative **only when the CWD-relative path does not exist**: a `tests/` in both places must resolve to the one the user is standing in, since silently preferring the project's copy would run the wrong suite. A path that exists in neither is passed through unchanged so the error names what was typed, not a rewritten path the user never mentioned. Tests `cmd/mxcli/cmd_test_run_paths_test.go`. mxcli-formula1 #13 | -| A test annotated `@cleanup rollback` (or with no `@cleanup` at all — rollback is the documented default) still leaves its rows in the database; a misspelled strategy like `@cleanup rollbak` does the same, silently, while the run reports PASS | `TestCase.Cleanup` was parsed and then used nowhere. The after-startup runner had no seam to implement it — tests run inside the startup action, so there is no context the runner owns. The test endpoint creates that seam: it builds the `IContext` each test runs on | `cmd/mxcli/testrunner/endpoint.go` (the handler's execute block), `cmd/mxcli/testrunner/cleanup_strategy.go` | Wrap the call in `ctx.startTransaction()` … `ctx.rollbackTransaction()` in a **finally** (a throwing test is the one most likely to leave half-written data), gated on a `rollback=1` query parameter the client sends per test. Report `rolledBack`/`rollbackError` in the response and warn per test — a rollback that fails silently is worse than none. Reject an unknown `@cleanup` value at **parse** time so `--list` catches it too. Verify against the database, not the endpoint's own claim: run one test with rollback and one with `@cleanup none` in the same suite and query Postgres — the `none` row must be the only survivor | -| A suite passes under `mxcli test --attach` and fails under `--local`, with assertions that depend on startup state (a loaded cache, seeded reference data) seeing zero rows | The `--local` runner pointed after-startup at its own registration microflow and did **not** chain the project's own, so the app's startup logic never ran. It was a deliberate choice (a known baseline) but was invisible: the run printed only `After-startup set to MxTest.RegisterEndpoint`, never that the user's microflow had been displaced | `cmd/mxcli/testrunner/runner.go` (`runEndpoint`), `cmd/mxcli/testrunner/cleanup_strategy.go` (`describeStartup`) | Capture project state **before** generating the endpoint MDL, and pass `state.afterStartup` to `GenerateEndpointMDL` so the generated flow chains it — the hosted `--test-endpoint` path already did this, and the mismatch between the two was the bug. Add `--skip-app-startup` for a deterministic empty baseline, and always print which of the two happened. Note the startup microflow's writes run at boot, outside any test transaction, so `@cleanup rollback` does not undo them. mxcli-formula1 findings #19 | -| `mxcli test tests/ -p app/App.mpr --list` fails with `stat tests/: no such file or directory` while the same command without `--list` runs fine | The `--list` branch passed raw `args` to `ListTests`, bypassing `resolveTestPaths` — so a path relative to the project (rather than the working directory) resolved for execution but not for listing | `cmd/mxcli/cmd_test_run.go` (the `if list` branch) | Pass `resolveTestPaths(args, projectPath)` there too. When a command has two entry points into the same input, check both go through the same path resolution. mxcli-formula1 findings #15 | -| After `SET` became optional, `mx check` on a project built from `02-microflow-examples.mdl` reports six errors that no MDL change caused: `[CE0109] "Undefined variable 'ProductList.Price'."` at four Aggregate list activities, and `[CE0015] "Aggregate function must specify a valid attribute."` at the expression-based one. Identical on both engines. `mxcli check` on the same script is silent | Two conversions existed for one syntax. `$Sum = sum($List.Price)` used to reach the dedicated `aggregateListStatement` rule; making `SET` optional put `setStatement` — alternative 5 of ~50 — in front of it, so ANTLR matched the lower-numbered alternative and the statement fell through to `buildSetStatement`'s fallback conversion, which joined list and attribute into one name and dropped the per-item expression entirely. Underneath, `buildListAggregateAsFunction` never appended the expression argument, so the SET path could not have seen it either | `mdl/grammar/domains/MDLMicroflow.g4` (`setStatement` moved LAST in `microflowStatement`), `mdl/visitor/visitor_microflow_statements.go` (`buildSetAggregate` replaces `extractVariableAndAttribute`), `mdl/visitor/visitor_microflow_expression.go` (`buildListAggregateAsFunction` appends the expression argument) | **A permissive alternative belongs last.** `$X = ` overlaps every `VARIABLE EQUALS ` statement in the rule — aggregates, list operations, RANGE — and ANTLR's ALL(*) picks the lowest-numbered alternative that matches, so a new general form silently steals from every specific one above it. **The measurement trap that let this ship**: the `SET?` change was swept with a control binary over every `mdl-examples/**/*.mdl` and found no difference — but with `mxcli check`, which parses and validates and never serializes. This defect lives between the AST and the BSON, where only `exec` + `mx check` can see it. Sweeping with `check` proves the grammar still *parses*; it proves nothing about what the visitor *builds*. For a grammar change, the control sweep must run the integration gate (`go test -tags integration -run TestMxCheck_DoctypeScripts`), not `check`. Fix proven by reverting both halves and watching the new tests fail with the reported symptom. Tests `mdl/visitor/visitor_microflow_aggregate_test.go`. Upstream CI on the ako→mendixlabs sync PR | -| `DESCRIBE microflow` prints a bare `else` on a `split type` the author never wrote one for, and each describe→exec pass accumulates another | An object-type decision always carries an `(empty)` outgoing flow (the null-object case), emitted by the builder whether or not an `else` was written. DESCRIBE rendered that flow as `else`. Invisible until the `InheritanceCase` writer landed — before that every branch degraded to `NoCase`, so nothing distinguished it from a real case | `mdl/executor/cmd_microflows_show_helpers.go` (the `elseFlow` block in the inheritance-split traversal) | Drop the `else` line when its body renders empty — the same `elseLineIdx`/truncate pattern the if/else emitters already use. Exec re-creates the flow, so the omission is lossless and the roundtrip is stable. **Do NOT 'fix' this in the builder**: removing the empty-entity branch there fails the build with **CE0089** "The '(empty)' value should be configured for an outgoing flow" — that flow is load-bearing and is why `else` cannot substitute for the base entity's case (CE0090); `(empty)` and the base type cover different things. That wrong fix was implemented first and caught only because every shape was re-run through mxbuild, not because a unit test failed. Tests `TestBuilder_InheritanceSplitKeepsEmptyCaseFlow` (builder must KEEP it) and `TestTraverseFlow_InheritanceSplitOmitsEmptyElse` (describe must not print it) | -| `alter page … set on ` reports `widget "X" not found` when the widget sits inside a datagrid column rendered as `customContent`; only CREATE OR REPLACE PAGE can touch it | `findInWidgetChildren`'s pluggable branch searched the grid's own `Object.Properties[].Value.Widgets` and matched columns by derived name, but never descended into a COLUMN's own content. Columns live at `Object.Properties[columns].Value.Objects[]`; their widgets are one level deeper at `Properties[content].Value.Widgets[]` | `mdl/backend/pagemutator/mutator.go` (`findInWidgetChildren`, the columns loop) | Descend into each column's `Properties[].Value` and reuse `findInWidgetArray(…, "Widgets", name)`. **Address by the nested widget's OWN name, not a `grid.column.widget` path**: DataGrid2 columns carry no stored name in the MPR (see `findBsonColumn`), so a column segment could only be a derived name that changes when the caption is edited — a path that goes silently stale. Keep the existing column-by-derived-name lookup working (a test pins it, or the descent could shadow it). Test `TestFindBsonWidget_InsideCustomContentColumn`; repro `mdl-examples/bug-tests/834-alter-customcontent-column-widget.mdl`. Issue #834 | -| `alter page … set Caption = '…' on ` fails with `widget has no Caption property` — for EVERY action button, nested or top-level | An ActionButton has no `Caption` document: its caption is a `Forms$ClientTemplate` under **`CaptionTemplate`** (Template → Items[] → Translation.Text), the same shape `setWidgetContentMut` already walked for `Content`. `setWidgetCaptionMut` only looked for `Caption` | `mdl/backend/pagemutator/mutator.go` (`setWidgetCaptionMut`, `setClientTemplateText`) | Fall back to `CaptionTemplate` via a shared `setClientTemplateText` helper, which `setWidgetContentMut` now uses too. **Found while fixing #834 — check whether a symptom reproduces OUTSIDE the reported context before attributing it**: this failed on a top-level button as well, so it was a second, independent defect and the #834 finder fix alone would not have made the reporter's command work. When a widget property will not set, dump the widget's key list (`mxcli bson dump --type page`) before assuming the setter is wired — the field is often stored under a different, template-shaped key | -| A published service will not build: every whole-number attribute is `[CE5016] "Attribute … has type Integer, but is published as Edm.Int32"`, and an exposed enumeration adds CE5016 plus `[CE4583] "Enumeration 'X' is not published in this service."` | `mendixAttrTypeToEdm` mapped Integer→Int32 (Mendix publishes it as **Int64**, same as Long), and the enum path wrote `Edm.String` while `EnumerationAsString` was hardcoded `false` — the one combination Mendix rejects, since with the flag false it wants the enumeration published as its own EDM enum type. The function's own comment flagged the unverified rows, and the existing unit test *pinned the wrong answer* | `mdl/executor/cmd_odata.go` (`mendixAttrTypeToEdm`, `enumPublishedAsString`, `publishedAttrType`), `model/types.go` (`PublishedMember.EnumerationAsString`), `mdl/backend/modelsdk/odata_write.go` + `sdk/mpr/writer_odata.go` (stop hardcoding the flag) | **Let mxbuild adjudicate the whole table at once**: publish one attribute of every Mendix type in one service and read the CE5016s off the build. That found Integer (reported) *and* Enumeration (only suspected), and confirmed String/Long/Decimal/Boolean/DateTime were already right — five verified rows for one build. Binary turns out to be unpublishable at all (CE5013), whatever type you give it. **A type and a flag that only work as a pair must travel as a pair** — `Edm.String` is ambiguous between String and a flattened enum, so the flag is the only thing distinguishing them and it belongs on the same struct. Watch for an existing test that encodes the bug: this one asserted `Edm.Int32`, so the fix *failed the suite* until the assertion was corrected. Tests `cmd_contract_test.go`, `cmd_odata_edm_type_test.go`. mxcli-formula1 #16 | -| `create or modify external entity Mod.E (… Countable: false)` — touching only an entity-level property — detonates every attribute: `[CE6612] "Attribute 'circuitId' of external entity 'Stg_Circuit' is not supported."`, one per attribute, leaving a project that cannot build | Not the executor: it already preserves attributes it was not asked to change (`if len(attrs) > 0`). One layer down, `attributeFromGen` handled `StoredValue` and `OqlViewValue` but **not** `Rest$ODataMappedValue`, so every attribute of an external entity read back with no `RemoteName`, and the writer's `isExternal && a.RemoteName != ""` arm then fell through to a plain StoredValue on the next read-modify-write | `mdl/backend/modelsdk/domainmodel.go` (`attributeFromGen` gains the `ODataMappedValue` / `ODataMappedPrimitiveCollectionValue` arms) | **The attribute-level half of #782**, which fixed the entity level and stopped there — when a read-modify-write loses data, check every *nesting level* of the read, not just the one named in the report. A polymorphic `Value` switch that silently ignores a variant is the shape to look for: it compiles, it reads, and it drops. Reproduce with a **local metadata file** (`MetadataUrl: './contract.xml'`) — no server needed, and the import is the same code path. Also learned here: the reported attribute *rename* (`name` → `Stg_Circuitname`) is a different thing entirely — it happens at import, from `reservedEntityAttrNames`, and `name` is **not** actually reserved (verified: Mendix builds an external entity with an attribute literally named `name`). Tests `external_entity_read_test.go`. mxcli-formula1 #25 | -| `execute database query … dynamic $Sql` reaches the runtime as the literal string `'$Sql'` — `Parser Error: syntax error at or near "$"` from the database, not from Mendix. Runtime-built SQL, and therefore query pushdown, is impossible | The builder quoted any dynamic query not already starting with a quote — right for `dynamic 'SELECT …'`, wrong for an expression — and the AST kept one `DynamicQuery` string whichever branch of the grammar produced it, so nothing downstream could tell them apart | `mdl/ast/ast_microflow.go` (`DynamicQueryIsExpression`), `mdl/visitor/visitor_microflow_actions.go` (set it in the `expr` branch), `mdl/executor/cmd_microflows_builder_calls.go` (`dynamicQueryExpression`) | **When a grammar has two alternatives that mean different things, the AST must record which one fired** — a shared field plus a "does it look quoted?" heuristic is a guess, and the workaround users find (`dynamic '' + $Sql`, which starts with a quote so the heuristic leaves it alone) is proof the heuristic is the bug. Verified by reading the stored BSON rather than by describe: `DynamicQuery\x00\x05\x00\x00\x00$Sql` — five bytes, no quotes. Tests `cmd_microflows_dynamic_query_test.go`. mxcli-formula1 #21 | -| `create odata client` against a service behind `authentication basic` prints `Warning: could not fetch $metadata: … HTTP 401`, creates the client anyway, and the following `create external entities from …` imports nothing — from a script that reports success | The statement's `HttpUsername`/`HttpPassword`/`HEADERS` are stored for the runtime, but the design-time fetch was a bare `client.Get`. The fetch failure is only a warning, so the empty client propagates silently | `mdl/executor/cmd_odata.go` (`metadataFetchAuth`, `metadataAuthFromStmt`, `fetchODataMetadata`), `mdl/ast/ast_odata.go` + `mdl/visitor/visitor_odata.go` (`HttpUsernameIsLiteral` / `HeaderDef.ValueIsLiteral`) | **Only a literal is usable at design time.** The visitor strips a quoted literal's quotes, so `'f1api'` and `Module.ApiUser` both arrive as bare strings — the AST has to record which was written, or mxcli sends a *constant's name* as the password. Unresolved names are reported instead, which is also the honest answer: mxcli has no runtime to resolve a constant against. **A warning on a step something else silently depends on needs to say what breaks next** — the message now names the empty client and the import that will do nothing. Verified against a real basic-auth server that 401s without credentials and 403s without the custom header, so both had to arrive. Tests `cmd_odata_metadata_auth_test.go`. mxcli-formula1 #23 | -| Re-running `create or modify odata service` after editing a `publish entity` block changes nothing — the served `$metadata` is identical, and only `drop odata service` + create picks the edit up | The modify branch updated the service's scalar properties and never touched `EntityTypes` / `EntitySets` | `mdl/executor/cmd_odata.go` (modify branch rebuilds published entities via `astEntityDefToModel`, and carries `AllowedModuleRoles` through) | **Replace, don't merge**: a member removed from the script has to leave the service, which merging cannot express — the script is the description of the service. **Carry through what the statement cannot express**: role grants come from a separate `grant access on odata service` and would otherwise be dropped by a modify (reported; *not* reproduced on 11.12.1 — kept as a guard, and the commit says so rather than claiming a fix). Verified: same script yields `Label as 'label'` before and `Label as 'label' (Filterable, Sortable)` after, build stays at 0 errors. Tests `cmd_odata_modify_members_test.go`. mxcli-formula1 #26 | -| `create external entities from` a contract that restricts capabilities produces a project that will not build: `'Seasons' is marked Countable=False in the OData service, but True in the app`, `'latitude' is marked Filterable=False …` — one per restricted resource or property | Insert/Update/Delete restrictions were parsed; **Count/Filter/Sort were not**, so the import had nothing to honour and defaulted all three to true — on the one command whose entire job is fidelity to the contract | `mdl/types/edmx.go` (`EdmEntitySet.Countable`, `NonFilterableProperties`, `NonSortableProperties` + the three `applyCapabilityAnnotations` arms), `mdl/executor/cmd_contract.go` | An unannotated set still means countable/filterable/sortable — **silence in a contract is not a restriction**, it is OData's own default, so `nil` and `false` must stay distinguishable (`*bool`, as with the publish-side query options). The generated entity is compared against the contract at *build* time, so anything the contract can say is something the importer must be able to read. Tests `mdl/types/edmx_test.go`. mxcli-formula1 #24 | -| A contract property called `name` is generated as `Stg_Drivername` / `Circuitname` — prefixed with the remote type. A page written against the published `$metadata` then fails with `The selected attribute 'F1Live.Drivers.name' no longer exists`, and the *same* field carries a different name in every module because the remote type names differ | `attrNameForOData` disambiguates any name in `reservedEntityAttrNames`, and `name` was on that list with the comment "Mendix system-managed attribute for the object name". It is not: Mendix builds an external entity with an attribute literally named `name` | `mdl/executor/cmd_contract.go` (`reservedEntityAttrNames` loses one entry; the import now reports the renames it does make) | **Test the whole list at once, not the reported entry.** One contract with a property per listed name, prefixing disabled, then `mx check`: CE7247 "The name 'x' is a reserved word" for `id`/`owner`/`changedBy`/`changedDate`/`createdDate`/`type`/`context`, and silence for `name`. That turns "is the list wrong?" into "which rows are wrong?" for the cost of a single build, and it *earns* the seven entries that stay rather than leaving them as folklore. Two existing tests pinned the old behaviour and had to be corrected — a hand-maintained list of platform rules will accrete guesses unless each row can point at an error code. **Migration**: a re-import renames the attribute back, so references to the prefixed name must follow. Tests `cmd_contract_reserved_test.go`. mxcli-formula1 #28 | -| `MOVE JAVA ACTION …` / `MOVE ODATA SERVICE …` is a parse error (`no viable alternative at input 'MOVEJAVA'`), and neither `CREATE` form takes a folder clause — so those documents can never leave the module root from MDL | The `moveStatement` rule listed seven doctypes and nothing else; the missing ones were never unimplemented, just unlisted | `mdl/grammar/MDLParser.g4` (two alternatives), `mdl/ast/ast.go`, `mdl/visitor/visitor_entity.go` (dispatch **and** the MOVE FOLDER discriminator), `mdl/executor/cmd_move.go`, backend `MoveJavaAction` / `MovePublishedODataService`, `sdk/mpr` exports `MoveUnitByID` | Both reduce to the existing reparent primitive — a top-level document move is one containment row, so a new doctype is a list entry plus a lookup, not new machinery. **Watch the discriminator**: `MOVE FOLDER` is told apart from a document move by the *absence* of a doctype keyword, so every keyword added to the rule must also be added to that condition or a folder move starts parsing as a document move. **Verify placement by differential count, not by reading the model**: run the script with and without the MOVE lines and diff `select ContainmentName, count(*) from Unit` — three new Folders rows (a nested path creates two) and an unchanged Documents count says reparented rather than copied or dropped. Grepping blobs for names is a trap; stock modules are full of the same words. Tests `visitor_move_doctypes_test.go`, example in `18-folder-examples.mdl` — which must sit **before** that script's `drop module`, a mistake the integration gate caught and `mxcli check` did not. mxcli-formula1 #32 | -| `create odata client` with credentials given as constants (`HttpUsername: '@Module.ApiUser'`) still gets HTTP 401 and an empty client, after the fix that made literal credentials work. Sharpened by the same release making a constant `ServiceUrl` mandatory — the shape the tool insists on is the shape whose credentials it will not read | `resolveCredential` trusted the visitor's isLiteral flag. `'@Module.ApiUser'` **is** a STRING_LITERAL, so the flag said "literal" and the previous code sent the fifteen characters `@Module.ApiUser` as the username — and the unresolved-credential note did not fire either, because as far as the code knew nothing was unresolved | `mdl/executor/cmd_odata.go` (`resolveCredential`, `constantReference`, `designTimeConstants`) | **A syntactic classification is not a semantic one.** The visitor can say "this was a quoted string"; only the executor can say "this quoted string names a constant". Any flag of the form isLiteral needs the consumer to ask what the literal *contains* before treating it as a value. **The fix is to resolve, not to refuse**: a constant's design-time default is exactly what Studio Pro sends on the same fetch, so reading it is the value rather than a workaround — and mxcli already has the project open. Three spellings must all work (`'v'`, `@M.C`, `'@M.C'`); a dotted literal like a password containing a dot must not be mistaken for a reference. Tests `cmd_odata_metadata_auth_test.go`. mxcli-formula1 #23 follow-up | -| An app themed dark still shows light-mode drop shadows under the datagrid's filter-operator popover and dropdown filter lists | The generated widget layer re-pointed `.column-selectors` but not the four rules in `_datagrid-filters.scss` that bake the same two-layer shadow. Each already takes its *background* from `--bg-color-secondary`, so Atlas re-colours the panel and leaves the shadow — which is why it reads as a partial fix rather than an untouched widget | `cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-widgets.scss` | **Read the selectors out of the shipped `themesource/`, not the bug report** — the fourth here is `.dropdown-container .dropdown-list`, nested under a parent the report did not name. **Verify in the compiled CSS, never the source** (the §33 rule): apply the theme to a real project, run `mxbuild --target=deploy --java-home=… --java-exe-path=…`, then grep `theme-cache/web/theme.compiled.css` and check the *line number* — the fix must land after the widget module's own rule (30794 vs 27765 here) or the cascade eats it. A theme-cache file older than the SCSS you just wrote is a stale compile, and mtime is the cheapest way to catch it. mxcli-formula1 #33 / suggested issue 4 | -| A `MOVE` cannot be confirmed and a module's layout cannot be reviewed: `SHOW STRUCTURE` groups by document type at every depth and never names a folder, `DESCRIBE` answers for one document at a time, so checking where things ended up means opening the `.mpr` as SQLite | The read side of folders was simply never built. `MOVE`/`DROP FOLDER` write containment; nothing read it back | New `mdl/executor/cmd_list_folders.go` + grammar (`FOLDERS` lexer token, `showOrList FOLDERS (IN …)?` in `MDLCatalog.g4`, `FOLDERS` added to the `keyword` rule so it stays usable as an identifier), `mdl/ast/ast_query.go`, `mdl/visitor/visitor_query.go`, `mdl/executor/executor_query.go` | **A layout listing must show what is *not* there**: empty folders (`[0]`) and documents still at the module root, or it cannot be diffed against an intended layout — that is the whole use. Documents are indexed by `ContainerID` across every list call the backend offers, each best-effort, so a backend that cannot answer one kind yields a listing missing that kind rather than no listing. **Do not stub the hierarchy in the test** — `mkHierarchy` populates `moduleNames` but not `folderNames`, so `BuildFolderPath` returns `""` and every folder silently collapses into the module root, which looks exactly like the bug. Build it from the mock's `ListModules`/`ListUnits`/`ListFolders`, as `getHierarchy` does. Tests `cmd_list_folders_test.go`, example in `18-folder-examples.mdl`. mxcli-formula1 issue #2 | -| Every page in a locally-run app is a black screen. `mxcli check` passes, the build reports success, the runtime log is quiet, `curl /` returns 200 with a valid HTML shell, and every OData service answers — only a browser sees it. It also comes and goes between runs of the same command at the same version | `run --local` bundles the browser client at step 5b, then the runtime boot runs Gradle `clean-custom-classes compile package`, whose package pass repopulates `deployment/web` and takes `dist/` with it — deleting the bundle 51 seconds after the same command wrote it. It only bites when Gradle has work to do (a new Java action, a full recompile), which is why the app boots fine for weeks and then stops | `cmd/mxcli/docker/webclient.go` (`WebClientBundled`, `EnsureWebClientBundle`, `ReportLostWebClientBundle`), `cmd/mxcli/docker/runlocal.go` (step 6b, after the boot), `cmd/mxcli/docker/localapp.go` (headless boots warn instead of paying ~30s) | **A pre-condition established before a step that rewrites the same directory is not a post-condition** — verify after, not before. The guard is a `stat` when the bundle survived, which is what makes it affordable at every boot; re-ordering the bundle to after the boot instead would leave the app reachable-but-blank for ~30s on every cold start. **`curl /` returning 200 is not evidence the app renders**: the shell is served by the runtime, the client by a file the shell references. The one-line check is `curl -o /dev/null -w '%{http_code}' /dist/index.js`. **Do not silently pay for a repair on a path that does not need it** — `test --local` destroys the bundle too, but tests are headless, so it prints the loss and the remedy rather than spending 30s on a two-second loop. Tests `webclient_bundle_test.go`; both controls run (guard never fires → the wipe test fails; guard always fires → the survivor test fails). mxcli-formula1 §35 | -| `DESCRIBE PAGE` reports a drill-down button as `linkbutton b (Caption: 'Weekend', Action: show_page Mod.Page)` — the `(Race: $currentObject)` argument is gone. The model is fine (`mx check` is clean, and an unmapped required page parameter is a consistency error, so it could not have built), but the description reads as a diagnosis mid-hunt and costs cycles fixing a button that was correct | mxcli deliberately writes `ParameterMappings` as an empty array for a page action, because Studio Pro infers the row object from the enclosing widget and rejects an explicit `$currentObject` Argument as CE0115 (#296). That decision was right; its other half was missing. `renderClientActionMDL` read only explicit mappings, and the writer's comment *asserted* DESCRIBE recovered the implicit one — it never did | `mdl/executor/cmd_pages_describe_output.go` (`pageActionParameters`, `targetPageParameterNames`), stale comment corrected in `sdk/mpr/writer_widgets_action.go` | **When a writer stores something implicitly, the reader owes it an explicit reconstruction — and a comment claiming the reader already does is worth nothing until a test says so.** Recover from the *target page's* declared parameters, not from the action, since that is where the information actually lives. Guard both ends: an explicit mapping still wins (recovery fills a gap, it does not override Studio Pro), and an unresolvable page yields no arguments rather than invented ones — a description that omits an argument is recoverable, one that names a parameter that does not exist is not. **A lossy DESCRIBE is costliest exactly when it is most used**, because DESCRIBE is what you reach for once you have stopped trusting the model. Tests `cmd_pages_describe_pageparams_test.go`; the control (explicit-only) reproduces the reported output verbatim. mxcli-formula1 §39 | -| `create or modify odata service` silently revokes the service's access; the next build fails with "At least one allowed role must be selected for the published OData service to be accessible." Re-granting fixes it until the next modify | `serializePublishedODataService` never wrote `AllowedModuleRoles`. The document is serialized wholesale and written with `updateUnit`, so a field the serializer omits is not left alone — it is deleted. The grants were read correctly and carried through the executor, then dropped one layer down | `sdk/mpr/writer_odata.go` (`AllowedModuleRoles`, marker 1 / BY_NAME, matching the working `GRANT` path's `makeMendixStringArray`); stale executor comment corrected in `mdl/executor/cmd_odata.go` | **A wholesale re-serialization deletes every field it does not write, so the writer's field list is a data-retention policy.** Audit it against the parser, not against the struct — the round trip is the contract. **Where you look decides what you conclude**: an earlier pass looked for this loss at model level, found the value present and carried, and recorded "reported but does not reproduce" — the loss only exists after the BSON round trip, so a model-level check could never have seen it. When a report says a value disappears, reproduce at the persistence boundary before disbelieving it. For the marker, copy the shape from the code path that already works (`GRANT` writes marker 1 and builds fine) instead of reasoning from an unrelated type. Tests `writer_odata_test.go`; the control (field omitted again) reproduces the empty-grants document. mxcli-formula1 §26 | -| `.ai-context/skills/` still carries the previous release's guidance after the mxcli binary is upgraded — binary rebuilt at 12:05, skills stamped the day before — with no warning that they disagree | The skills are embedded in the binary and written exactly once, by `mxcli init`. Nothing re-ran init on upgrade, and nothing compared what was on disk against what the binary carried | New `cmd/mxcli/init_skills_sync.go` (`syncAIContextSkills`, `reportSkillSync`), `--sync-skills` flag in `cmd/mxcli/init.go`, and a sync step in the SessionStart bootstrap in `cmd/mxcli/init_hook.go` | **Stale guidance is worse than missing guidance**, because an agent reads it with identical confidence either way — so the failure mode is silent and confident. Fix it where it is consumed, not where it is authored: the SessionStart bootstrap already runs on every session and can fetch the binary, so it is the one place guaranteed to execute immediately before an agent reads the files. Two properties keep an every-session job acceptable: **write only what differs** (an mtime that moves every session makes "when did this last change" unanswerable — a test asserts the mtime holds) and **stay silent when current**. Never fatal: a skills refresh must not block a session, hence `|| true`. A test asserts the bootstrap script actually calls it *before* the exec'd setup, since a step ordered after an `exec` never runs. Tests `init_skills_sync_test.go`; the control (write-once) leaves the stale file in place. mxcli-formula1 §16 | -| `publish entity … (TopSupported: No)` (or `SkipSupported`/`Countable`) parses, `DESCRIBE` reads it back as `No`, and the published `$metadata` still advertises `true`. A client then believes paging works when nothing implements it | `serializeEntitySet` hardcoded all three `QueryOptions` to `true`, ignoring the `*bool` fields the model already carried — the AST/model/DESCRIBE half of the feature shipped without the writer half | `sdk/mpr/writer_odata.go` (`boolOrTrue`, entity-set `QueryOptions`) | **A capability annotation on a microflow-backed resource is load-bearing, not decorative.** Mendix applies *no* query options to a read-microflow resource — it hands the request over and returns what comes back — so the annotation is the only thing a client has to go on, and an over-claim is not cosmetic: the client reads a whole collection believing it is a page. **Check the writer whenever a property round-trips correctly through DESCRIBE**: DESCRIBE reads the model, so a model-only feature describes perfectly and publishes wrong, and the round-trip test everyone reaches for cannot see it. A tri-state `*bool` needs an explicit nil policy at the boundary (`nil` = the platform default, only an explicit `false` opts out) or the pointer is pointless. Tests `writer_odata_test.go`; the control (hardcoded true) reproduces the over-claim. mxcli-formula1 §20 | -| A published OData resource backed by a read microflow silently returns the wrong thing: `?$top=5` yields the whole collection with a 200, and a client re-reading a held row by key gets the collection default and adopts the FIRST row as that object's identity. No error anywhere — well-formed request, valid collection, correct `$count`, 200 | Two promises the service makes on the microflow's behalf and nothing checked: the `KEY` in `expose (…)`, and the `TopSupported`/`SkipSupported` annotations (which default to **true** when unspecified). Mendix applies no query options to a read-microflow resource — it hands over the request and returns what comes back | New `mdl/executor/validate_odata_read_contract.go` (MDL-ODATA02 key promise, MDL-ODATA03 capability over-claim), wired in `cmd/mxcli/cmd_check.go`; the response contract documented in `.claude/skills/mendix/odata-data-sharing.md` | **A read microflow cannot answer 400** — unlike an OData action or an insert/update/delete microflow, the read capability has no `System.HttpResponse` parameter ([docs](https://docs.mendix.com/refguide/published-odata-entity/#custom-http-response)). Its contract is therefore *declarative*: declaring `TopSupported: No` is the read path's only substitute for the refusal it cannot send. **Pick a trigger you can prove**: both rules fire only when the microflow takes no `System.HttpRequest` parameter, because then it provably cannot see a key or a query option; a microflow that does take it gets the benefit of the doubt, since proving *which* options it parses needs real analysis and a rule that guesses gets switched off. **Verify a check rule against a real parse, never a hand-built AST** — the visitor stores `ReadMode` as `MICROFLOW Module.Name` **upper-cased**, so a case-sensitive prefix match made the whole rule dead while it still looked right; the casing is now pinned by its own test. Tests `validate_odata_read_contract_test.go`, examples in `10-odata-examples.mdl` (both correct shapes: request-aware, and honestly declared). mxcli-formula1 §37/§20, suggested issues 2 and 3 | -| No way to see what a running app's subsystem is doing: everything logs at `INFO`, the detail is not in the log at all, and raising the whole runtime to `TRACE` is unusable on a busy app | Nothing in mxcli drove the runtime's per-node log levels, though the M2EE admin API has exposed them all along | New `cmd/mxcli/docker/loglevels.go` (`GetLogSettings`, `SetLogLevels`, `NormalizeLogLevel`, `MatchLogNodes`), `cmd/mxcli/cmd_log.go` (`mxcli log list` / `log set`), documented in `.claude/skills/mendix/analyze-runtime.md` | **Probe an undocumented API before designing the command around it.** Every fact here came from a live 11.12.1 runtime: `get_log_settings` requires one of `node`/`subscriber`/`sort`; `sort` accepts only `node` and `subscriber`; `set_log_level` takes `{"nodes":[{name,level}],"force":bool}`. **The HTTP response for an AdminException says only "See logging output for details" — the actual message ("Please specify node, subscriber or sort option in params", "Unknown sort option: name", "Unknown LogNode X. Use the 'force' parameter…") is in the runtime log**, so probe with the runtime log open or you learn nothing. `force` means "allow a node that does not exist yet" and **permanently registers the name**, so a typo becomes a real empty node — hence it is opt-in, and an unknown node is an error by default. **Resist the subsystem-specific command**: this was proposed as `mxcli odata trace`, but `set_log_level` takes a list of nodes, so the primitive is generic and the OData knowledge belongs in docs. **The node list is a property of the APP, not of Mendix** — a node appears only once something registers it. A first pass concluded "there is no log node for a published OData service" from a project that had none; adding one service turns 57 nodes into 58 and `OData Publish` (with a space) appears, logging the full incoming URI at TRACE — exactly the question that motivated the command. Enumerating capabilities against one sample app and generalising is the trap; `log list` against *this* app is the answer. Distinguish "cannot reach the admin API" from "the runtime refused the request" (`ErrAdminUnreachable`) or the wrong hint buries the real one. Tests `cmd_log_test.go`, `loglevels_test.go`; verified end-to-end against a booted runtime (both arg forms, multi-node, typo refused, `--force` accepted, unreachable port). mxcli-formula1 suggested issue 4 | -| A headless browser cannot log in to an app started with `--hub`, so every screenshot silently shows the login page and rendering defects survive every other check | Under `--hub` the runtime boots with the public **https** root URL, so it marks session cookies `Secure` and prefixes them `__Host-`; a browser on a non-trustworthy http origin cannot store them | `cmd/mxcli/docker/screenshot_login.go` — the login browser context declares `X-Forwarded-Proto: http` when the target is http (Mendix 10.24+ lets that header override `ApplicationRootUrl`) | **The reported cause did not reproduce as stated, and saying so is part of the fix.** On 11.12.1 an https root URL does *not* block a headless browser on `127.0.0.1`: loopback is a **trustworthy origin**, so Chromium accepts `Secure`/`__Host-` cookies there — the app rendered clean, no console errors, no failed requests. The mechanism is real only for a **non-loopback** http origin (a container hostname, a LAN address). Fix shipped anyway because the header is *accurate* rather than a workaround (the request genuinely is http), it costs nothing on an already-http root URL, and real users over https are unaffected. Verified at the layer the bug lives in: the captured Playwright storage state goes from `__Host-XASSESSIONID(secure=true)` to `XASSESSIONID(secure=false)`. **`curl` cannot see this class of bug and neither can a loopback browser** — when a report blames cookie flags, check whether the origin is trustworthy before believing the flags are the blocker. mxcli-formula1 §38 / suggested issue 7 | -| A `--` comment written between two operands of a Mendix expression ends up **inside** the expression; the build fails **CE0117** "Error(s) in expression". `mxcli check` passes and `DESCRIBE` round-trips the comment, so nothing before mxbuild objects | `extractOriginalText` reads the raw input stream between two token positions — which is exactly what preserves an expression's spacing, and also drags in every token the lexer sent to a hidden channel. MDL's `--` and `/* */` are `-> skip`, so they never appear in `ctx.GetText()` but always appear in the source slice | `mdl/visitor/visitor_helpers.go` (`stripMDLComments`, `extractExpressionText`), applied at the six microflow-expression sites in `visitor_microflow_statements.go` / `visitor_microflow_actions.go` | **The ANTLR trap: `ctx.GetText()` excludes hidden tokens, a source-interval slice includes them.** Any code reaching for original text to preserve formatting inherits every comment in that span. **Replace a comment with whitespace, never with nothing** — `1 --c\n+ 2` must not become `1+ 2`, and `'a'--c\n'b'` must not weld into one token. **Respect single-quoted strings**: a Mendix string may legitimately contain `--` or `/*`, and stripping those corrupts the value (tested both, plus the `''` escape that keeps a string open). **Left OQL alone on purpose** — `visitor_entity.go` uses the same helper for view-entity queries, where `--` is legitimate SQL comment syntax; stripping it would change a different language's meaning. **The unit test alone would not have caught a wiring mistake**: it still passed with `extractExpressionText` bypassed, and only the mxbuild run (1 error → 0, same script, same project) proved the call sites were converted. Tests `visitor_strip_comments_test.go`, repro `mdl-examples/bug-tests/comment-in-expression.mdl`. mxcli-formula1 §34 / suggested issue 11 | -| A published OData entity with no `KEY` fails the build with **CE6585** "Published entity 'X' must have a key defined." — so any advice of the form "drop the KEY" is impossible to follow | Mendix requires every published entity to have a key. MDL-ODATA02's suggestion offered "…or drop the KEY" as the alternative to answering a key lookup, and a doctype example demonstrated that non-existent option | `mdl/executor/validate_odata_read_contract.go` (suggestion text), `mdl-examples/doctype-tests/10-odata-examples.mdl`, `.claude/skills/mendix/odata-data-sharing.md` | **Query options you may decline; the key you may not.** A microflow-backed resource whose rows a client can hold *must* answer the key lookup — there is no opt-out, which makes MDL-ODATA02's real remedy singular rather than a choice. **Verify the remedy a diagnostic recommends, not just the diagnosis** — the rule correctly identified an unanswerable KEY and then proposed something mxbuild rejects, which is worse than saying nothing. This is the second time in one session that a doctype example was validated with `mxcli check` (parse-only) instead of the integration gate; `mxcli check` cannot see CE-codes at all, so **any change to `mdl-examples/doctype-tests/` needs `go test -tags integration -run TestMxCheck_DoctypeScripts/