From d4e9ebce46852ad84e84e33bd764f578a721fbe7 Mon Sep 17 00:00:00 2001 From: Ako Date: Tue, 1 Sep 2026 07:40:47 +0000 Subject: [PATCH 1/5] docs(wiki): digest the cmd/mxcli findings into five bug-pattern pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 87 of the 95 cmd/mxcli findings (92%) now belong to a named class. This is the tooling surface rather than the model, and the classes look nothing like the executor's. - test-runner-cannot-fail (14). The one class whose cost is confidence rather than debugging time. `@expect 1 = 2` passed; `@verify` was parsed and read by nothing but --list; --require-assertions exited 0 on a suite that asserted nothing. The mechanism is silent ABSENCE — an annotation parsed into a field no consumer reads — amplified by two result-assembly paths. The remedies that held are structural: fail closed on an annotation that cannot be honoured, one constructor and one pre-run verdict function, each pinned to a single call site. - local-loop-silence (16). The warm loop orchestrates mxbuild, a JVM, a bundler, PostgreSQL and a browser, so the thing that breaks is not the thing that reports: a missing client bundle answers HTTP 200, a dead runtime leaves the CLI spinning, and the log being captured is not the log the user needs. - styling-compiles-to-nothing (11). Nothing validates CSS. Location decides whether SCSS compiles at all, an unrecognised token is indistinguishable from a design never applied, and contrast is a correctness property no check measures. - package-operations-damage (11). Handing the project to tools mxcli does not control, and being told it went fine — the MPR v2 to v1 collapse reporting 0 errors is the worst of them. - cli-contract-defects (14). The class with no Mendix document in it. It matters disproportionately because agents take the tool's word: help that teaches unparseable syntax, an unqualified "Check passed!" that resolved nothing, a flag that parses and does nothing. Every sources: path and [[wiki-link]] verified before committing. Seed table extended and five rows appended to SYNC_LOG.md. Co-Authored-By: Claude Opus 5 --- .claude/skills/maintain-wiki.md | 5 ++ CLAUDE.md | 2 +- docs-wiki/SYNC_LOG.md | 5 ++ .../bug-patterns/cli-contract-defects.md | 64 +++++++++++++++ docs-wiki/bug-patterns/local-loop-silence.md | 76 +++++++++++++++++ .../bug-patterns/package-operations-damage.md | 66 +++++++++++++++ .../styling-compiles-to-nothing.md | 62 ++++++++++++++ .../bug-patterns/test-runner-cannot-fail.md | 82 +++++++++++++++++++ 8 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 docs-wiki/bug-patterns/cli-contract-defects.md create mode 100644 docs-wiki/bug-patterns/local-loop-silence.md create mode 100644 docs-wiki/bug-patterns/package-operations-damage.md create mode 100644 docs-wiki/bug-patterns/styling-compiles-to-nothing.md create mode 100644 docs-wiki/bug-patterns/test-runner-cannot-fail.md diff --git a/.claude/skills/maintain-wiki.md b/.claude/skills/maintain-wiki.md index fbb261879..86f092ba7 100644 --- a/.claude/skills/maintain-wiki.md +++ b/.claude/skills/maintain-wiki.md @@ -152,6 +152,11 @@ isn't better served by an existing page or a different doc artifact. | `bug-patterns/rewrite-drops-unauthored-state.md` | bug-pattern | CREATE OR REPLACE losing what the statement did not mention | | `bug-patterns/flow-graph-geometry.md` | bug-pattern | generated microflow coordinates and sequence-flow wiring | | `bug-patterns/integration-contract-drift.md` | bug-pattern | OData / REST / mappings, where neither check nor mxbuild is an oracle | +| `bug-patterns/test-runner-cannot-fail.md` | bug-pattern | `mxcli test` reporting PASS for what did not hold, run or get evaluated | +| `bug-patterns/local-loop-silence.md` | bug-pattern | the warm loop failing quietly across processes mxcli does not own | +| `bug-patterns/styling-compiles-to-nothing.md` | bug-pattern | SCSS and tokens written correctly, compiled nowhere, checked by nothing | +| `bug-patterns/package-operations-damage.md` | bug-pattern | marketplace / `mx` operations that change the project and report success | +| `bug-patterns/cli-contract-defects.md` | bug-pattern | flags, paths, stdout and help — the class with no Mendix document in it | ## Adding a new page diff --git a/CLAUDE.md b/CLAUDE.md index ede6c1467..6efa2829e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -564,7 +564,7 @@ mxcli uses a layered documentation system — each artifact type has a single ca **ADRs are immutable once accepted.** Supersede with a new ADR rather than editing in place. Conventions and template in [`docs/13-decisions/README.md`](docs/13-decisions/README.md). -**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest covers `mdl/executor` (12 pattern pages, 83% of that area's findings) and little else; `make digest-status` reports the gap per area, so a pattern miss means "not yet digested", not "not seen before". +**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest covers `mdl/executor` (83% of its findings) and `cmd/mxcli` (92%) across 17 pattern pages, and little else; `make digest-status` reports the gap per area, so a pattern miss means "not yet digested", not "not seen before". **The wiki is synthesized, not stated.** It frames and connects across the other artifacts — it never restates content that has a canonical home. Rules and seed page list in [`.claude/skills/maintain-wiki.md`](.claude/skills/maintain-wiki.md). diff --git a/docs-wiki/SYNC_LOG.md b/docs-wiki/SYNC_LOG.md index b99fa30a6..17f92daef 100644 --- a/docs-wiki/SYNC_LOG.md +++ b/docs-wiki/SYNC_LOG.md @@ -40,3 +40,8 @@ not capture, because sources are upstream of the commit. | 2026-08-31 | bug-patterns/rewrite-drops-unauthored-state.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (13 records), docs/13-decisions/0005-semantic-model-interface-currency.md, docs/13-decisions/0008-identity-and-idempotence.md | New page (added to seed table). Guard-don't-drop as a failure class rather than a decision: the loss is invisible at every checkpoint, the most expensive variant is identity rather than content, and delete-then-create opts out of every protection | | 2026-08-31 | bug-patterns/flow-graph-geometry.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (10 records), mdl/executor/layout.go, mdl/executor/cmd_microflows_builder_control.go | New page (added to seed table). A microflow is a graph with coordinates and mxcli generates both; geometry fails ugly, wiring fails invalid, and a flow that looks redundant is often load-bearing | | 2026-08-31 | bug-patterns/integration-contract-drift.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (30 records), mdl/executor/cmd_odata.go | New page (added to seed table). The one area where neither mxcli check nor mxbuild is an oracle, so a silent downgrade can return HTTP 200; defaults, EDM mapping and lagging modify paths | +| 2026-08-31 | bug-patterns/test-runner-cannot-fail.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (14 records), cmd/mxcli/testrunner/parser.go, cmd/mxcli/testrunner/generator_endpoint.go | New page (added to seed table). First pass over cmd/mxcli. The one class whose cost is confidence rather than debugging time: silent absence (an annotation parsed and read by nothing), two result-assembly paths, and why the remedy is fail-closed plus a single call site | +| 2026-08-31 | bug-patterns/local-loop-silence.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (16 records), cmd/mxcli/docker/localboot.go, cmd/mxcli/docker/runlocal.go | New page (added to seed table). The warm loop orchestrates processes mxcli did not write, so the thing that breaks is not the thing that reports — a missing bundle answers 200, a dead child does not stop the parent, and every resolved-for-another-tool value is a silent disagreement | +| 2026-08-31 | bug-patterns/styling-compiles-to-nothing.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (11 records), cmd/mxcli/theme/block.go | New page (added to seed table). No validator exists for CSS, so location decides whether anything compiles, an unread token is indistinguishable from a design not applied, and contrast is a correctness property nothing measures | +| 2026-08-31 | bug-patterns/package-operations-damage.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (11 records), cmd/mxcli/marketplace/update.go | New page (added to seed table). Damage from handing the project to tools mxcli does not control, reported as success — the v2 to v1 collapse at 0 errors being the worst of it | +| 2026-08-31 | bug-patterns/cli-contract-defects.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (14 records), cmd/mxcli/syntax/ | New page (added to seed table). The class with no Mendix document in it: help that teaches unparseable syntax, unqualified success messages, accepted-but-inert flags, stdout as a data channel. Matters disproportionately because agents take the tool's word | diff --git a/docs-wiki/bug-patterns/cli-contract-defects.md b/docs-wiki/bug-patterns/cli-contract-defects.md new file mode 100644 index 000000000..80f95ca85 --- /dev/null +++ b/docs-wiki/bug-patterns/cli-contract-defects.md @@ -0,0 +1,64 @@ +--- +title: When the CLI Is the Defect +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/cmd-mxcli.jsonl + - cmd/mxcli/syntax/ +--- + +> **Do not duplicate**: individual flags, paths and messages live in the CLI help +> and the findings. This page is about the class where nothing is wrong with the +> model at all. + +## What this is + +Roughly fourteen `cmd/mxcli` findings are defects in the tool's own contract with +its caller — the flags it accepts, the paths it resolves, what it writes to +stdout, and what its help teaches. No Mendix document is involved. They matter +disproportionately because mxcli is driven by agents as often as by people, and +an agent takes the tool's word for everything. + +## How it fits + +**Help that teaches syntax the parser rejects is worse than no help.** `mxcli +syntax` documented spellings that fail to parse, so an agent following the +reference produced MDL that could not run. The mirror image is just as expensive: +a widget keyword the grammar accepts but the reference omits was concluded not to +exist and worked around at length — two days for a construct that already +existed. A syntax reference is an API surface and drifts like one. + +**An unqualified success message is a claim.** `check` printed `Check passed!` +having resolved nothing against the project; a misspelled entity or page name +sailed through the command whose entire job is to catch it. Where a command's +depth depends on what it was given, the message has to say which check ran. + +**Accepted-but-inert flags.** `--require-assertions` parsed, appeared in `--help`, +and was consulted in one of two code paths. A flag the tool advertises is a +promise, and the cheapest guard is that its effect has exactly one call site. + +**stdout is a data channel.** `--format json | jq` failed because progress lines +shared the stream. Anything with a machine-readable format has to keep +diagnostics on stderr. + +**Path handling is where portability bugs live.** Windows backslashes mangled by +escape processing, a relative `-p` rejected by a downstream tool with its own +error text, a test directory resolved relative to the wrong base, `-` not +accepted for stdin so MDL cannot be piped. Each is small and each stops a +workflow completely. + +**Say what was skipped and why.** "Found 0 test(s) in 1 file(s)" counted a +directory as a file, so it read as though a file had been opened and rejected. +Naming the files that were skipped — and why — turns a dead end into a rename. + +**Shipped guidance is code.** The skills and skill packs mxcli embeds are +compiled in and versioned with the binary, so they go stale against it, and a +malformed one ships as-is if the test only checks that a file exists. Assert on +the rendered content, not on presence. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + flags, paths and messages +- [[test-runner-cannot-fail]] — the same "accepted but inert" shape, where its + consequence is a false pass diff --git a/docs-wiki/bug-patterns/local-loop-silence.md b/docs-wiki/bug-patterns/local-loop-silence.md new file mode 100644 index 000000000..13e97e0a7 --- /dev/null +++ b/docs-wiki/bug-patterns/local-loop-silence.md @@ -0,0 +1,76 @@ +--- +title: The Warm Loop Fails Quietly +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/cmd-mxcli.jsonl + - cmd/mxcli/docker/localboot.go + - cmd/mxcli/docker/runlocal.go +--- + +> **Do not duplicate**: the flags and their behaviour live in +> `.claude/skills/mendix/run-local.md`; the individual fixes live in the +> findings. This page describes the shape of failure in a loop mxcli only partly +> owns. + +## What this is + +`mxcli run --local` orchestrates processes it did not write: mxbuild, a JVM +runtime, a rollup bundler, PostgreSQL, and a browser. Sixteen `cmd/mxcli` +findings are failures in that loop, and they share a signature — **the thing that +breaks is not the thing that reports**. A page goes black with HTTP 200. The +runtime is gone and the CLI sits at several hundred percent CPU. Styles do not +change and nothing is stale. A login that is correct reports "Sign in failed". + +## How it fits + +**A missing asset answers 200.** Mendix serves an SPA shell before it knows +whether the client bundle exists, so a deleted `deployment/web/dist` renders a +black screen while every HTTP-level check passes. `curl /` is not a liveness +probe for this app; the bundle is. + +**A dead child does not stop the parent.** A runtime that exited left a zombie +under a CLI that kept polling, for hours. Anything that supervises a long-lived +child needs to reap it and exit, or the failure presents as a hang rather than an +error. + +**The log the user needs is not the log being captured.** Several rounds went +into `runtime.log` holding only the JVM banner: the application's own logging +goes through a subscriber that attaches after startup, so anything logged +*during* the start action — which is exactly where the after-startup test runner +logged — is not in the file. Capturing "the process's stdout" and capturing "the +app's log" are different jobs. + +**Everything the loop resolves for another tool is a place to disagree +silently.** A relative `-p` reaches mxbuild as its own error text plus a page of +Windows JSON. A version resolved for one component and defaulted in another +produces a project at a Mendix version nobody asked for. An architecture picked +for `docker build` and not for `--local` gives `exec format error`. A credential +that is right for one API is wrong for the other — the M2EE admin password and +the test endpoint token are different secrets, and passing one where the other +belongs reports "Authentication failed" after the model has already been +modified. + +**Guards that name a cause can name the wrong one.** A port-in-use guard blamed a +previous `run --local` that the user had already killed. A guard is a diagnosis, +and a wrong diagnosis costs more than a plain error — this is why the port owner +is now resolved from `/proc` rather than asserted. + +**Stale state has more than one home.** A theme edit that does not appear, a +constant that does not reach a running app, a page that dies after ten +hot-applies with a 404 for a hashed chunk — each is a cache or a running process +holding something the file no longer says. The loop's value is that it does not +restart; the cost is that "what is actually being served" and "what is on disk" +are separate questions, and the answer has to be printed rather than assumed. + +**Half-applied is a state the loop can reach.** A watch that redeploys while an +`mxcli exec` is mid-script can serve a partial model, and re-running the script +fixes nothing because the write is idempotent and there is nothing left to write. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + probes, guards and controls +- `.claude/skills/mendix/run-local.md` — what the flags actually do +- `.claude/skills/verify-in-runtime.md` — proving a fix in a real browser, which + is the only check that sees most of this class diff --git a/docs-wiki/bug-patterns/package-operations-damage.md b/docs-wiki/bug-patterns/package-operations-damage.md new file mode 100644 index 000000000..1c73469c4 --- /dev/null +++ b/docs-wiki/bug-patterns/package-operations-damage.md @@ -0,0 +1,66 @@ +--- +title: Package Operations That Damage the Project +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/cmd-mxcli.jsonl + - cmd/mxcli/marketplace/update.go +--- + +> **Do not duplicate**: the marketplace command surface lives in the CLI help and +> `PROPOSAL_marketplace_module_upgrade.md`; the per-operation repairs live in the +> findings. This page describes the shared hazard. + +## What this is + +Installing or updating a marketplace module, or running `mx update-widgets`, +hands the project to a tool mxcli does not control — and several of those tools +change things nobody asked them to. Eleven `cmd/mxcli` findings are damage from +that hand-off, and the damage is usually reported as success. + +## How it fits + +**The v2 → v1 collapse is the worst of them.** Mendix's own tooling rewrites an +MPR v2 project (`.mpr` plus a `mprcontents/` tree) into a single-file v1 +database. `mx check` afterwards reports **0 errors** and looks like a clean run; +what has actually happened is that `mprcontents/` is gone, the `.mpr` has grown +from tens of kilobytes to tens of megabytes, and per-document diffing no longer +works. Nothing about the output says so. mxcli's repair commands exist because of +this: let the tool convert, read the units back, restore v2, and write only the +changed ones through mxcli's own writer. + +**Version numbers are not identity.** Matching an installed module against a +marketplace release by version *number* is ambiguous — a blank project ships two +different modules that both published a 4.1.0. The module's `AppStoreGuid` is the +version UUID and is the thing to match on. Getting this wrong produces +"version not found" for a module the project demonstrably has. + +**An update can move a dependency backwards.** Installing one module rolled a +project's widgets from 3.11.3 to 3.4.0 because an unrelated package bundled an +older copy, and `mx check` stayed clean throughout. Anything that copies bundled +files needs to compare versions rather than overwrite, and to *report* what it +skipped — "the package wanted a different version" is exactly the fact that was +invisible. + +**A model that builds can still be running the old code.** After an update the +model referenced new widget definitions while `widgets/` held the old binaries; +`show modules` reported the new version. The model and the deployed artefacts are +separate, and only one of them is what `mx check` inspects. + +**Report what could not be determined, never "unchanged".** Drift detection +compares an installed module against a freshly imported reference, and an element +that cannot be described is reported **unknown**. Reporting it as unchanged would +turn a gap in coverage into a claim about the user's project — and the same +report also has to say when "no modifications found" is not a conclusion. + +**Environment failures masquerade as project corruption.** A path too long, an +`.mpk` built by a newer Studio Pro, a `mx` invocation resolving to a different +cached version — each leaves output that looks like a broken install. The +remedies are mostly diagnosis: say which version, which path, which binary. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + repairs and their measurements +- [[mpr-read-write]] — what v1 and v2 actually are, and why the collapse matters +- `PROPOSAL_marketplace_module_upgrade.md` — the drift-detection design diff --git a/docs-wiki/bug-patterns/styling-compiles-to-nothing.md b/docs-wiki/bug-patterns/styling-compiles-to-nothing.md new file mode 100644 index 000000000..fc9612484 --- /dev/null +++ b/docs-wiki/bug-patterns/styling-compiles-to-nothing.md @@ -0,0 +1,62 @@ +--- +title: Styling That Compiles to Nothing +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/cmd-mxcli.jsonl + - cmd/mxcli/theme/block.go +--- + +> **Do not duplicate**: where SCSS compiles and the Atlas layering rules are +> canonical in CLAUDE.md; the theme commands are in the skills. This page +> describes why the failures are invisible. + +## What this is + +Eleven `cmd/mxcli` findings are styling that was written correctly, built +successfully, and had no effect — or had an effect nobody could read. There is no +validator for CSS. `mx check` says nothing, the build succeeds, and the rules are +simply absent from the compiled stylesheet, which in a browser is +indistinguishable from a specificity problem. + +## How it fits + +**Location decides whether anything compiles at all.** mxbuild walks the model's +modules to find theme sources; it never globs the directory. SCSS written to a +folder that does not match a real module is skipped without a warning. The +same applies in reverse to files imported once per module, where a rule — as +opposed to a declaration — is emitted N times. + +**A token nobody reads is indistinguishable from a design not applied.** Writing +an unrecognised `--mxt-*` name produces a theme that applies cleanly and renders +exactly as before. That is why an unknown token is refused rather than written: +the alternative is a success message for a no-op. + +**Contrast is a correctness property, and nothing measures it.** Several findings +are text at 1.02:1 or 1.13:1 — invisible, and reported as "contrast is low" +rather than as a defect in a specific rule. Two Atlas rules assume a dark +navigation rail and paint topbar text with a fixed colour, so a theme that +lightens the rail loses its own labels. A literal colour outside the palette +survives a theme swap and is wrong under every theme but one. + +**The compile-time failures are outside Go's reach.** A Sass variable holding a +selector must be a quoted string; a bare selector is not a Sass expression, and +the failure happens in mxbuild's SCSS compiler where no Go test and no `mx check` +can see it. The only signal is the build log of a real build. + +**Writing into files the project already owns needs a fence, not a rewrite.** +`theme/web/main.scss` is Mendix's own three lines and `custom-variables.scss` +carries Atlas defaults, so mxcli replaces only a digest-fenced region and refuses +a block a human has edited. The record lives in the file rather than in sidecar +state, which is what keeps it from drifting — and it is the same guard-don't-drop +rule as [[rewrite-drops-unauthored-state]], applied to files instead of BSON. +Its failure mode is the mirror image: a project that has silently taken ownership +of a file, reported as `skipped` forever after. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + selectors, tokens and measurements +- [[rewrite-drops-unauthored-state]] — the same guard, applied to model documents +- `.claude/skills/verify-in-runtime.md` — the only way most of this class is + observable diff --git a/docs-wiki/bug-patterns/test-runner-cannot-fail.md b/docs-wiki/bug-patterns/test-runner-cannot-fail.md new file mode 100644 index 000000000..fa4f18647 --- /dev/null +++ b/docs-wiki/bug-patterns/test-runner-cannot-fail.md @@ -0,0 +1,82 @@ +--- +title: A Test Runner That Cannot Fail +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/cmd-mxcli.jsonl + - cmd/mxcli/testrunner/parser.go + - cmd/mxcli/testrunner/generator_endpoint.go +--- + +> **Do not duplicate**: the annotation reference and the `.test.mdl` format live +> in `.claude/skills/test-microflows.md` and `docs/15-testing/`; the individual +> fixes live in the findings. This page describes why the class is uniquely +> dangerous. + +## What this is + +Fourteen `cmd/mxcli` findings are `mxcli test` reporting **PASS** for something +that did not hold, or did not run, or was never evaluated. `@expect 1 = 2` +passed. `@verify` — the annotation covering the harder half of Mendix testing, +where a microflow's only observable effect is rows written — was parsed and read +by nothing but `--list`. `--require-assertions` exited 0 on a suite where nothing +asserted. A test with both `@throws` and `@expect` reported two assertions and +made one. + +Every other class in this wiki costs a debugging session. This one **spends +confidence that was never earned**: green output is the entire product, and a +runner that cannot fail is worse than no runner, because a real suite gets +written against it and believed. + +## How it fits + +**Silent absence, not silent breakage.** The recurring mechanism is not a wrong +answer but a missing consumer. An annotation is parsed into a struct field, and +nothing downstream reads it. `grep -n '\.Verify'` over the package was the whole +diagnosis. The parse succeeding is what makes it invisible: `--list` prints the +annotation back, so the feature looks wired. + +**Two result-assembly paths, and a decision that lives in one of them.** The +runner has two backends — the after-startup runner and the HTTP test endpoint — +and results were assembled separately in each. A flag consulted in one loop, a +field populated at one of five `TestResult` literal sites, a verdict computed in +the endpoint path only: each is the [[duplicate-resolver-drift]] shape landing +where its consequence is a false pass. The remedy that held was structural — one +constructor, one pre-run verdict function — pinned by a test asserting it is the +*only* call site. + +**Fail closed.** An annotation that claims to assert something and cannot be +honoured is now an **error**, and the test does not run. That is the opposite of +the instinct (drop what you cannot handle and carry on), and it is the only +policy under which a passing report means anything. + +**The default output has to distinguish an assertion from a silence.** A test +that asserted nothing printed the same `PASS` as one with six assertions. Putting +the count behind `--verbose` would have missed the point: the *unread* output is +where the false confidence lives. + +**Substring where structure was needed.** Two defects came from searching text +instead of parsing it. Unanchored annotation patterns matched inside prose, so a +doc comment *explaining* `@expect $x = 1` gave the test that assertion — found +while writing the reproduction for another bug, whose header caused it. And a +line-number estimate searched reconstructed text inside the raw bytes, which +cannot match under CRLF and panicked with a negative slice bound. + +**The environment is part of the assertion.** A suite that passes under +`--attach` and fails under `--local` is usually neither: the two boot paths +differed in whether constant overrides were applied and whether the project's own +after-startup microflow was chained. Nothing in either output explained the +difference. Both are now printed, and both boot paths share one options mapping. + +**A test run must not change the project.** The runner injects a module and takes +it back out; restoring the model still moved the `.mpr`'s bytes, so a +"tests pass and the tree is clean" CI step failed on a meaningless diff. Snapshot +and restore the file, and refuse to restore when cleanup failed — a tree that +reads clean while the project is not is worse than a visible discrepancy. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — each annotation, + its consumer, and the control that proves it can now fail +- [[duplicate-resolver-drift]] — the same structural cause, in the executor +- `.claude/skills/test-microflows.md` — the annotation reference itself From 5f103a053fd210ae556cccb898a538e3919f1925 Mon Sep 17 00:00:00 2001 From: Ako Date: Tue, 1 Sep 2026 08:12:12 +0000 Subject: [PATCH 2/5] docs(wiki): digest the mdl/backend findings into three bug-pattern pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 82 of the 83 mdl/backend findings now fall under a named class. Only three pages are new: most of the area reuses classes written for mdl/executor and cmd/mxcli, which is the digest working rather than a shortcut. - engine-divergence (30). The dominant class here. Two implementations behind one interface, with the newer, less complete one as the default — so a gap is the behaviour most users get while tests formed against legacy still pass. A gap on one engine is invisible from inside that engine: everything is self-consistent, and the field that never existed is never missed. The cross-engine DESCRIBE matrix (write with A, read with B, all four) is the only check that sees it. Two failure modes and only one is honest — a refusal naming MXCLI_ENGINE=legacy costs a flag, a `-- Empty action` placeholder makes describe→exec delete the construct. The worst instance was a read that under-reported page access. - mutator-addressing (19). ALTER edits a stored document in place, which means naming a node — and DataGrid2 columns, object-list items and layout regions store no Name at all. Derived names are unstable, ambiguity is refused rather than resolved, and hand-built BSON drifts from codec-built BSON in ways only a dump-diff shows. Getting a typed-array marker wrong turns a silent no-op into an unopenable project. - access-rule-reconciliation (6). Small and graded high: GRANT looks additive and is a read-modify-write, so both widening and narrowing report success. Records two method points — the writer was innocent and a later reconcile removed the rules, and the reported trigger (WHERE) was a red herring that a scoped fix would have satisfied. The ~33 MCP/PED findings are deliberately NOT given a page: that subsystem already has architecture/mcp-backend.md and models/ped-mutation-constraints.md, and restating them would break the never-restate rule. Recorded as its own SYNC_LOG row so the omission reads as a decision rather than an oversight. Co-Authored-By: Claude Opus 5 --- .claude/skills/maintain-wiki.md | 3 + CLAUDE.md | 2 +- docs-wiki/SYNC_LOG.md | 4 + .../access-rule-reconciliation.md | 62 +++++++++++++++ docs-wiki/bug-patterns/engine-divergence.md | 77 +++++++++++++++++++ docs-wiki/bug-patterns/mutator-addressing.md | 74 ++++++++++++++++++ 6 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 docs-wiki/bug-patterns/access-rule-reconciliation.md create mode 100644 docs-wiki/bug-patterns/engine-divergence.md create mode 100644 docs-wiki/bug-patterns/mutator-addressing.md diff --git a/.claude/skills/maintain-wiki.md b/.claude/skills/maintain-wiki.md index 86f092ba7..60fffc093 100644 --- a/.claude/skills/maintain-wiki.md +++ b/.claude/skills/maintain-wiki.md @@ -157,6 +157,9 @@ isn't better served by an existing page or a different doc artifact. | `bug-patterns/styling-compiles-to-nothing.md` | bug-pattern | SCSS and tokens written correctly, compiled nowhere, checked by nothing | | `bug-patterns/package-operations-damage.md` | bug-pattern | marketplace / `mx` operations that change the project and report success | | `bug-patterns/cli-contract-defects.md` | bug-pattern | flags, paths, stdout and help — the class with no Mendix document in it | +| `bug-patterns/engine-divergence.md` | bug-pattern | two backend implementations, and a gap in one that is invisible from inside it | +| `bug-patterns/mutator-addressing.md` | bug-pattern | in-place edits to nodes the model does not name | +| `bug-patterns/access-rule-reconciliation.md` | bug-pattern | GRANT is a read-modify-write, and both directions of loss report success | ## Adding a new page diff --git a/CLAUDE.md b/CLAUDE.md index 6efa2829e..5bb0ca61f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -564,7 +564,7 @@ mxcli uses a layered documentation system — each artifact type has a single ca **ADRs are immutable once accepted.** Supersede with a new ADR rather than editing in place. Conventions and template in [`docs/13-decisions/README.md`](docs/13-decisions/README.md). -**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest covers `mdl/executor` (83% of its findings) and `cmd/mxcli` (92%) across 17 pattern pages, and little else; `make digest-status` reports the gap per area, so a pattern miss means "not yet digested", not "not seen before". +**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest covers `mdl/executor` (83% of its findings), `cmd/mxcli` (92%) and `mdl/backend` (99%) across 20 pattern pages; `make digest-status` reports the gap per area, so a pattern miss means "not yet digested", not "not seen before". **The wiki is synthesized, not stated.** It frames and connects across the other artifacts — it never restates content that has a canonical home. Rules and seed page list in [`.claude/skills/maintain-wiki.md`](.claude/skills/maintain-wiki.md). diff --git a/docs-wiki/SYNC_LOG.md b/docs-wiki/SYNC_LOG.md index 17f92daef..55d25d60a 100644 --- a/docs-wiki/SYNC_LOG.md +++ b/docs-wiki/SYNC_LOG.md @@ -45,3 +45,7 @@ not capture, because sources are upstream of the commit. | 2026-08-31 | bug-patterns/styling-compiles-to-nothing.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (11 records), cmd/mxcli/theme/block.go | New page (added to seed table). No validator exists for CSS, so location decides whether anything compiles, an unread token is indistinguishable from a design not applied, and contrast is a correctness property nothing measures | | 2026-08-31 | bug-patterns/package-operations-damage.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (11 records), cmd/mxcli/marketplace/update.go | New page (added to seed table). Damage from handing the project to tools mxcli does not control, reported as success — the v2 to v1 collapse at 0 errors being the worst of it | | 2026-08-31 | bug-patterns/cli-contract-defects.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (14 records), cmd/mxcli/syntax/ | New page (added to seed table). The class with no Mendix document in it: help that teaches unparseable syntax, unqualified success messages, accepted-but-inert flags, stdout as a data channel. Matters disproportionately because agents take the tool's word | +| 2026-08-31 | bug-patterns/engine-divergence.md | .claude/skills/fix-issue/findings/mdl-backend.jsonl (30 records), mdl/backend/modelsdk/microflow.go, docs/13-decisions/0004-full-codec-engine.md | New page (added to seed table). First pass over mdl/backend. A gap on one engine is invisible from inside that engine, so the cross-engine DESCRIBE matrix is the only check that finds them; a refusal is honest and a `-- Empty action` placeholder is not; read against the WRITER's keys rather than gen's accessors | +| 2026-08-31 | bug-patterns/mutator-addressing.md | .claude/skills/fix-issue/findings/mdl-backend.jsonl (19 records), mdl/backend/mpr/page_mutator.go, mdl/backend/modelsdk/page_write.go | New page (added to seed table). Many addressable nodes — DataGrid2 columns, object-list items, layout regions — store no Name, so addressing is derived or positional; ambiguity is refused rather than resolved, and hand-built BSON drifts from codec-built BSON in ways only a dump-diff shows | +| 2026-08-31 | bug-patterns/access-rule-reconciliation.md | .claude/skills/fix-issue/findings/mdl-backend.jsonl (6 records), mdl/backend/modelsdk/domainmodel_security_write.go | New page (added to seed table). Small class graded high: GRANT is a read-modify-write and both directions of loss report success. Records that the writer was innocent in the instructive case — a later reconcile removed the rules — and that the reported trigger (WHERE) was a red herring | +| 2026-08-31 | (no page) mdl/backend MCP findings | .claude/skills/fix-issue/findings/mdl-backend.jsonl | Deliberately NOT given a page: ~33 records touch the MCP/PED backend, which already has architecture/mcp-backend.md and models/ped-mutation-constraints.md. Restating them here would violate the never-restate rule; the findings link to those pages instead | diff --git a/docs-wiki/bug-patterns/access-rule-reconciliation.md b/docs-wiki/bug-patterns/access-rule-reconciliation.md new file mode 100644 index 000000000..34181c2bb --- /dev/null +++ b/docs-wiki/bug-patterns/access-rule-reconciliation.md @@ -0,0 +1,62 @@ +--- +title: Access Rules Are Reconciled, Not Appended +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-backend.jsonl + - mdl/backend/modelsdk/domainmodel_security_write.go +--- + +> **Do not duplicate**: the GRANT syntax and the security workflow live in +> `.claude/skills/manage-security.md`; the CE numbers and per-member rules live +> in the findings. This page describes why this small class is graded high. + +## What this is + +`GRANT` looks additive and is not. A grant is stored as an *entity access rule* +carrying a complete picture of what a role may do, so writing one is a +read-modify-write over an existing rule — and five `mdl/backend` findings are +that reconciliation losing rights the statement never mentioned. + +The class is small and worth its own page because of what it costs when wrong. +Every other silent loss in this wiki costs a feature; this one **quietly widens +or narrows who can read data**, and both directions are reported as success. + +## How it fits + +**A grant that mentions one attribute must not revoke the others.** The reported +symptom was attributes granted by an earlier statement coming back as `None`. +Structural rights — create, delete, the default member access — went with them, +which the report did not mention: **re-derive the blast radius rather than +inheriting it from the reporter**. + +**The reported trigger is often not the trigger.** A constrained (`WHERE`) grant +was blamed, and the same loss reproduced with no `WHERE` and with `READ *`. A fix +scoped to the reported path would have passed the reporter's reproduction and +left most of the defect in place. + +**When a value is written and then absent, instrument the later pass.** In the +most instructive of these the writer was innocent — it stored all three rules — +and a reconciliation running afterwards removed them. Starting at the writer is +the natural instinct and the wrong end. + +**Preserve what cannot be checked.** Membership questions do not all resolve +locally: an association is qualified by the module that *declares* it, so a +specialization inheriting from a generalization in another module has members the +local walk cannot see. Dropping what the walk cannot confirm produced +`CE0066 "Entity access is out of date"` — the model claiming rights over members +it no longer lists. The safe default is to carry an unconfirmable member through +rather than to prune it. + +**Under-reporting access is the read-side twin.** A restricted page reported as +having "no roles" is the same class seen from the query side, and it is the shape +most likely to be believed, because "no roles" reads like a finding rather than a +gap. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the member + walks, the CE numbers and the controls +- [[association-pointers]] — why a member belongs to the FROM entity, which + decides where a MemberAccess may appear +- [[engine-divergence]] — where the "no roles" read gap came from diff --git a/docs-wiki/bug-patterns/engine-divergence.md b/docs-wiki/bug-patterns/engine-divergence.md new file mode 100644 index 000000000..567adbfa8 --- /dev/null +++ b/docs-wiki/bug-patterns/engine-divergence.md @@ -0,0 +1,77 @@ +--- +title: Two Engines, One Interface +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-backend.jsonl + - mdl/backend/modelsdk/microflow.go + - docs/13-decisions/0004-full-codec-engine.md +--- + +> **Do not duplicate**: the abstraction's rationale is canonical in ADR-0002 and +> ADR-0004 and framed in [[backend-abstraction]]; the per-field fixes live in the +> findings. This page describes what goes wrong while two implementations exist. + +## What this is + +The backend interface has two implementations — the legacy `sdk/mpr` writer and +the codec-based `modelsdk` engine — and roughly a quarter of the `mdl/backend` +findings are one of them doing something the other does not. The default is the +newer engine, so a gap in it is the behaviour most users get, while the tests and +habits formed against legacy still pass. + +A gap on one engine is **invisible from inside that engine**. Everything is +self-consistent: the write stores what the read returns, the round trip is +stable, and the field that never existed is never missed. + +## How it fits + +**Two failure modes, and only one of them is honest.** A refusal — +`… not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy` — +costs the user a flag and tells the truth. The alternative is a placeholder: +`-- Empty action` from a describer that does not recognise an activity type, a +sort clause silently absent, an argument list quietly dropped. The placeholder is +worse than the refusal in every way, because `describe → edit → exec` then +deletes the construct and everything reports success. + +**The worst instance was a read that under-reported access.** A restricted page +came back as "no roles" on the default engine, so `SHOW ACCESS` and the security +matrix both understated who could reach it. A missing field is a cosmetic bug +almost everywhere and a security-relevant one here. + +**The check that finds these is the cross-engine matrix.** Write with engine A, +read with engine B, all four combinations. Every other test shape is +engine-local and therefore blind to exactly this class. + +**Read against the keys the writer builds, not against gen's accessors.** The +generated bindings and the stored document disagree in places — one action's +result is bound as `VariableName` where the model stores `OutputVariableName` — +so an accessor-based reader silently returns nothing. Round-trip tests +(`toGen → encode → decode → fromGen`) catch it; a reader test over hand-written +BSON does not, because the hand-written BSON was written from the same wrong +assumption. These have to be Go tests rather than MDL ones: `create or modify` +rebuilds the document from MDL, so it cannot reproduce a read-back defect at all. + +**Count the gap before fixing the instance.** Decoding every `$Type` through the +real reader turned one reported unsupported action into twenty-seven, one of +which was a shipped feature that could not be described back. Related: several +activity types live in **their own sub-metamodel** — `DatabaseConnector$…`, not +`Microflows$…` — so a probe or a grep under one prefix systematically +under-counts what is covered. + +**Fixing one field and not its sibling is the characteristic mistake.** A reader +restored for `TableMappings` and not `Parameters`, a source type ported without +its key parts. Enumerate every child of the element rather than the one the +report named. + +**Guard the write side with a round trip, not with `mx check`.** Several of these +produced models mxbuild accepts: a box size of zero renders every activity as a +one-pixel sliver in Studio Pro and validates at 0 errors, because `mx check` does +not look at geometry. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + fields, sub-metamodels and round-trip guards +- [[backend-abstraction]] — why the seam exists +- [[describe-round-trip-gaps]] — the same read-side failure, engine-independent diff --git a/docs-wiki/bug-patterns/mutator-addressing.md b/docs-wiki/bug-patterns/mutator-addressing.md new file mode 100644 index 000000000..be6e76517 --- /dev/null +++ b/docs-wiki/bug-patterns/mutator-addressing.md @@ -0,0 +1,74 @@ +--- +title: Addressing Things the Model Does Not Name +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-backend.jsonl + - mdl/backend/mpr/page_mutator.go + - mdl/backend/modelsdk/page_write.go +--- + +> **Do not duplicate**: the ALTER PAGE / ALTER LAYOUT syntax lives in the skills +> and `MDL_QUICK_REFERENCE.md`; the per-property fixes live in the findings. This +> page is about the addressing problem underneath them. + +## What this is + +`ALTER PAGE`, `ALTER LAYOUT` and `ALTER WORKFLOW` edit a stored document in +place, which means naming a node inside it. Roughly thirteen `mdl/backend` +findings come from the fact that **many of the interesting nodes have no name**. +A DataGrid 2 column, an Accordion group, a pop-up menu item and a layout region +are all addressable in Studio Pro and store no `Name` at all. + +The resulting failures have a distinctive signature: the command reports +success and nothing changes. Or it reports `widget "X" not found` for something +`DESCRIBE` plainly shows. + +## How it fits + +**A derived name is the only option, and it is not stable.** A column's +addressable name comes from its attribute or caption, so it moves when the +caption is edited. Persisting the name the author wrote is not available either: +there is no slot for it, and inventing one is the write-a-property-Studio-Pro- +does-not-declare hazard. What is left is to make the derivation *visible* — warn +at authoring time which name will be addressable, and on a lookup miss list the +addressable names rather than reporting a bare not-found. + +**Ambiguity must be refused, not resolved.** Two columns deriving the same name +is a real state, and taking the first is a data hazard rather than a wart. The +same applies to a bare name that could mean either an object-list item or a +widget: discriminate on the resolved node's `$Type`, name the qualified form in +the error, and do not guess which grid was meant — guessing is what produced an +invalid document. + +**A slot is not a name.** A layout region has neither, so it is addressed +positionally (`layoutContainer.top`), reusing the dotted reference that also +serves columns and disambiguated the same way. Only `INSERT INTO` accepts one: +BEFORE and AFTER position a widget among siblings, and treating them as INTO +would silently put widgets somewhere the script did not ask for. + +**Property lookup is per-shape, and the shapes differ.** A button's text is a +`CaptionTemplate`, not a `Caption`. Page-level property names are case-sensitive +while widget ones are matched lowercase. A column's value kind comes from the +schema — expression, primitive or text template — and writing a string where a +reference belongs is accepted by everything and visible to nothing. + +**Hand-built BSON drifts from codec-built BSON.** The mutator constructs +documents directly while CREATE goes through the codec, so the two encodings of +"the same" widget diverge — an empty-string value where the codec writes an +explicit key, a typed-array marker present in one and not the other. The reliable +method is to **diff the two encodings** with a BSON dump rather than eyeball the +hand-built one. Getting the marker wrong is not a smaller version of the same +bug: it turns a silent no-op into a project Studio Pro cannot open. + +**Check whether the symptom reproduces outside the reported context.** One +"widget not found" turned out to be two independent defects, and the fix for the +reported nesting alone would not have made the reporter's command work. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + properties, shapes and refusals +- [[unloadable-model-writes]] — where a wrong typed-array marker ends up +- [[silent-property-drop]] — the executor-side twin of "reports success, writes + nothing" From 155b4d9dee6a167bbf8ea29ad4dce79b9a202d78 Mon Sep 17 00:00:00 2001 From: Ako Date: Tue, 1 Sep 2026 08:18:05 +0000 Subject: [PATCH 3/5] docs(wiki): digest the mdl/grammar findings into three bug-pattern pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 49 of the 53 mdl/grammar findings (92%) now fall under a named class. Three pages are new; the rest reuse describe-round-trip-gaps and silent-property-drop. - capability-gap-as-parse-error (36 touched, ~20 primary). The distinctive one. Someone tries to express something Mendix supports, the parser says `no viable alternative`, and they conclude the FEATURE is impossible — a parse error is indistinguishable from a mistake they made. So the reports in this class are not feature requests, they are workarounds: an admin screen hand-rolled as five pages because a tab container was believed not to exist, "use a Java action" for a binary upload, "go to Studio Pro" as the standing answer to translation, a headless pipeline ending in a manual step. The gap is bidirectional — DESCRIBE of a document that HAS the construct must do something, and in the mapping findings it emitted MDL that parsed and rebuilt a different document, which is worse than a parse error. Also records that a narrow/wide statement pair (SET vs REPLACE) is a whitelist extended one bug report at a time, and that "not in the metamodel" is not a conclusion until the namespace is right. - keyword-collisions (22 touched, ~8 primary). The distinguishing question is how a collision fails, not whether one happens: a parse error is recoverable, a different valid parse is not — a widget conditional calling trim() was silently dropped. Records that a grammar alternative and its visitor case are ONE change (accepting -7 without the AST case serialized `[Amount > ]`), and the control-binary sweep for proving a relaxation causes no regressions. - scripts-that-cannot-rerun (5). Statement-level idempotence, kept deliberately apart from ADR-0008's write-level idempotence, which is what people usually mean when they say mxcli is idempotent. `exec` halting on the first error turns "90% already applied" into "none of the remaining 10% applied", and the silent variant — a duplicated index — is worse than the error. Coverage stated in CLAUDE.md was corrected to the measured 92% before committing; the first draft claimed 98% from memory. Co-Authored-By: Claude Opus 5 --- .claude/skills/maintain-wiki.md | 3 + CLAUDE.md | 2 +- docs-wiki/SYNC_LOG.md | 4 + .../capability-gap-as-parse-error.md | 74 +++++++++++++++++++ docs-wiki/bug-patterns/keyword-collisions.md | 72 ++++++++++++++++++ .../bug-patterns/scripts-that-cannot-rerun.md | 68 +++++++++++++++++ 6 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 docs-wiki/bug-patterns/capability-gap-as-parse-error.md create mode 100644 docs-wiki/bug-patterns/keyword-collisions.md create mode 100644 docs-wiki/bug-patterns/scripts-that-cannot-rerun.md diff --git a/.claude/skills/maintain-wiki.md b/.claude/skills/maintain-wiki.md index 60fffc093..7d3d98f8f 100644 --- a/.claude/skills/maintain-wiki.md +++ b/.claude/skills/maintain-wiki.md @@ -160,6 +160,9 @@ isn't better served by an existing page or a different doc artifact. | `bug-patterns/engine-divergence.md` | bug-pattern | two backend implementations, and a gap in one that is invisible from inside it | | `bug-patterns/mutator-addressing.md` | bug-pattern | in-place edits to nodes the model does not name | | `bug-patterns/access-rule-reconciliation.md` | bug-pattern | GRANT is a read-modify-write, and both directions of loss report success | +| `bug-patterns/capability-gap-as-parse-error.md` | bug-pattern | an unspelled capability presenting as `no viable alternative`, and the workarounds that follow | +| `bug-patterns/keyword-collisions.md` | bug-pattern | MDL's keyword set occupying positions where user data lives | +| `bug-patterns/scripts-that-cannot-rerun.md` | bug-pattern | statement-level idempotence, and why it is not write-level idempotence | ## Adding a new page diff --git a/CLAUDE.md b/CLAUDE.md index 48fbfadf4..4af0738f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -564,7 +564,7 @@ mxcli uses a layered documentation system — each artifact type has a single ca **ADRs are immutable once accepted.** Supersede with a new ADR rather than editing in place. Conventions and template in [`docs/13-decisions/README.md`](docs/13-decisions/README.md). -**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest covers `mdl/executor` (83% of its findings), `cmd/mxcli` (92%) and `mdl/backend` (99%) across 20 pattern pages; `make digest-status` reports the gap per area, so a pattern miss means "not yet digested", not "not seen before". +**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest covers `mdl/executor` (83% of its findings), `cmd/mxcli` (92%), `mdl/backend` (99%) and `mdl/grammar` (92%) across 23 pattern pages; `make digest-status` reports the gap per area, so a pattern miss means "not yet digested", not "not seen before". **The wiki is synthesized, not stated.** It frames and connects across the other artifacts — it never restates content that has a canonical home. Rules and seed page list in [`.claude/skills/maintain-wiki.md`](.claude/skills/maintain-wiki.md). diff --git a/docs-wiki/SYNC_LOG.md b/docs-wiki/SYNC_LOG.md index 55d25d60a..5eea4be60 100644 --- a/docs-wiki/SYNC_LOG.md +++ b/docs-wiki/SYNC_LOG.md @@ -49,3 +49,7 @@ not capture, because sources are upstream of the commit. | 2026-08-31 | bug-patterns/mutator-addressing.md | .claude/skills/fix-issue/findings/mdl-backend.jsonl (19 records), mdl/backend/mpr/page_mutator.go, mdl/backend/modelsdk/page_write.go | New page (added to seed table). Many addressable nodes — DataGrid2 columns, object-list items, layout regions — store no Name, so addressing is derived or positional; ambiguity is refused rather than resolved, and hand-built BSON drifts from codec-built BSON in ways only a dump-diff shows | | 2026-08-31 | bug-patterns/access-rule-reconciliation.md | .claude/skills/fix-issue/findings/mdl-backend.jsonl (6 records), mdl/backend/modelsdk/domainmodel_security_write.go | New page (added to seed table). Small class graded high: GRANT is a read-modify-write and both directions of loss report success. Records that the writer was innocent in the instructive case — a later reconcile removed the rules — and that the reported trigger (WHERE) was a red herring | | 2026-08-31 | (no page) mdl/backend MCP findings | .claude/skills/fix-issue/findings/mdl-backend.jsonl | Deliberately NOT given a page: ~33 records touch the MCP/PED backend, which already has architecture/mcp-backend.md and models/ped-mutation-constraints.md. Restating them here would violate the never-restate rule; the findings link to those pages instead | +| 2026-08-31 | bug-patterns/capability-gap-as-parse-error.md | .claude/skills/fix-issue/findings/mdl-grammar.jsonl (20 records), mdl/grammar/MDLParser.g4, docs/13-decisions/0003-mdl-is-sql-shaped.md | New page (added to seed table). First pass over mdl/grammar. A parse error is indistinguishable from a mistake the user made, so the reports in this class are workarounds rather than feature requests; the gap is bidirectional (DESCRIBE must do something too, and rebuilding a DIFFERENT document is worse than a parse error); a narrow/wide statement pair is a whitelist | +| 2026-08-31 | bug-patterns/keyword-collisions.md | .claude/skills/fix-issue/findings/mdl-grammar.jsonl (8 records), mdl/grammar/MDLLexer.g4, mdl/executor/identifier_quoting.go | New page (added to seed table). The distinguishing question is how a collision fails, not whether one happens: a parse error is recoverable, a different valid parse is not. Records that a grammar alternative and its visitor case are one change, and the control-binary sweep for proving a relaxation | +| 2026-08-31 | bug-patterns/scripts-that-cannot-rerun.md | .claude/skills/fix-issue/findings/mdl-grammar.jsonl (5 records), mdl/grammar/domains/MDLDomainModel.g4, docs/13-decisions/0003-mdl-is-sql-shaped.md | New page (added to seed table). Statement-level idempotence, kept explicitly apart from ADR-0008 write-level idempotence, which is what people usually mean when they say mxcli is idempotent. The silent variant (a duplicated index) is worse than the error | + diff --git a/docs-wiki/bug-patterns/capability-gap-as-parse-error.md b/docs-wiki/bug-patterns/capability-gap-as-parse-error.md new file mode 100644 index 000000000..beea3e861 --- /dev/null +++ b/docs-wiki/bug-patterns/capability-gap-as-parse-error.md @@ -0,0 +1,74 @@ +--- +title: A Missing Capability Looks Like a Syntax Error +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-grammar.jsonl + - mdl/grammar/MDLParser.g4 + - docs/13-decisions/0003-mdl-is-sql-shaped.md +--- + +> **Do not duplicate**: the syntax itself lives in `MDL_QUICK_REFERENCE.md` and +> the skills; each construct's grammar rule and BSON shape live in the findings. +> This page is about what a parse error costs when it means "not implemented". + +## What this is + +Roughly twenty of the `mdl/grammar` findings are the same event: someone tries to +express something Mendix supports, the parser says `no viable alternative`, and +they conclude **the feature is impossible**. It is not — the capability is simply +unspelled, and a parse error is indistinguishable from a mistake the user made. + +That confusion is the expensive part. The reports in this class are rarely +"MDL should support X"; they are workarounds. An admin screen hand-rolled as five +pages with bespoke SCSS because a tab container was believed not to exist. "Use a +Java action" for a binary upload. "Go to Studio Pro" as the standing answer to +translating an app. A headless pipeline that ends with a manual step because +anonymous access has no statement. + +## How it fits + +**The gap is bidirectional.** MDL not being able to *write* a construct is only +half of it: `DESCRIBE` of a document that has one has to do something, and +dropping it silently is the common outcome. Worse, in the mapping findings, +DESCRIBE emitted MDL that **parsed and rebuilt a different document** — output +that looks like a successful round trip and is not. When adding a spelling, the +read side is part of the same change, not a follow-up. + +**A narrow/wide pair is a whitelist.** Where two statements can set the same +property and one accepts less than the other — `SET` versus `REPLACE` here — the +narrow one gets extended a bug report at a time, and the documented workaround +routes users through the wide one, which rebuilds more than they asked. The fix +that holds is to reuse the wide rule and its builder, not to add another case to +the narrow one. + +**"Not in the metamodel" is not a conclusion until the namespace is right.** A +search under one prefix "disproved" a capability that lives under another — +`Microflows$…RequestHandling` rather than `Rest$…Body`. Asking for a Studio Pro +example settled in minutes what a metamodel grep had ruled out, and CLAUDE.md's +rule applies: when the shape is unknown, get a reference document rather than +reason from an absence. + +**With no reference available, derive from something already proven.** Where no +example could be obtained, the shapes came from the generated metamodel and from +sibling writers that already ship the same by-name reference form — not from +guessing and not from stopping. Where a reference *is* available, pin against it: +a guess got four things wrong at once about one element — the type's namespace, a +property that does not exist, append-versus-sort ordering, and a flag's default. + +**A reference is a reference, not a string.** An icon, an entity, a microflow — +anything that names a model element is spelled as a qualified name, which is what +makes the language consistent (ADR-0003) and what forces the quoting question +into the describe emitter as well. + +**The documentation is part of the surface.** Several of these were found by +running the statements quoted in mxcli's own skills through `mxcli check` rather +than by reading them. Advice that cannot parse is the same defect as a missing +rule, and it is the version an agent will follow confidently. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — each construct, + its rule, and the reference document that settled its shape +- [[mdl-as-sql]] — why the language is shaped the way it is +- [[describe-round-trip-gaps]] — the read half of every gap here diff --git a/docs-wiki/bug-patterns/keyword-collisions.md b/docs-wiki/bug-patterns/keyword-collisions.md new file mode 100644 index 000000000..f6fb8dd0a --- /dev/null +++ b/docs-wiki/bug-patterns/keyword-collisions.md @@ -0,0 +1,72 @@ +--- +title: The Keyword Set Leaks Into User Data +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-grammar.jsonl + - mdl/grammar/MDLLexer.g4 + - mdl/executor/identifier_quoting.go +--- + +> **Do not duplicate**: the quoting rules a user needs are in the skills and +> CLAUDE.md ("Quoting Escapes Parser Keywords, Not Platform-Reserved Member +> Names"); each collision's fix is in the findings. This page is about the shape. + +## What this is + +MDL has a large keyword vocabulary, and those keywords occupy positions where +user data also lives — a widget called `List`, an attribute called `Template`, an +XPath predicate calling `trim()`, a negative number, a string containing a +newline. Eight `mdl/grammar` findings are a collision between the two. + +The distinguishing question is not whether a collision happens but **how it +fails**. A parse error is recoverable: the user sees it and quotes the name. The +same collision resolving to a *different valid parse* is not, and several of +these did exactly that. + +## How it fits + +**Silent is the dangerous half.** A widget conditional using a function whose +name is also a lexer keyword did not error — it was dropped, so the widget lost +its visibility rule and the page built cleanly. A describer emitting a +keyword-named widget bare produced output that failed only when someone re-ran +it. The loud failures in this class cost minutes; the quiet ones cost a +round trip. + +**A grammar fix alone can be worse than the bug.** Accepting an unquoted negative +number in XPath made the parser succeed while the AST builder had no case for the +new alternative — so the constraint serialized as `[Amount > ]`, a dropped +operand instead of a loud error. **A grammar alternative and its visitor case are +one change**, and only a round-trip helper caught it; the write path used the raw +text and looked fine. + +**Prove a relaxation with a control binary, not by reading.** Making a keyword +optional or a rule more permissive can change how unrelated input parses. The +method that works: stash the `.g4`, regenerate, build a control binary, and sweep +every example with both. Thirteen scripts failed with the change — the *same* +thirteen, all pre-existing. + +**Derive the accepted set from the grammar, in a test.** Where a keyword list is +maintained on one side and derived on the other, the next widget package +re-opens the gap. A test that reads the accepted keywords straight out of the +`.g4` cannot drift from it, and can carry the fix instructions in its failure +message. + +**A renderer that stringifies an enum outgrows its grammar silently.** An emitter +that formats a value by name will happily print the ninth member of an +enumeration that the grammar knows eight of. Switching on concrete types cannot +do that, which is why the sibling emitter never had the bug — and where +stringifying is unavoidable, the guard is a describe-then-parse loop over the +enumeration's full membership. + +**Quoting is not one question.** Quoting escapes MDL's *parser* keywords. It does +not escape Mendix's platform-reserved member names, and it does not help in an +OQL alias. Three vocabularies stack, and a rule that conflates them produces +confident, wrong advice. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + collisions and their controls +- [[describe-round-trip-gaps]] — where an unquoted emit surfaces +- [[platform-semantics-gaps]] — the other two vocabularies quoting does not reach diff --git a/docs-wiki/bug-patterns/scripts-that-cannot-rerun.md b/docs-wiki/bug-patterns/scripts-that-cannot-rerun.md new file mode 100644 index 000000000..98e1b80fe --- /dev/null +++ b/docs-wiki/bug-patterns/scripts-that-cannot-rerun.md @@ -0,0 +1,68 @@ +--- +title: Scripts That Cannot Be Re-Run +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-grammar.jsonl + - mdl/grammar/domains/MDLDomainModel.g4 + - docs/13-decisions/0003-mdl-is-sql-shaped.md +--- + +> **Do not duplicate**: the `IF NOT EXISTS` / `create or modify` spellings live +> in `MDL_QUICK_REFERENCE.md`; write-level idempotence is ADR-0008 and +> [[element-identity]]. This page is about idempotence at the *statement* level, +> which is a different thing. + +## What this is + +mxcli's writes are idempotent — a unit whose content has not changed is not +written at all. Its *statements* are not, and the two get conflated. `create` +follows SQL convention and fails on an existing object; `alter entity … add +attribute` fails when the attribute is already there. Five `mdl/grammar` findings +are the consequence, and the compounding one is that `exec` halts on the first +error. + +Put together: **a domain script that is 90% already applied applies none of the +remaining 10%.** The first `add attribute` that already exists ends the run, and +every later statement — including the new ones — is skipped. The user's reading is +usually that mxcli is broken, because re-running a script is the natural way to +converge a project on a description of it. + +## How it fits + +**The silent variant is worse than the error.** `alter entity … add index` did +not fail on a re-run: it reported "Added index" and appended a duplicate, and the +build then rejected the project. An error that stops the script is recoverable; a +success that corrupts is not, and the two live in the same statement family. + +**Non-idempotence is a design decision, not a bug — but it has to be +discoverable.** MDL is SQL-shaped deliberately, and `CREATE TABLE` failing on an +existing table is the behaviour that shape implies. What was missing was the +signpost: the bare "already exists" error did not name `create or modify`, and +the idempotent spellings existed without being reachable from the failure. + +**Two remedies, and they answer different questions.** `IF NOT EXISTS` / +`IF EXISTS` on a statement says *this one is optional*, which is what a +converging domain script wants. `exec --continue-on-error` says *attempt +everything and tell me what failed*, which is what a partially-applied script +wants — and it must still exit non-zero, or it trades a stopped run for a hidden +failure. + +**Watch for the pair that cannot both be re-run.** `ADD EVENT HANDLER` errors +when the handler exists and `DROP EVENT HANDLER` errors when it does not, so +neither a plain script nor a defensive drop-then-add is re-runnable. A statement +family needs the guard on both halves or the workaround is unavailable too. + +**A list rule without a separator reads as a value error.** Several parse +failures in this area were reported against the *value* in the second item of a +list — `add attribute A: integer default 9, add attribute B: …` — when the list +rule simply had no comma alternative. When a parse error names something that is +plainly valid in the first position, suspect the list before the value. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the guards, the + flags and the bug-tests +- [[mdl-as-sql]] — why `create` is non-idempotent on purpose +- [[element-identity]] — write-level idempotence, which is unrelated and is often + what people mean when they say mxcli is idempotent From de3b2b2354e7cb12a2cf975253cb46e84813c2ed Mon Sep 17 00:00:00 2001 From: Ako Date: Tue, 1 Sep 2026 08:24:09 +0000 Subject: [PATCH 4/5] =?UTF-8?q?docs(wiki):=20digest=20mdl/visitor=20?= =?UTF-8?q?=E2=80=94=20two=20new=20pages=20and=20the=20first=20re-sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 27 of the 28 mdl/visitor findings (96%). Two pages are new and one is a re-sync — the first any page has had since the initial synthesis on 2026-05-24. - expression-translation-drift (13). Deliberately distinct from platform-semantics-gaps: there the MDL is illegal Mendix, here the MDL is correct, the emitted Mendix expression is well-formed, and it says something else. The worst instance is the quietest — an additive chain rebuilt with its operators reordered, so a microflow computed a different number than its source said with mxcli check, mx check and the build all green, and the corruption in the stored document rather than in DESCRIBE. Three ways meaning is lost (a literal changes type, an operator changes, a function resolves to the wrong overload), plus the ANTLR trap behind the standard fix: GetText() excludes hidden tokens and a source-interval slice includes them, so preserving raw source inherits every comment in that span. - misleading-diagnostics (6). Graded above "the message could be clearer" because a wrong hint costs however long the reader spends acting on it: they blamed their quoting, renamed an attribute that was fine, or concluded a construct was unsupported. A hint's precision matters more than its coverage — the "unescaped apostrophe" hint matched any short lowercase word and so fired on every genuine error at `on`, `in`, `as`, `to`, `by`. - visitor-wiring-gaps — RE-SYNCED. It described one size of gap (a field); the findings show three. A field, a structure (ELSIF arms, which Mendix has no native form for and which must be lowered into nested ifs), and a whole statement that parses, exits 0 and dispatches to nothing — the last presenting as an empty result rather than a missing feature, which is what lets it survive. Added the neighbouring failure where a field is wired to the WRONG thing, which reports success and changes meaning rather than losing it. Every sources: path and every [[wiki-link]] across all 25 pages verified to resolve. Co-Authored-By: Claude Opus 5 --- .claude/skills/maintain-wiki.md | 2 + CLAUDE.md | 2 +- docs-wiki/SYNC_LOG.md | 4 +- .../expression-translation-drift.md | 81 +++++++++++++++++++ .../bug-patterns/misleading-diagnostics.md | 66 +++++++++++++++ docs-wiki/bug-patterns/visitor-wiring-gaps.md | 61 +++++++++++--- 6 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 docs-wiki/bug-patterns/expression-translation-drift.md create mode 100644 docs-wiki/bug-patterns/misleading-diagnostics.md diff --git a/.claude/skills/maintain-wiki.md b/.claude/skills/maintain-wiki.md index 7d3d98f8f..02f758ce8 100644 --- a/.claude/skills/maintain-wiki.md +++ b/.claude/skills/maintain-wiki.md @@ -163,6 +163,8 @@ isn't better served by an existing page or a different doc artifact. | `bug-patterns/capability-gap-as-parse-error.md` | bug-pattern | an unspelled capability presenting as `no viable alternative`, and the workarounds that follow | | `bug-patterns/keyword-collisions.md` | bug-pattern | MDL's keyword set occupying positions where user data lives | | `bug-patterns/scripts-that-cannot-rerun.md` | bug-pattern | statement-level idempotence, and why it is not write-level idempotence | +| `bug-patterns/expression-translation-drift.md` | bug-pattern | MDL expression to Mendix expression, where the translation changes the meaning | +| `bug-patterns/misleading-diagnostics.md` | bug-pattern | hints that fire on the wrong thing and send the reader somewhere the problem is not | ## Adding a new page diff --git a/CLAUDE.md b/CLAUDE.md index 4af0738f2..737e678fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -564,7 +564,7 @@ mxcli uses a layered documentation system — each artifact type has a single ca **ADRs are immutable once accepted.** Supersede with a new ADR rather than editing in place. Conventions and template in [`docs/13-decisions/README.md`](docs/13-decisions/README.md). -**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest covers `mdl/executor` (83% of its findings), `cmd/mxcli` (92%), `mdl/backend` (99%) and `mdl/grammar` (92%) across 23 pattern pages; `make digest-status` reports the gap per area, so a pattern miss means "not yet digested", not "not seen before". +**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest covers `mdl/executor` (83% of its findings), `cmd/mxcli` (92%), `mdl/backend` (99%), `mdl/grammar` (92%) and `mdl/visitor` (96%) across 25 pattern pages; `make digest-status` reports the gap per area, so a pattern miss means "not yet digested", not "not seen before". **The wiki is synthesized, not stated.** It frames and connects across the other artifacts — it never restates content that has a canonical home. Rules and seed page list in [`.claude/skills/maintain-wiki.md`](.claude/skills/maintain-wiki.md). diff --git a/docs-wiki/SYNC_LOG.md b/docs-wiki/SYNC_LOG.md index 5eea4be60..74275c034 100644 --- a/docs-wiki/SYNC_LOG.md +++ b/docs-wiki/SYNC_LOG.md @@ -52,4 +52,6 @@ not capture, because sources are upstream of the commit. | 2026-08-31 | bug-patterns/capability-gap-as-parse-error.md | .claude/skills/fix-issue/findings/mdl-grammar.jsonl (20 records), mdl/grammar/MDLParser.g4, docs/13-decisions/0003-mdl-is-sql-shaped.md | New page (added to seed table). First pass over mdl/grammar. A parse error is indistinguishable from a mistake the user made, so the reports in this class are workarounds rather than feature requests; the gap is bidirectional (DESCRIBE must do something too, and rebuilding a DIFFERENT document is worse than a parse error); a narrow/wide statement pair is a whitelist | | 2026-08-31 | bug-patterns/keyword-collisions.md | .claude/skills/fix-issue/findings/mdl-grammar.jsonl (8 records), mdl/grammar/MDLLexer.g4, mdl/executor/identifier_quoting.go | New page (added to seed table). The distinguishing question is how a collision fails, not whether one happens: a parse error is recoverable, a different valid parse is not. Records that a grammar alternative and its visitor case are one change, and the control-binary sweep for proving a relaxation | | 2026-08-31 | bug-patterns/scripts-that-cannot-rerun.md | .claude/skills/fix-issue/findings/mdl-grammar.jsonl (5 records), mdl/grammar/domains/MDLDomainModel.g4, docs/13-decisions/0003-mdl-is-sql-shaped.md | New page (added to seed table). Statement-level idempotence, kept explicitly apart from ADR-0008 write-level idempotence, which is what people usually mean when they say mxcli is idempotent. The silent variant (a duplicated index) is worse than the error | - +| 2026-08-31 | bug-patterns/expression-translation-drift.md | .claude/skills/fix-issue/findings/mdl-visitor.jsonl (13 records), mdl/visitor/visitor_microflow_expression.go, mdl/visitor/visitor_helpers.go | New page (added to seed table). First pass over mdl/visitor. Distinct from platform-semantics-gaps: there the MDL is illegal Mendix, here the MDL is correct and the TRANSLATION says something else — the worst case a microflow computing a different number with every check green. Records the ANTLR hidden-token trap and why the same expression text means different things in different slots | +| 2026-08-31 | bug-patterns/misleading-diagnostics.md | .claude/skills/fix-issue/findings/mdl-visitor.jsonl (6 records), mdl/visitor/visitor.go | New page (added to seed table). Graded above "the message could be clearer": a wrong hint costs however long the reader spends acting on it, and in the reported cases they blamed their quoting, renamed an attribute that was fine, or concluded a construct was unsupported. A hint's precision matters more than its coverage | +| 2026-08-31 | bug-patterns/visitor-wiring-gaps.md | .claude/skills/fix-issue/findings/mdl-visitor.jsonl, mdl/visitor/visitor_enumeration.go, mdl/visitor/visitor_helpers.go | **Re-sync** (first since the 2026-05-24 initial synthesis). Broadened from one size of gap to three — a field, a structure (ELSIF arms lowered into nested ifs), and a whole statement that parses and dispatches to nothing — and added the neighbouring failure where a field is wired to the WRONG thing, which reports success and changes meaning. sources: updated from the findings directory to the specific shard; wiki-links normalised to bare slugs | diff --git a/docs-wiki/bug-patterns/expression-translation-drift.md b/docs-wiki/bug-patterns/expression-translation-drift.md new file mode 100644 index 000000000..d0572cb46 --- /dev/null +++ b/docs-wiki/bug-patterns/expression-translation-drift.md @@ -0,0 +1,81 @@ +--- +title: The Expression Translation Loses Meaning +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-visitor.jsonl + - mdl/visitor/visitor_microflow_expression.go + - mdl/visitor/visitor_helpers.go +--- + +> **Do not duplicate**: the Mendix expression rules themselves are +> [[platform-semantics-gaps]] and the skills; each translation's fix is in the +> findings. This page is about the step in between. + +## What this is + +An MDL expression is not a Mendix expression. The visitor parses one and emits +the other, and thirteen `mdl/visitor` findings are that translation changing +what the expression *means*. This is a different class from writing something +Mendix forbids: here the MDL is correct, the Mendix expression is well-formed, +and it says something else. + +The worst of them is the quietest. An additive chain came back from the model +with its operators reordered, so **a microflow computed a different number than +its source said** — with `mxcli check`, `mx check` and the build all green, and +the corruption in the stored document rather than in the description. + +## How it fits + +**Three ways meaning is lost.** + +*A literal changes type.* A decimal literal losing its fraction; an enumeration +value stored as a quoted string instead of a qualified name; a bare identifier +that needed a `$currentObject/` prefix and did not get one. Each produces an +expression Mendix parses and evaluates differently, or rejects with a generic +`CE0117` that names nothing. + +*An operator changes.* `/` is division in most languages and member navigation in +Mendix, so the same characters mean two things depending on what is either side +of them. An additive chain rebuilt without walking its children in order swaps +operands. + +*A function resolves to the wrong overload.* `contains` and `find` exist as both +string functions and list operations, and the visitor has to decide from the +argument shapes which one was meant — a string literal in the second position +means the string function, two bare variables are genuinely ambiguous at parse +time and get decided later from the declared type. + +**Preserving raw source is the standard fix, and it carries hidden tokens with +it.** Where the rebuilt expression is lossy, the visitor keeps the original text. +The trap is that ANTLR's `GetText()` excludes hidden tokens while a source-interval +slice *includes* them — so any code reaching for original text inherits every +comment in that span, and a `--` comment between two operands ends up inside the +Mendix expression. Replacing a comment with whitespace rather than with nothing +matters for the same reason: `1 --c\n+ 2` must not become `1+ 2`. + +**Quoting is the other systematic leak.** The guidance to quote identifiers is +about the MDL *parser*; a quote that survives into the stored expression produces +`Mod."Entity".Attr`, which Mendix rejects. The tell is a **half-stripped** name in +the error message — one half went through the unquoting reader and the other did +not, which localises the bug to a `GetText()` call that should have been the +structured accessor. + +**The same expression text means different things in different slots.** A widget +`Visible:` is a client expression and needs bare identifiers prefixed; a +datasource `where` is real XPath and must not be. An enum in the first stringifies +to a qualified name and in the second to a quoted string. A fix applied to +"expressions" rather than to a specific slot breaks the other one. + +**Nothing downstream can catch this.** The output is a syntactically valid Mendix +expression, so `mx check` is satisfied, the build succeeds, and the only +observable difference is a value at runtime. Verification has to be the round +trip plus reading the stored bytes — `strings` on the `.mxunit` is what showed the +swapped operands. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + literals, operators and overloads +- [[platform-semantics-gaps]] — expressions Mendix forbids outright +- [[keyword-collisions]] — where the quoting leak starts diff --git a/docs-wiki/bug-patterns/misleading-diagnostics.md b/docs-wiki/bug-patterns/misleading-diagnostics.md new file mode 100644 index 000000000..b03954989 --- /dev/null +++ b/docs-wiki/bug-patterns/misleading-diagnostics.md @@ -0,0 +1,66 @@ +--- +title: A Wrong Hint Is Worse Than No Hint +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-visitor.jsonl + - mdl/visitor/visitor.go +--- + +> **Do not duplicate**: the individual hints and their trigger conditions live in +> the findings and in `mdl/visitor/`. This page is about why the class is graded +> higher than "the message could be clearer". + +## What this is + +ANTLR's raw errors name tokens, not intentions: `no viable alternative at input +'add'` for a missing keyword, `mismatched input '=' expecting ')'` for a +caption written with the wrong separator. mxcli adds hints on top. Six +`mdl/visitor` findings are those hints firing on the wrong thing and **sending +the reader somewhere the problem is not**. + +A missing hint costs the reader a minute of confusion. A wrong one costs however +long they spend acting on it — and in the reported cases they blamed their +quoting, renamed an attribute that was fine, or concluded a construct was +unsupported. + +## How it fits + +**A pattern broad enough to be useful is broad enough to be wrong.** The +"unescaped apostrophe" hint matched any short lowercase word, so every genuine +parse error on `on`, `in`, `as`, `to` or `by` was diagnosed as a quoting mistake. +Narrowing it to the actual contraction suffixes — `s`, `t`, `d`, `m`, `re`, `ve`, +`ll` — keeps the real cases and drops the false ones. A hint's precision matters +more than its coverage, because its whole value is that the reader trusts it. + +**Key the hint off the source line, not the parser message.** The token error for +a misplaced `index` clause lands on the index *name*, so the message never +mentions `index` at all and no message-matching hint can fire. Reading the +offending line is what makes the diagnosis possible — and it also lets the hint +discriminate against the valid form of the same construct. + +**A hint that recommends a rename is a strong claim.** One offered three +alternative names for an attribute that parsed perfectly well elsewhere, because +it conflated two kinds of reserved word: an MDL *parser* keyword, which quoting +escapes and the model keeps, and a name the *platform* reserves, which quoting +does not help with at all. Conflating them is expensive in both directions — +advising a quote where the platform reserves the name produces something that +parses and then fails the build. + +**Some errors are the platform's, and the hint should say so.** Mendix XPath +cannot compute values, so `[Seq = $Game/MoveSeq + 1]` is a limitation rather than +a syntax mistake; a bare `mismatched input '+'` reads as the latter. Naming the +constraint turns a dead end into a redesign. + +**The reachable-form question.** Several of these are not really about wording: +the reader's next question is "then how *do* I write it?", and a hint that names +the working spelling answers it. Where there is no working spelling, saying so is +still better than a token error — that is the boundary with +[[capability-gap-as-parse-error]]. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the hints, + their triggers and their controls +- [[capability-gap-as-parse-error]] — when the honest hint is "you cannot" +- [[keyword-collisions]] — the two kinds of reserved word, kept apart diff --git a/docs-wiki/bug-patterns/visitor-wiring-gaps.md b/docs-wiki/bug-patterns/visitor-wiring-gaps.md index 35baee242..3b65149ac 100644 --- a/docs-wiki/bug-patterns/visitor-wiring-gaps.md +++ b/docs-wiki/bug-patterns/visitor-wiring-gaps.md @@ -1,28 +1,69 @@ --- title: Visitor Wiring Gaps category: bug-pattern -last-synced: 4e185f73 +last-synced: ced830e0 sources: - - .claude/skills/fix-issue/findings/ + - .claude/skills/fix-issue/findings/mdl-visitor.jsonl - mdl/visitor/visitor_enumeration.go + - mdl/visitor/visitor_helpers.go --- -> **Do not duplicate**: the specific fix recipe (issue #393) lives in the `.claude/skills/fix-issue/findings/` symptom row, and the canonical wiring blocks live in `mdl/visitor/`. This page describes the pattern only. +> **Do not duplicate**: the per-instance fix recipes and the exact blocks to copy +> live in the findings; the canonical wiring lives in `mdl/visitor/`. This page +> describes the pattern only. ## What this is -A family of "parsed-but-not-stored" bugs: a property the user set in a `CREATE` statement silently vanishes after a roundtrip. The grammar accepts it, the AST struct has a field for it, the model and writer both carry it, and `DESCRIBE` knows how to print it — yet it never appears, because the value was never copied out of the parse tree into the AST. +A family of "parsed-but-not-stored" bugs. The grammar accepts the input, the AST +struct has a field for it, the model and writer both carry it, and `DESCRIBE` +knows how to print it — and it never appears, because the value was never copied +out of the parse tree into the AST. + +The visitor is the one **hand-written** bridge in the grammar → visitor → AST → +executor → backend pipeline, and it is per-statement boilerplate with no compiler +check that every field was copied. That is the whole mechanism. ## How it fits -mxcli's pipeline runs grammar → visitor → AST → executor → backend, and the visitor is the one hand-written bridge in that chain. Each `ExitCreateXxxStatement` must explicitly read fields off the ANTLR parse-tree context and assign them onto the AST struct. When a new `CREATE` variant is added, it is easy to wire every other layer and forget one assignment in the visitor — so the field round-trips through nothing. +**The gap comes in three sizes, and they present very differently.** + +*A field.* The original instance: an enum-level doc comment vanishing after a +round trip, a `CREATE OR REPLACE` flag silently running as a plain `CREATE`. Every +layer below the visitor is correct, and the loss is one document type wide. The +canonical fix is to diff the broken visitor against a known-good sibling — the +enumeration and constant visitors share the same two standard blocks verbatim. + +*A structure.* An `ELSIF` chain whose middle arms were dropped on write. Mendix +has no native `elsif`, so each arm has to be lowered into a nested `if` in the +previous arm's `else` branch; a visitor that reads only the first and last arms +produces a valid microflow that is missing behaviour. The read path was never +wrong — a round trip showed `if … else …` because the arms were absent from the +model, which is the diagnostic that separates a write gap from a read gap. + +*A whole statement.* A `DROP` or a `SHOW` that parses, exits 0, prints nothing +and does nothing, because the statement reached the AST and no dispatch case. The +symptom reads as an empty result — "this page has no content", "there are no +connections" — rather than as a missing feature, which is what makes it survive. +Anything that can end in an empty output needs to distinguish *nothing found* +from *nothing ran*. -The tell-tale: every layer below the visitor is correct (the field exists in the AST, the writer serializes it, `DESCRIBE` prints it for other types) but a doc-comment or `OR MODIFY`/`OR REPLACE` flag disappears for one specific document type. The class recurs because visitor methods are per-statement boilerplate with no compiler check that all fields were copied. +**The neighbouring failure is a field wired to the wrong thing**, which is worse +than one not wired at all: a delete-behaviour keyword storing a different +behaviour, a dollar-quoted SQL body overwritten by a parameter's default. Those +report success and change the model's meaning, where an unwired field merely +loses it. -The canonical fix is to diff the broken visitor against a known-good sibling. The enumeration and constant visitors in [`mdl/visitor/`](../../mdl/visitor/visitor_enumeration.go) share the same two standard blocks verbatim — `stmt.Documentation = findDocCommentText(ctx)` and the `OR MODIFY`/`OR REPLACE` flag detection. The exact blocks to copy are in the symptom table. +**Guard the class, not the instance.** These recur because the boilerplate is +per-statement. The remedies that have held are structural: route every qualified +name through one accessor rather than `GetText()`, and assert reachability — a +test that every exported validator is called from the one entry point catches a +whole statement that was never wired, which no per-feature test can. ## See also -- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the per-instance fix recipes for this pattern -- [[architecture/mdl-execution]] — the grammar → visitor → AST → executor pipeline this gap sits in -- [[rationale/mdl-as-sql]] — why MDL statements roundtrip through `DESCRIBE` +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the per-instance + recipes and the blocks to copy +- [[mdl-execution]] — the pipeline this gap sits in +- [[duplicate-resolver-drift]] — the sibling shape one layer down +- [[expression-translation-drift]] — where the visitor changes meaning rather than + dropping it From 680025d7f9d5368ded6cd4745cf7313ea407064d Mon Sep 17 00:00:00 2001 From: Ako Date: Tue, 1 Sep 2026 15:07:55 +0000 Subject: [PATCH 5/5] fix(findings): recover 13 lost pattern pages; close the digest's blind spot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two problems, found by running `make digest-status` on main after the stack merged. **13 pattern pages never reached main.** All six PRs report MERGED and only #356's content is on main: the stacked PRs each merged into their BASE BRANCH, which by then was no longer on the path to main. GitHub was telling the truth — they merged into what they targeted. This branch is docs/bug-patterns-visitor, which holds all 25 pages, merged with current main. Verified: 25 pages, 883 findings, every sources: path exists and every [[wiki-link]] resolves. **The digest report said 0% outstanding while 249 findings were undated.** Undated records were skipped by the "since last sync" comparison entirely, so an area could go completely undigested and the headline would still read current — the exact failure the report exists to prevent, in the report itself. Three changes: - the 249 undated findings are backfilled from git blame on the shards (mdl/executor gained 250 records from other sessions since the last pass); - an undated finding now COUNTS AS NOT DIGESTED rather than being skipped, and the headline says "not yet digested" rather than "added since", which is what it always meant; - `date` is now required by check-findings. Warning was not an option: 249 records accumulated undated, and nobody saw the warning that was not there. Two limits are documented in the script header rather than engineered away, because both make the number look better than reality: `date` is day-resolution and the comparison is strictly greater-than, so 256 mdl/executor findings carrying the sync's own date read as digested when ~250 arrived after the pages were written; and the blame-based backfill invalidated the blame it read, so the field is now the only record. Co-Authored-By: Claude Opus 5 --- .../fix-issue/findings/mdl-executor.jsonl | 500 +++++++++--------- scripts/check-findings.sh | 6 + scripts/digest-status.sh | 26 +- 3 files changed, 278 insertions(+), 254 deletions(-) diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index ee4a3065d..08ea896a5 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -246,253 +246,253 @@ {"area": "mdl/executor", "date": "2026-08-31", "raw": "| An `image` widget mxcli wrote shows nothing, and `mx check` fails with **\"No image selected.\"**; and a `describe` → rename → `exec` copy of an Atlas layout (or any page with a brand image) comes out with the image gone, describe having emitted `image staticImage1 (Responsive: false)` with no reference at all | `mdl/executor/widget_engine.go` (the `image` operation), `mdl/executor/widget_defs.go` + `mdl/executor/widget_registry.go` (`operationForType`, `defaultKnownOperations`), `mdl/backend/widgetobj/builder.go` (`SetImage`, `setImageValue`), `mdl/backend/mutation.go` + `mdl/backend/mcp/widget.go`, `{modelsdk,sdk}/widgets/definitions/image.def.json` (`imageObject` → `Image`), `mdl/executor/cmd_pages_describe_pluggable.go` (`extractCustomWidgetPropertyImage`) + `cmd_pages_describe_output.go` (`describeImageWidgetProps`) | MDL had no spelling for **which** image an image widget shows, so its default source (`ImageType: image`, an image collection entry) could never be satisfied and describe had nothing to emit — mxcli-formula1 FINDINGS §142. The name is three parts like an icon reference, `Module.Collection.Image`, stored as a plain string on the `WidgetValue`'s `Image` key. Both halves are needed: with only the write, a round trip still loses it. `setImageValue` **replaces** the existing `Image` key and never adds one — adding a key the widget definition does not declare is the CE0463 shape. Controls: stub `setImageValue` → `TestSetImageValue_SetsTheQualifiedName` fails; stub the describe emission → `TestDescribeImageWidget_EmitsTheImageReference` fails |", "ce": ["CE0463"]} {"area": "mdl/executor", "date": "2026-08-31", "raw": "| `image imgTypo (Image: 'MyFirstModule.Images.NoSuchImage')` passes `mxcli check --references` (\"Check passed!\") and the build then fails **CE1613** \"The selected image '…' no longer exists.\" | `mdl/executor/validate_widget_image_ref.go` (new, `imageRefErrors`, `buildImageQualifiedNames`), `mdl/executor/helpers.go` (`widgetRefCollector.images`, `validateWidgetReferences`) | Wiring `Image:` created a new qualified name and nothing resolved it — every other name a widget carries (microflow, nanoflow, page, snippet, entity) already was. This one has **three** parts, so the two mistakes get two messages: a missing collection sends the reader to `show image collections`, a missing image to that collection's contents (which the diagnostic lists). A **nil** name set means the collections could not be read and nothing is reported; an empty non-nil one is a real answer. No same-script exemption — MDL cannot create an image collection entry, so the reference can only resolve against the project. Control: `TestImageRefErrors_KnownImageIsClean`, and `TestImageRefErrors_NoCollectionsMeansNoOpinion` for the failed-read direction |", "ce": ["CE1613"]} {"area": "mdl/executor", "date": "2026-08-31", "symptom": "`main` goes red on a test that passed in **both** PRs that touched it — here `TestDescribeWorkflow_NoAnnotationEmitsNoComment`: `unexpected violations [MDL-WF05] for a plain jump`", "cause": "Two PRs merged in sequence. One added a validator rule (MDL-WF05, dangling `jump to` target); the other's control test asserted `len(violations) == 0` over a fixture that was **not a valid workflow** — a lone jump whose target did not exist. Each CI run was green because neither saw the other's change", "file": "`mdl/executor/issue1007_annotation_emit_test.go` (the fixture), `mdl/executor/validate_workflow_jump.go` (the rule, which is right)", "insight": "**A test fixture that is not a valid instance of the thing under test is a landmine for the next rule.** \"No violations at all\" is only meaningful over input that *should* have none; over an invalid fixture it silently asserts \"no rule has been written yet that notices this\". Fix the **fixture**, not the rule. Generalise: when a test asserts the ABSENCE of diagnostics, make the input something you would be happy to ship. Two green PRs can still merge to red and **neither PR's CI can detect it** — the only protection is a fixture that does not depend on which rules exist today. Found by running `make test` on an unrelated docs branch cut from the merged main, which is an argument for doing that on any branch cut after a batch merge.", "refs": ["#350", "#351"], "rules": ["MDL-WF04", "MDL-WF05"]} -{"area": "mdl/executor", "symptom": "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\"", "cause": "`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`", "file": "`mdl/executor/layout.go` (`measureStatementsSpan`), used by `addLoopStatement`/`addWhileStatement` in `cmd_microflows_builder_control.go`", "insight": "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", "refs": ["#790"]} -{"area": "mdl/executor", "symptom": "`create or modify external entities` silently resets a per-entity setting the user had changed (e.g. allow-create-change-locally)", "cause": "`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", "file": "`mdl/executor/cmd_contract.go` (`applyExternalEntityFields`)", "insight": "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", "refs": ["#782"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "`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)", "file": "`mdl/executor/cmd_pages_describe_flowcontext.go` (`dataSourceEntityContext`, `flowReturnEntity`) + the five assignment sites in `cmd_pages_describe_parse.go`", "insight": "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"} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/cmd_pages_describe_pluggable.go` (`microflowSourceRef`, `nanoflowSourceRef`, `extractDataGrid2DataSource`, `extractGalleryDataSource`, `parseCustomWidgetDataSource`)", "insight": "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", "refs": ["#795"]} -{"area": "mdl/executor", "symptom": "`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`", "cause": "`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", "file": "`mdl/executor/cmd_settings.go` (`settingsInt`, `settingsBool`) + `mdl/executor/validate_settings.go` (`typedSettingsKeys`, MDL-SET01/MDL-SET02)", "insight": "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", "refs": ["#805"], "rules": ["MDL-SET01", "MDL-SET02"]} -{"area": "mdl/executor", "symptom": "Retrieve/datasource XPath with a `[%…%]` token (e.g. `[System.owner = '[%CurrentUser%]']` or `[Title = '[%CurrentUser%]']`) fails `mx check` CE0161, but `[Title='abc']` is clean", "cause": "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`)", "file": "`mdl/executor/validate.go` (`validateRetrieveConstraints`, `baseSystemMemberRe`) — diagnose with `mxcli bson dump --type microflow` + `mx check`; verify the token alone works", "insight": "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", "refs": ["#641"], "ce": ["CE0161"]} -{"area": "mdl/executor", "symptom": "`describe` shows `$var = action ...;`", "cause": "Missing formatter case", "file": "`mdl/executor/cmd_microflows_format_action.go` → `formatActionStatement()`", "insight": "Add `case *microflows.XxxAction:` with `fmt.Sprintf` output"} -{"area": "mdl/executor", "symptom": "`describe` shows `$var = list operation %T;` (with type name)", "cause": "Missing formatter case", "file": "`mdl/executor/cmd_microflows_format_action.go` → `formatListOperation()`", "insight": "Add `case *microflows.XxxOperation:` before the `default`"} -{"area": "mdl/executor", "symptom": "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\"", "cause": "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", "file": "`mdl/executor/cmd_pages_builder_v3.go` (`buildPageV3`, the `if page.LayoutCall != nil` block)", "insight": "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`", "ce": ["CE1613"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/cmd_microflows_builder_actions.go` (`resolveMemberChange`, the \"Not an association in the authored module\" branch)", "insight": "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", "refs": ["#51"]} -{"area": "mdl/executor", "symptom": "`mxcli check` rejects a **valid** microflow: **MDL048** on `retrieve … where [id = '[%CurrentUser%]']` (the standard signed-in-user idiom) — but `mx check` → 0 errors", "cause": "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", "file": "`mdl/executor/validate_microflow.go` (`checkXPathIdConstraint`)", "insight": "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", "refs": ["#53"], "rules": ["MDL048"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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)", "file": "`mdl/executor/validate_microflow.go` (`exprHasSlashDivision`)", "insight": "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", "refs": ["#52"], "rules": ["MDL045"]} -{"area": "mdl/executor", "symptom": "`create association X …` errors \"association already exists\" on re-run and aborts the script", "cause": "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", "file": "`mdl/executor/cmd_associations.go` (the `NewAlreadyExists(\"association\", …)` sites)", "insight": "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", "refs": ["#51"]} -{"area": "mdl/executor", "symptom": "`dynamictext x (Content: '')` builds with **CE0720** \"Place holder index 1 is greater than 0\" — `mxcli check` ✓, describe shows `Content: '{1}'` with no params", "cause": "The builder unconditionally defaulted empty content to the template `{1}`, creating a placeholder with no matching parameter (orphaned)", "file": "`mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildDynamicTextV3`, final `content == \"\"` guard)", "insight": "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", "refs": ["#9"], "ce": ["CE0720"]} -{"area": "mdl/executor", "symptom": "`dynamictext s (Content: '$318')` builds with **CE0402/CE1613** (\"attribute '$318' no longer exists\") — the literal was turned into an unbound `{1}` param", "cause": "The auto-bind check treated ANY `$`-prefixed content as a variable; `$318` (dollar + digits) is not a valid Mendix variable", "file": "`mdl/executor/cmd_pages_builder_v3_widgets.go` (`isDynamicTextVariableRef` / `dynamicTextVariableRe`)", "insight": "Treat `$` as a variable ONLY when followed by a letter/underscore (`^\\$[A-Za-z_]`); `$318` stays literal content. Tests `TestBuildDynamicTextV3_DollarDigitLiteral`, `TestIsDynamicTextVariableRef`. traceops #10", "refs": ["#10"], "ce": ["CE0402", "CE1613"]} -{"area": "mdl/executor", "symptom": "`listview lv (… PageSize: 200)` always pages at 20 — `mxcli check` ✓, `mx check` ✓, describe shows no PageSize", "cause": "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", "file": "`mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildListViewV3`) + `cmd_pages_describe_parse.go` (Forms$ListView case) + `cmd_pages_describe_output.go` (listview case)", "insight": "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", "refs": ["#17"]} -{"area": "mdl/executor", "symptom": "`combobox (Association: Mod.Ref, …)` drops the binding — `mxcli check` ✓ but MxBuild fails **CE0642** \"Property 'Attribute' is required\"", "cause": "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", "file": "`mdl/executor/widget_engine.go` (`case \"Association\"`) + `mdl/executor/validate_widgets.go` (`validateComboBoxAssociation`)", "insight": "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", "refs": ["#23"], "ce": ["CE0642"]} -{"area": "mdl/executor", "raw": "| `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` |", "ce": ["CE0148"]} -{"area": "mdl/executor", "symptom": "CE7054 \"parameters updated\" / CE7067 \"does not support body entity\" after `send rest request`", "cause": "`addSendRestRequestAction` emitted wrong BSON: all params as query params, BodyVariable set for JSON bodies", "file": "`mdl/executor/cmd_microflows_builder_calls.go` → `addSendRestRequestAction`", "insight": "Look up operation via `fb.restServices`; route path/query params with `buildRestParameterMappings`; suppress BodyVariable for JSON/TEMPLATE/FILE via `shouldSetBodyVariable`", "ce": ["CE7054", "CE7067"]} -{"area": "mdl/executor", "symptom": "`CREATE X` returns \"already exists — use create or replace to overwrite\" but OR REPLACE is not valid for that type", "cause": "Error message in executor points to wrong keyword", "file": "`mdl/executor/cmd__*.go` — find the `NewAlreadyExistsMsg` call", "insight": "Change hint from `or replace` to `or modify`; verify the AST stmt uses `CreateOrModify` not `CreateOrReplace`"} -{"area": "mdl/executor", "symptom": "`mx check` CE0126 \"Missing value for parameter X\" on `call java action ... ($Param = empty)` for typed (non-entity, non-microflow) parameters", "cause": "Builder emitted `BasicCodeActionParameterValue.Argument: \"\"` instead of the literal `\"empty\"` keyword", "file": "`mdl/executor/cmd_microflows_builder_calls.go` → `addCallJavaActionAction`", "insight": "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", "ce": ["CE0126"]} -{"area": "mdl/executor", "symptom": "`DESCRIBE microflow` puts shared activities inside an `if … then` block — they should appear after `end if;`", "cause": "Nested guard split inside `traverseFlowUntilMerge` crosses the outer merge boundary", "file": "`mdl/executor/cmd_microflows_show_helpers.go` — guard path in `traverseFlowUntilMerge` (~line 854)", "insight": "Add `if contID != mergeID` guard before the `isMerge` skip-through so the guard continuation never crosses the outer merge"} -{"area": "mdl/executor", "symptom": "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)", "cause": "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)", "file": "`mdl/executor/cmd_odata.go` (`mendixAttrTypeToEdm`, `lookupEntityMembers`, `astEntityDefToModel`) + both writers (`publishedMemberToGen`, `serializePublishedMember`) + both reads", "insight": "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", "ce": ["CE5016"]} -{"area": "mdl/executor", "symptom": "Published OData service: `mx check` reports **CE5022** \"Published association X has changed multiplicity\" for every exposed association — modelsdk engine only", "cause": "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", "file": "`mdl/executor/cmd_odata.go` (`assocMembership.Type`, `astEntityDefToModel`) + both writers/reads", "insight": "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", "ce": ["CE5016", "CE5022"]} -{"area": "mdl/executor", "symptom": "MDL widget property `mxcli check`s clean but Studio Pro renders the default (e.g. `dataview ... (FormOrientation: Vertical)` always Horizontal)", "cause": "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", "file": "`mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildXxxV3`) + `sdk/mpr/writer_widgets_*.go` (`serializeXxx`) + `sdk/pages/pages_widgets_*.go`", "insight": "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"} -{"area": "mdl/executor", "symptom": "Pluggable widget datasource property (`optionsSourceAssociationDataSource: Module.Entity`) passes `check` + `exec` but `mx check` reports CE0642 \"Property 'Entity' is required\"", "cause": "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)", "file": "`mdl/executor/validate_widgets.go` (`datasourceTypedKeys`) — and author via the `datasource:` clause", "insight": "`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)", "ce": ["CE0642"]} -{"area": "mdl/executor", "symptom": "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", "cause": "`ValidateEntity` early-returned for NPEs; reserved-word check was not wired into the executor", "file": "`mdl/executor/cmd_enumerations.go` (`ValidateEntity`) and `mdl/executor/cmd_entities.go` (`execCreateEntity`)", "insight": "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", "refs": ["#552"], "ce": ["CE7247"], "rules": ["MDL020", "MDL021"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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)", "file": "`mdl/executor/cmd_microflows_show.go` (`duplicateOutputVariableWarnings`)", "insight": "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", "refs": ["#710"]} -{"area": "mdl/executor", "symptom": "`DESCRIBE` of an entity/module emits `grant ... (read (Module.Entity.Attr))` that fails to re-parse with `mismatched input '.'` — breaks the DESCRIBE roundtrip", "cause": "Member emitter used the fully-qualified BY_NAME reference; grant grammar accepts a bare `IDENTIFIER` only", "file": "`mdl/executor/cmd_entities_access.go` → `resolveEntityMemberAccess`", "insight": "Strip `memberName` to the last `.`-segment before appending (bare names have no dot, so it's a no-op for them). Issue #633", "refs": ["#633"]} -{"area": "mdl/executor", "symptom": "`style:` on a `dynamictext` crashes MxBuild with a NullReferenceException (fails at build, not check)", "cause": "The generic appearance applier wrote an inline Style into the DynamicText's `Forms$Appearance`; Mendix's metamodel can't handle it", "file": "`mdl/executor/cmd_pages_builder_v3.go` (`applyWidgetAppearance`) + `mdl/executor/validate_widgets.go` (`validateStaticWidget`)", "insight": "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", "refs": ["#673"]} -{"area": "mdl/executor", "symptom": "`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\"", "cause": "`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", "file": "`mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildDynamicTextV3`) + `mdl/executor/validate_widgets.go` (`validateStaticWidget`/`validateDynamicTextPlaceholders`)", "insight": "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", "refs": ["#650"], "ce": ["CE0720"]} -{"area": "mdl/executor", "symptom": "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\"", "cause": "The whole-script scriptContext used by reference validation tolerates forward refs (the page exists *somewhere*); the executor resolves page refs in statement order", "file": "`mdl/executor/validate.go` (`validateForwardPageRefs`)", "insight": "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", "refs": ["#674"]} -{"area": "mdl/executor", "symptom": "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'`", "cause": "`CreateWorkflowStmt` had **no** case in `validateWithContext` and no `ValidateWorkflow` at all — workflows received zero semantic validation", "file": "`mdl/executor/validate_workflow.go` (`ValidateWorkflow`, wired from `cmd/mxcli/cmd_check.go`)", "insight": "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`", "ce": ["CE1834", "CE1876", "CE7412"], "rules": ["MDL-WF01", "MDL-WF02", "MDL-WF03"]} -{"area": "mdl/executor", "symptom": "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 …\"", "cause": "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", "file": "`mdl/executor/validate_page_button_context.go` (`ValidatePageButtonContext`, wired from `cmd/mxcli/cmd_check.go`)", "insight": "**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", "ce": ["CE1571"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "Action emitters prefixed `call_`; the page-action grammar (`actionExprV3`) accepts `MICROFLOW`/`NANOFLOW` only", "file": "`mdl/executor/cmd_pages_describe_output.go` (`extractButtonAction`) + `cmd_pages_describe_pluggable.go` (`extractCustomWidgetPropertyAction`)", "insight": "Emit `microflow `/`nanoflow ` without the `call_` prefix. Issue #634", "refs": ["#634"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "Body emitted only when `readJavaActionUserCode` returns non-empty, but the grammar requires `AS DOLLAR_STRING` (body is mandatory)", "file": "`mdl/executor/cmd_javaactions.go` → `describeJavaAction`", "insight": "Always emit the `as $$ … $$;` block; when source can't be read, write a placeholder comment body. Issue #637", "refs": ["#637"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "#619 added `mdlIdent` (quote-if-reserved) for general widget names but missed datagrid column names", "file": "`mdl/executor/cmd_pages_describe_output.go` (`column %s` header, ~line 837)", "insight": "Wrap the column name: `fmt.Sprintf(\"column %s\", mdlIdent(colName))`. Issue #638", "refs": ["#619", "#638"]} -{"area": "mdl/executor", "symptom": "`DESCRIBE` of a page/snippet emits widget-action microflow args as `Param = $value` that fails with `mismatched input '=' expecting ':'` — breaks the DESCRIBE roundtrip", "cause": "Arg emitters used `=`, but `microflowArgV3` accepts `IDENTIFIER COLON expr` (`Param: $value`) or `VARIABLE EQUALS expr` (`$Param = …`) — not `IDENTIFIER EQUALS`", "file": "`mdl/executor/cmd_pages_describe_output.go` → `extractMicroflowParameters` / `extractNanoflowParameters`", "insight": "Emit the canonical colon form: `paramName+\": \"+value`. Issue #640", "refs": ["#640"]} -{"area": "mdl/executor", "symptom": "DESCRIBE drops `DataSource: selection X` for a DataView bound to a gallery/listview selection (master-detail pages)", "cause": "`extractDataViewDataSource` only handled `Forms$MicroflowSource` / `Forms$NanoflowSource` / `Forms$DataViewSource` / `Forms$DatabaseSource`; `Forms$ListenTargetSource` fell through to `return nil`", "file": "`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)", "insight": "Add `case \"Forms$ListenTargetSource\":` returning `{Type: \"selection\", Reference: ds[\"ListenTarget\"]}`; add `case \"selection\":` in the DataView output switch emitting `DataSource: selection `"} -{"area": "mdl/executor", "symptom": "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", "cause": "Engine has no per-property visibility metadata; the hide rules live in the widget's compiled `editorConfig.js` (`hidePropertyIn`/`hidePropertiesIn`), not `widget.xml`", "file": "`mdl/executor/widget_defs.go` `widgetVisibilityRules` table + `mdl/backend/mpr/widget_builder.go` `ApplyPropertyVisibility`", "insight": "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", "refs": ["#574"], "ce": ["CE0463"]} -{"area": "mdl/executor", "symptom": "CE0463 on a `datagrid` whose column uses `ColumnWidth: manual` + `Size: N` — Studio Pro resets the column `size` to `1`", "cause": "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\")`)", "file": "`mdl/executor/widget_defs.go` `itemPropertyAliases`", "insight": "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", "ce": ["CE0463"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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)", "file": "`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`", "insight": "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", "refs": ["#702"]} -{"area": "mdl/executor", "symptom": "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", "cause": "Case-mismatched prefix strip: visitor emits uppercase `\"MICROFLOW \"` from `odataValueText`, but `extractMicroflowRef` only trimmed lowercase `\"microflow \"`, so the keyword survived into BSON", "file": "`mdl/executor/cmd_odata.go` → `extractMicroflowRef`", "insight": "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", "refs": ["#573"]} -{"area": "mdl/executor", "symptom": "`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`", "cause": "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", "file": "`mdl/executor/oql_type_inference.go` → `findEntity`", "insight": "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", "refs": ["#610"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/cmd_styling.go` (`execAlterStyling`, `execDescribeStyling`) + `mdl/backend/mpr/page_mutator.go`", "insight": "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", "refs": ["#402", "#631"]} -{"area": "mdl/executor", "symptom": "`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\")", "cause": "`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", "file": "`mdl/executor/validate_microflow.go` → `walkBody` `*ast.DeclareStmt` case", "insight": "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", "refs": ["#607"], "ce": ["CE0038", "CE0053"], "rules": ["MDL002", "MDL040"]} -{"area": "mdl/executor", "raw": "| `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 |", "refs": ["#619"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/cmd_pages_describe_parse.go` (`extractDesignProperties` → `parseDesignProperty`) + `cmd_pages_describe_output.go` (`formatDesignPropertiesMDL` → `joinDesignPropertyEntries`) + `cmd_styling.go` (DESCRIBE STYLING emitter)", "insight": "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", "refs": ["#668"]} -{"area": "mdl/executor", "symptom": "`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)", "cause": "`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", "file": "`mdl/executor/oql_type_inference.go` → `extractSelectClause`", "insight": "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", "rules": ["MDL031"]} -{"area": "mdl/executor", "symptom": "`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)", "cause": "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", "file": "`mdl/executor/widget_defs.go` → `RegenerateWidgetDocs`", "insight": "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", "refs": ["#679"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/cmd_enumerations.go` (`mendixReservedWords`, `mendixSystemAttributeNames`) — no code change needed; docs only", "insight": "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", "ce": ["CE0142", "CE7247"], "rules": ["MDL021"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "`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\"`", "file": "`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`", "insight": "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`", "ce": ["CE0463"]} -{"area": "mdl/executor", "symptom": "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", "cause": "`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", "file": "`mdl/executor/executor_dispatch.go` → `executeInner`/`syncBack`", "insight": "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)"} -{"area": "mdl/executor", "symptom": "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)", "cause": "`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", "file": "`mdl/executor/validate_widgets.go` (`staticWidgetKnownProps`, `validateStaticWidgetUnknownProps`, wired in `validateWidgetTree` gated on `lookupWidgetDef==nil`)", "insight": "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`."} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`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`)", "insight": "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"} -{"area": "mdl/executor", "symptom": "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", "cause": "`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\"", "file": "`mdl/executor/cmd_pages_builder_v3.go` (`expandFragments`)", "insight": "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`"} -{"area": "mdl/executor", "symptom": "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`", "cause": "`ValidateEntity` had no autonumber-seed rule; the writer emits no `AttributeValue` when `!attr.HasDefault`, so Studio Pro has no start value. Sudoku findings #6", "file": "`mdl/executor/cmd_enumerations.go` (`ValidateEntity`)", "insight": "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`", "refs": ["#6"], "ce": ["CE7247"], "rules": ["MDL023"]} -{"area": "mdl/executor", "symptom": "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)", "cause": "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", "file": "`mdl/executor/cmd_enumerations.go` (`ValidateEntity`, `autoMemberNames`)", "insight": "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`", "refs": ["#7"], "ce": ["CE1613"], "rules": ["MDL022"]} -{"area": "mdl/executor", "symptom": "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.)", "cause": "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", "file": "`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`)", "insight": "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", "ce": ["CE1613"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`mdl/executor/widget_defs.go` (`itemPropertyAliases`, `columns.columnClass`) + `mdl/executor/widget_engine.go` (`WidgetDefGeneratorVersion` bump)", "insight": "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"} -{"area": "mdl/executor", "symptom": "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\"", "cause": "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", "file": "`mdl/executor/validate_widgets.go` (`validateStaticWidget`, MDL-WIDGET09) + `mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildDataViewV3` exec refusal)", "insight": "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)", "ce": ["CE7007"]} -{"area": "mdl/executor", "symptom": "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", "cause": "`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", "file": "`mdl/executor/cmd_pages_builder_v3.go` (`associationDestination`, new `entityIsOrDescendsFrom` + `entityGeneralizations`)", "insight": "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)", "ce": ["CE0402"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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`.)", "file": "`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)", "insight": "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`."} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`mdl/executor/widget_defs.go` (`itemPropertyAliases`, `heatmap.scaleColors.colour`) + `mdl/executor/widget_engine.go` (`WidgetDefGeneratorVersion` bump)", "insight": "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"} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`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)", "insight": "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", "ce": ["CE0463", "CE0642", "CE4899"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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)", "file": "`mdl/executor/oql_type_inference.go` (`ValidateOQLSyntax`, `oqlReservedWords`, **MDL032**, **MDL072**); `mdl/executor/validate_oql_reserved_names.go` (**MDL071**)", "insight": "**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.", "ce": ["CE0174"], "rules": ["MDL032", "MDL071", "MDL072"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`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**", "insight": "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`", "ce": ["CE6770"], "rules": ["MDL031"]} -{"area": "mdl/executor", "symptom": "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**", "cause": "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", "file": "`mdl/executor/oql_type_inference.go` (`passthroughStringLengthMismatch`, called in `validateViewEntityTypes` before the generic `typesCompatible`)", "insight": "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", "refs": ["#36"], "ce": ["CE6770"]} -{"area": "mdl/executor", "raw": "| 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) |", "ce": ["CE0038", "CE0053", "CE7247"], "rules": ["MDL040", "MDL043"]} -{"area": "mdl/executor", "symptom": "`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)", "cause": "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", "file": "`mdl/executor/cmd_enumerations.go` (`ValidateAlterEntity`, `validateEntityAttribute`) wired from `cmd/mxcli/cmd_check.go` + `cmd/mxcli/lsp_diagnostics.go`", "insight": "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)", "refs": ["#6"], "ce": ["CE7247"], "rules": ["MDL020", "MDL021", "MDL022", "MDL023"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "`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\"", "file": "`mdl/executor/cmd_entities.go` (`droppedEntityMembers`, the warn block before `UpdateEntity`, and the `execCreateEntity` already-exists message)", "insight": "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", "refs": ["#24"], "ce": ["CE1613"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`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`)", "insight": "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`", "ce": ["CE6084"]} -{"area": "mdl/executor", "symptom": "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`", "cause": "`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", "file": "`mdl/executor/cmd_pages_builder_v3.go` (`resolveDesignPropertyValueType`, now takes the value and reuses `themeOptionAllowed`)", "insight": "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", "ce": ["CE6084"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "`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", "file": "`mdl/executor/cmd_entities.go` (`execCreateEntity`)", "insight": "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", "refs": ["#13"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`mdl/executor/validate_widgets.go` (`validateWidgetExpressionAssociations`, called from `validateStaticWidget`)", "insight": "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", "refs": ["#4"], "ce": ["CE0117"]} -{"area": "mdl/executor", "symptom": "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)", "cause": "Message-only defects in the linter", "file": "`mdl/executor/validate_microflow.go` (MDL001 message ~216; `checkExprFunctions` ~283; `mendixAggregateFuncs`)", "insight": "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", "refs": ["#14", "#2", "#7", "#8"], "rules": ["MDL001", "MDL044"]} -{"area": "mdl/executor", "symptom": "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\"", "cause": "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", "file": "`mdl/executor/cmd_microflows_builder_validate.go` (`validateOutputVariable`, `validateScopedStatements`)", "insight": "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)", "refs": ["#5"], "ce": ["CE0111"]} -{"area": "mdl/executor", "symptom": "`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`", "cause": "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", "file": "`mdl/executor/validate.go` (`flowRefCollector` → `codeActionCallRef`, `validateCodeActionParams`)", "insight": "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", "ce": ["CE1613"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "The describe read path (`parseRawWidget`) only read `LabelTemplate` + `AttributeRef` for a textbox; `PlaceholderTemplate` and `OnChangeAction` were never read back into `rawWidget`", "file": "`mdl/executor/cmd_pages_describe_parse.go` (`extractPlaceholderText`), `cmd_pages_describe.go` (`rawWidget` fields), `cmd_pages_describe_output.go` (`renderClientActionMDL`/`extractOnChangeAction` + TextBox emit)", "insight": "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)", "refs": ["#9"]} -{"area": "mdl/executor", "symptom": "`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)", "cause": "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", "file": "`mdl/executor/cmd_pages_describe_output.go` (`unwrapToStringAttrParam` in `extractClientTemplateParameters`)", "insight": "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", "ce": ["CE1613"]} -{"area": "mdl/executor", "raw": "| 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 |", "refs": ["#26"], "ce": ["CE1613"]} -{"area": "mdl/executor", "symptom": "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", "cause": "No check inspected date-construction arguments; the function name resolves so the unknown-function check (MDL044) is silent", "file": "`mdl/executor/validate_microflow.go` (`checkDateTimeLiterals`/`exprHasNonLiteralDateTime`, MDL046)", "insight": "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", "refs": ["#21"], "ce": ["CE0117"], "rules": ["MDL044", "MDL046"]} -{"area": "mdl/executor", "symptom": "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", "cause": "No check inspected constraint strings for an association compared to `empty`; a bare attribute `= empty` IS valid, so a blanket ban would false-positive", "file": "`mdl/executor/validate_microflow.go` (`checkXPathAssociationEmpty`/`xpathAssocEmptyRe`, MDL047)", "insight": "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", "refs": ["#25"], "ce": ["CE0161"], "rules": ["MDL047"]} -{"area": "mdl/executor", "symptom": "Two sibling `dynamictext` widgets in a container render as `€ 310Last import: 7/24/2026` — concatenated with no separator, regardless of each one's RenderMode", "cause": "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", "file": "`mdl/executor/validate_widgets.go` (`validateConsecutiveDynamicText`, called from `validateWidgetTreeIn`)", "insight": "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", "refs": ["#27", "#29"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/validate.go` (CreateAssociationStmt case) + `mdl/executor/cmd_entities.go` (`isViewEntity`)", "insight": "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", "refs": ["#41"], "ce": ["CE6771"]} -{"area": "mdl/executor", "symptom": "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 ()`)", "cause": "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", "file": "`mdl/executor/cmd_entities.go` (`reconcileDroppedIndexes`, before `UpdateEntity`) + `mdl/backend/modelsdk/domainmodel_alter.go` (`UpdateEntity` empty-index dirtying)", "insight": "(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", "refs": ["#13", "#39"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "No check inspected retrieve constraints for an id comparison; `id` is a reserved member so it resolves loosely", "file": "`mdl/executor/validate_microflow.go` (`checkXPathIdConstraint`/`xpathIdConstraintRe`, MDL048)", "insight": "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", "refs": ["#42"], "ce": ["CE0161"], "rules": ["MDL048"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`mdl/executor/validate_microflow.go` (`checkAssociationObjectArgs`/`exprIsAssociationObjectPath`, MDL049; wired for CallMicroflowStmt + CallNanoflowStmt)", "insight": "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", "refs": ["#43", "#44"], "ce": ["CE0117"], "rules": ["MDL049"]} -{"area": "mdl/executor", "symptom": "~~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", "cause": "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", "file": "`mdl/executor/validate_microflow.go` (removed `checkFormatWithAssociation`/`exprHasRenderFunc`/`exprHasAssociationNav`/`renderFuncsIncompatibleWithAssoc`)", "insight": "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)", "refs": ["#48"], "ce": ["CE0117"], "rules": ["MDL050"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`mdl/executor/widget_defs.go` (`GenerateDefJSON` `case \"action\"` + `actionSourceForKey`); `mdl/executor/widget_engine.go` (`resolveMapping` `case \"OnChange\"`; `WidgetDefGeneratorVersion` bump 12→13)", "insight": "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)", "refs": ["#67"]} -{"area": "mdl/executor", "symptom": "**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)", "cause": "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", "file": "`mdl/executor/widget_defs.go` (`GenerateDefJSON` — populate `KnownProperties`); `WidgetDefGeneratorVersion` bump 13→14", "insight": "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)", "refs": ["#67"]} -{"area": "mdl/executor", "symptom": "**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", "cause": "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", "file": "`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)", "insight": "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)", "refs": ["#67"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`mdl/executor/validate_microflow.go` (`checkDuplicateLoopVariables`, MDL052)", "insight": "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", "refs": ["#64"], "ce": ["CE0111"], "rules": ["MDL052"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`mdl/executor/validate_microflow.go` (`loopBodyHasConditionalBreak`/`stmtsContainBreak`, MDL051) — **interim check**, pending the serialization fix", "insight": "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", "refs": ["#52"], "rules": ["MDL051"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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`)", "file": "`mdl/executor/widget_engine.go` (`applyColumnHeaderFallback`)", "insight": "**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)", "refs": ["#54"], "ce": ["CE0463"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`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`", "insight": "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", "refs": ["#759"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/validate_grant_roles.go` (`ValidateGrantRoles`, MDL-GRANT01), `mdl/executor/cmd_security_defaults.go` (`validateCrossModuleGrant`), wired in `cmd/mxcli/cmd_check.go`", "insight": "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", "refs": ["#836"], "ce": ["CE0148"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "`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\"", "file": "`mdl/executor/cmd_microflows_show_helpers.go` (`formatErrorHandlingSuffix`)", "insight": "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", "refs": ["#840"]} -{"area": "mdl/executor", "symptom": "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", "cause": "**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", "file": "`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)", "insight": "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", "refs": ["#840", "#843"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/cmd_microflows_builder_control.go` (`addLoopStatement` body loop)", "insight": "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", "refs": ["#52", "#791"], "ce": ["CE0079"], "rules": ["MDL051"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`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)", "insight": "**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.", "refs": ["#15"], "rules": ["MDL-WF04"]} -{"area": "mdl/executor", "symptom": "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", "cause": "`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", "file": "`mdl/executor/cmd_pages_builder_v3.go` (`resolveAssociationDestination` fallbacks + a hard guard in the `association` case of `buildDataSourceV3`)", "insight": "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", "refs": ["#14"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "`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", "file": "`mdl/executor/cmd_workflows.go` (`JumpToActivity` case in `formatWorkflowActivities`)", "insight": "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", "refs": ["#16"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`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)", "insight": "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", "refs": ["#17"], "ce": ["CE0117"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`mdl/executor/cmd_pages_builder_v3.go` (`associationEndpoints`, `resolveAssociationDestination`), `mdl/executor/widget_engine.go` + `cmd_pages_builder_input.go` (`resolveAssociationPathIn`, `storedSystemMemberName`)", "insight": "**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", "refs": ["#19"], "ce": ["CE1613"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`mdl/executor/cmd_security_write.go` (`execGrantEntityAccess`, `storedAuditMembers`, `otherModuleBothOwnerAssociations`), `mdl/backend/modelsdk/domainmodel_security_write.go` (`ReconcileMemberAccesses`)", "insight": "**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", "refs": ["#20"], "ce": ["CE0066"]} -{"area": "mdl/executor", "symptom": "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)", "cause": "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", "file": "`mdl/executor/validate_microflow_loop_scope.go` (new `MDL053`, wired from `microflowValidator.validate`), skill `.claude/skills/mendix/write-microflows.md`", "insight": "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", "ce": ["CE0108", "CE0111"], "rules": ["MDL052", "MDL053"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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`)", "file": "`mdl/executor/cmd_pages_builder_input.go` (`declaringEntityFor`, `entityAttributeOwners`), `mdl/executor/cmd_pages_builder_v3.go` (final attribute of the association path)", "insight": "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", "refs": ["#12"], "ce": ["CE1613"]} -{"area": "mdl/executor", "symptom": "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`", "cause": "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", "file": "`mdl/executor/cmd_microflows_helpers.go` (`normalizeMendixOperatorCase`, applied in the `SourceExpr` branch)", "insight": "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", "refs": ["#14"], "ce": ["CE0117"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/cmd_security_write.go` (`warnDemoUsersInert`, called after a successful create)", "insight": "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", "refs": ["#15"]} -{"area": "mdl/executor", "symptom": "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 …`", "cause": "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", "file": "`mdl/executor/cmd_entities.go` (`checkBeforeCreateHandlerHasNoParameters`, called from `buildEventHandlers`)", "insight": "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", "refs": ["#14"], "ce": ["CE7247"]} -{"area": "mdl/executor", "symptom": "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'`", "cause": "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", "file": "`mdl/executor/validate_icon_refs.go` (`validateIconRefs`), wired into `validateProgram`", "insight": "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)", "refs": ["#836"], "ce": ["CE1613"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/validate_page_order.go` (`ValidateScriptPageOrder`, MDL-PAGE01), wired in `cmd/mxcli/cmd_check.go` beside the other project-free validators", "insight": "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", "refs": ["#9"]} -{"area": "mdl/executor", "symptom": "`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\"", "cause": "`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", "file": "`mdl/executor/cmd_enumerations.go` (`validateNPEValidationRules`, called from `ValidateEntity`)", "insight": "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", "refs": ["#832"], "ce": ["CE0070"], "rules": ["MDL020", "MDL054"]} -{"area": "mdl/executor", "symptom": "`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\"", "cause": "Mendix XPath reaches at most ONE hop off a variable, and nothing checked the hop count", "file": "`mdl/executor/validate_microflow.go` (`checkXPathVariableTraversal`, `xpathVarTraversalRe`), called from the `*ast.RetrieveStmt` arm beside MDL047/MDL048", "insight": "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", "refs": ["#831"], "ce": ["CE0161"], "rules": ["MDL047", "MDL048", "MDL055"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`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`)", "insight": "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", "refs": ["#833", "#836"], "ce": ["CE0079", "CE0773"], "rules": ["MDL008", "MDL009", "MDL048", "MDL056"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/validate_microflow.go` (the `*ast.EnumSplitStmt` arm; `checkEnumSplitEmptyBranch`)", "insight": "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`", "ce": ["CE0079", "CE0773"], "rules": ["MDL008", "MDL009", "MDL056"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`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`)", "insight": "**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", "refs": ["#10"], "ce": ["CE0339", "CE0729", "CE7375"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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)", "file": "`mdl/executor/cmd_odata.go` (`odataModeToMDL`, `bareMemberName`, entity-set exposed name, `KEY` over `IsPartOfKey`)", "insight": "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", "refs": ["#10"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`mdl/executor/validate_database_type.go` (`ValidateDatabaseConnectionType`, MDL-DB01), wired in `cmd/mxcli/cmd_check.go`; `.claude/skills/mendix/database-connections.md`", "insight": "**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", "refs": ["#6"], "rules": ["MDL-DB01"]} -{"area": "mdl/executor", "symptom": "`DESCRIBE microflow` prints a bare `else` on a `split type` the author never wrote one for, and each describe→exec pass accumulates another", "cause": "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", "file": "`mdl/executor/cmd_microflows_show_helpers.go` (the `elseFlow` block in the inheritance-split traversal)", "insight": "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)", "ce": ["CE0089", "CE0090"]} -{"area": "mdl/executor", "symptom": "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.\"`", "cause": "`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*", "file": "`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)", "insight": "**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", "refs": ["#16"], "ce": ["CE4583", "CE5013", "CE5016"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/cmd_odata.go` (`metadataFetchAuth`, `metadataAuthFromStmt`, `fetchODataMetadata`), `mdl/ast/ast_odata.go` + `mdl/visitor/visitor_odata.go` (`HttpUsernameIsLiteral` / `HeaderDef.ValueIsLiteral`)", "insight": "**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", "refs": ["#23"]} -{"area": "mdl/executor", "symptom": "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", "cause": "The modify branch updated the service's scalar properties and never touched `EntityTypes` / `EntitySets`", "file": "`mdl/executor/cmd_odata.go` (modify branch rebuilds published entities via `astEntityDefToModel`, and carries `AllowedModuleRoles` through)", "insight": "**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", "refs": ["#26"]} -{"area": "mdl/executor", "symptom": "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", "cause": "`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`", "file": "`mdl/executor/cmd_contract.go` (`reservedEntityAttrNames` loses one entry; the import now reports the renames it does make)", "insight": "**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", "refs": ["#28"], "ce": ["CE7247"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "`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", "file": "`mdl/executor/cmd_odata.go` (`resolveCredential`, `constantReference`, `designTimeConstants`)", "insight": "**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", "refs": ["#23"]} -{"area": "mdl/executor", "symptom": "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", "cause": "The read side of folders was simply never built. `MOVE`/`DROP FOLDER` write containment; nothing read it back", "file": "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`", "insight": "**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", "refs": ["#2"]} -{"area": "mdl/executor", "symptom": "`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", "cause": "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", "file": "`mdl/executor/cmd_pages_describe_output.go` (`pageActionParameters`, `targetPageParameterNames`), stale comment corrected in `sdk/mpr/writer_widgets_action.go`", "insight": "**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", "refs": ["#296"], "ce": ["CE0115"]} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "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`", "insight": "**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"} -{"area": "mdl/executor", "symptom": "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", "cause": "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", "file": "`mdl/executor/validate_odata_read_contract.go` (suggestion text), `mdl-examples/doctype-tests/10-odata-examples.mdl`, `.claude/skills/mendix/odata-data-sharing.md`", "insight": "**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/