diff --git a/.claude/commands/mxcli-dev/fix-issue.md b/.claude/commands/mxcli-dev/fix-issue.md index ba9bfb8cc8..9273380ce9 100644 --- a/.claude/commands/mxcli-dev/fix-issue.md +++ b/.claude/commands/mxcli-dev/fix-issue.md @@ -21,7 +21,7 @@ is the order to do things in. 2. **Match the failure class**, then the instance: - `docs-wiki/bug-patterns/` for the class (small, read it) - - `grep -il '' .claude/skills/fix-issue/findings/*.jsonl` + - `grep -ril '' .claude/skills/fix-issue/findings/` for the instance A pattern-page miss means "not yet digested", never "not seen before". @@ -59,7 +59,7 @@ is the order to do things in. 7. **Add the regression case**: `mdl-examples/bug-tests/-.mdl`, and check it parses (`mxcli check`) and passes `make check-mdl`. -8. **Append one finding** to `.claude/skills/fix-issue/findings/.jsonl` and run +8. **Add one finding file**, `.claude/skills/fix-issue/findings//-.json` (one JSON object on one line; never append to an existing file), and run `make check-findings`. Write the insight — what would have made this cheaper to find, and which plausible wrong turn to skip — not the changelog. diff --git a/.claude/commands/mxcli-dev/wiki-sync.md b/.claude/commands/mxcli-dev/wiki-sync.md index 80bfaed679..cce6078c46 100644 --- a/.claude/commands/mxcli-dev/wiki-sync.md +++ b/.claude/commands/mxcli-dev/wiki-sync.md @@ -22,7 +22,7 @@ This command is the trigger; the skill is the contract. For `bug-patterns/` specifically, run **`make digest-status`** first: it reports how many findings have landed since the last bug-pattern sync and which areas no page mentions. That is the scope question for this category — the pages -digest `.claude/skills/fix-issue/findings/*.jsonl`, and an area with many +digest `.claude/skills/fix-issue/findings/*/*.json`, and an area with many findings and no page is an undigested failure class, not a missing file. If invoked with no arguments, ask the user which page(s) to sync. List the diff --git a/.claude/skills/fix-issue.md b/.claude/skills/fix-issue.md index 26093d2fa8..bdf7bac6a6 100644 --- a/.claude/skills/fix-issue.md +++ b/.claude/skills/fix-issue.md @@ -2,8 +2,8 @@ A fast-path workflow for diagnosing and fixing bugs in mxcli. -Each fix appends one finding to `findings/*.jsonl`. Those are **evidence, not -reading material**: 630 findings at ~1.7 KB each. The entry point for a diagnosis +Each fix adds one finding file under `findings//`. Those are **evidence, not +reading material**: over 1,500 findings at ~1.7 KB each. The entry point for a diagnosis is [`docs-wiki/bug-patterns/`](../../docs-wiki/bug-patterns/), which digests them into failure classes; the findings are where you drill down for the instance. @@ -45,31 +45,27 @@ into failure classes; the findings are where you drill down for the instance. > `.gitattributes` gave `merge=union` to the whole file, which kept two concurrent > appends instead of conflicting. That was the right driver on the wrong unit: union > applies file-wide, so two branches editing the same *prose* line would silently -> keep both. The note that shipped with it called this exact shot — "if that starts -> happening, split the table into its own file so `union` covers only append-only -> content" — and the union driver now sits on `findings/*.jsonl`, where every line is -> an independent record and keeping both sides is always correct. -> -> It never solved the conflicts anyway: GitHub's server-side merge does not run merge -> drivers, so every PR still had to merge `main` locally first. Sharding by area is -> what actually reduces them — two fixes now collide only when they touch the same -> area. +> keep both. The table moved to `findings/.jsonl` shards with the union driver +> on them, which still left every fix-PR conflicting on GitHub: its server-side +> merge does not run merge drivers, and nearly every fix appends to the same +> `mdl-executor` shard. The findings are now one file per finding, so two fixes add +> two different files and nothing conflicts at all. History in +> [`findings/README.md`](fix-issue/findings/README.md). --- ## Finding a prior fix -The findings live in `findings/*.jsonl`, one JSON object per line, sharded by -area. They are **data, not reading material**: 630 findings at ~1.7 KB each do -not fit in a context window, and the table they used to live in had grown past -the point where GitHub's web editor would open it. +The findings live in `findings//-.json`, one finding per +file, each a single JSON object on a single line. They are **data, not reading +material**: over 1,500 findings at ~1.7 KB each do not fit in a context window. -Grep is the fast path — the shard name narrows it, and every line is -self-contained: +Grep is the fast path. The directory narrows it by area, and every file is one +self-contained line: ```bash -grep -il 'CE0463' .claude/skills/fix-issue/findings/*.jsonl # which areas -grep -h 'CE0463' .claude/skills/fix-issue/findings/mdl-executor.jsonl | jq . +grep -ril 'CE0463' .claude/skills/fix-issue/findings/ # which findings +grep -rh 'CE0463' .claude/skills/fix-issue/findings/mdl-executor/ | jq . ``` For anything with a shape to it, the records have fields — `area`, `symptom`, @@ -77,7 +73,7 @@ For anything with a shape to it, the records have fields — `area`, `symptom`, them — and DuckDB reads the files in place, no import step: ```bash -duckdb -c "select symptom, file from '.claude/skills/fix-issue/findings/*.jsonl' +duckdb -c "select symptom, file from read_json_auto('.claude/skills/fix-issue/findings/*/*.json') where list_contains(ce, 'CE0463')" ``` @@ -90,18 +86,25 @@ small enough to read. Start there, come here for the instance. ## Recording a fix -Append one line to the shard for the area you touched — a new shard is fine if -none fits. Keep it one line: `merge=union` in `.gitattributes` resolves two -concurrent appends by keeping both, which is correct for a file of independent -records and is not correct for prose. +Add one new file in the directory for the area you touched (a new directory is +fine if none fits), named `-.json`, with the slug a few words +of the symptom. Keep the record on **one line**, so `grep -h` returns it whole. +Never append to another finding's file: one file per finding is what keeps two +fix-PRs from conflicting, since GitHub's merge cannot resolve two appends to +the same file. ```bash -cat >> .claude/skills/fix-issue/findings/mdl-executor.jsonl <<'JSON' +cat > .claude/skills/fix-issue/findings/mdl-executor/2026-08-31-describe-drops-sort-order.json <<'JSON' {"area":"mdl/executor","date":"2026-08-31","symptom":"...","cause":"...","file":"...","insight":"...","refs":["#123"]} JSON make check-findings ``` +If a merge with `main` brings back a `findings/.jsonl` file (a branch from +before the split), run `scripts/split-findings.py`. It turns those lines into +files and skips any that already exist; `check-findings` refuses the stray +`.jsonl` until you do. + `check-findings` prints one line saying how far `docs-wiki/bug-patterns/` has fallen behind; `make digest-status` breaks it down by area. **If the class of failure keeps recurring, sync its pattern page** (`/mxcli-dev:wiki-sync @@ -128,7 +131,7 @@ Step 1: write a failing test at the layer the symptom lives in Step 2: confirm it fails — and that it fails with the REPORTED symptom Step 3: implement the minimum code to make it pass Step 4: go test ./... (or the packages you touched) -Step 5: append a finding to findings/.jsonl +Step 5: add a finding file under findings// ``` Where the test goes, by layer: @@ -216,5 +219,5 @@ Before writing "cannot": - [ ] Any "cannot be verified" claim carries its evidence, and a control that could have falsified it - [ ] `make test && make lint` pass - [ ] `mdl-examples/bug-tests/-.mdl` added for the regression case -- [ ] New finding appended to `findings/.jsonl` (if not already covered), and `make check-findings` passes +- [ ] New finding file added under `findings//` (if not already covered), and `make check-findings` passes - [ ] PR title: `fix: ` diff --git a/.claude/skills/fix-issue/findings/README.md b/.claude/skills/fix-issue/findings/README.md index 1cdcc482e3..de89db15cf 100644 --- a/.claude/skills/fix-issue/findings/README.md +++ b/.claude/skills/fix-issue/findings/README.md @@ -1,59 +1,73 @@ # Findings -One JSON object per line, sharded by area. 630 records, extracted from the -symptom table that used to live inside `fix-issue.md` — see that file for how to -search them and how to add one. +One file per finding, in a directory per area: +`/-.json`, holding one JSON object on one line. The +records were extracted from the symptom table that used to live inside +`fix-issue.md` — see that file for how to search them and how to add one. -## Why JSONL, and why sharded +## Why one file per finding -The table had grown to 1.05 MB. That is past a context window, past what -GitHub's web editor will open, and past what the wiki's own `wiki-sync` can -consume (its Phase 2 requires reading a source in full) — so the digest step that -turns findings into `docs-wiki/bug-patterns/` pages had been blocked since the -day it was created. +The findings started as a Markdown table inside `fix-issue.md`, which grew to +1.05 MB: past a context window, past what GitHub's web editor will open, and +past what `wiki-sync` can consume. They then became nine `.jsonl` shards, +one record per line, with `merge=union` in `.gitattributes` so two fixes +appending to the same shard kept both lines. -Line-oriented records fix all three at once. A shard is small enough to open and -grep; `merge=union` is *correct* on a file of independent records, where on the -old mixed prose-and-table file it was a hazard; and a query can pull one area at -a time instead of the whole corpus. +That fixed the size and did not fix the conflicts. GitHub's server-side merge +does not run merge drivers, so every fix-PR that appended to a shard conflicted +with every other open one the moment one of them merged — measured on +2026-10-08, eight open PRs re-conflicting after each merge, each needing a +local merge of `main`, a rebuild and a re-test. Sharding only lowered the odds; +almost every fix lands in `mdl-executor`. And `union` had quietly done damage: +six records were in that shard twice, byte for byte, from merges that kept both +sides of the same line. The split dropped the duplicates. -Nine shards rather than one file per finding: 630 files would trade a -too-large file for a directory nobody can scan, and the conflicts that motivated -sharding only happen between fixes touching the *same* area. +With one file per finding, two fixes add two different files. There is nothing +to merge, so nothing conflicts, on GitHub or anywhere else. The cost the shard +design was avoiding, "a directory nobody can scan", is not one: nobody scans it. +The findings are looked up by `grep` or DuckDB, both of which take a glob. + +The split (`scripts/split-findings.py`) wrote each original line verbatim as a +file, so it is lossless by construction: the multiset of file contents equals +the multiset of unique original lines, checked byte for byte. ## Fields | field | | |---|---| -| `area` | package the fix landed in, e.g. `mdl/executor`. Decides the shard | -| `date` | when the finding was last touched, `YYYY-MM-DD`. Backfilled by `git blame` over the table this came from, so it is *last touched*, not *first written* — a row that was later corrected carries the correction's date. Used by `make digest-status` to measure how far the wiki digest has fallen behind | +| `area` | package the fix landed in, e.g. `mdl/executor`. The directory is the coarse shard (`mdl-executor/`); `area` is the precise package | +| `date` | when the finding was last touched, `YYYY-MM-DD`. Also the file name's prefix when the file was written. The older records were backfilled by `git blame` over the table they came from, so for those it is *last touched*, not *first written*. Used by `make digest-status` to measure how far the wiki digest has fallen behind | | `symptom` | what the user saw — the thing you match against | | `cause` | the mechanism | | `file` | where to look first | | `insight` | what would have made it cheaper to find; the part worth reading | | `refs`, `ce`, `rules` | issue/PR refs, Mendix `CE####` codes, MDL rule ids — extracted from the text, present only where the text carried them | -| `raw` | the original table row, for the 68 records whose row could not be split into four columns (an unescaped `\|` in the text, or a row that never had four cells). `symptom`/`cause`/`file`/`insight` are absent on these | +| `raw` | the original table row, for the records whose row could not be split into four columns (an unescaped `\|` in the text, or a row that never had four cells). `symptom`/`cause`/`file`/`insight` are absent on these | A record has **either** the four structured fields **or** `raw`. Queries that must cover everything need to account for both. -## Guard +## Naming -`make check-findings` (`scripts/check-findings.sh`) validates every line: one -JSON object, an `area`, a `date`, and either the four fields or `raw`. It prints -one line saying how far `docs-wiki/bug-patterns/` has fallen behind these -findings — see `make digest-status` for the breakdown. +`-.json`: the record's `date`, then a slug of its symptom +(lowercase words joined by hyphens, about 60 characters at most). The name is +for a human skimming `ls`; nothing parses it beyond the pattern check. If the +name is taken, add `-2`. -It runs in CI as of the change that added this sentence. It did not before, -for the two weeks this file claimed it did — which is the same shape as every -other finding here, so it is recorded rather than quietly corrected. +## Guard -The extraction was verified lossless by regenerating every table row from the -records and diffing against the original 630 — byte-identical as a multiset. -Order carries no meaning: these are looked up by matching a symptom. +`make check-findings` (`scripts/check-findings.sh`, run in CI) validates every +file: the name pattern, exactly one line, one JSON object, an `area`, a `date`, +and either the four fields or `raw`. It also **refuses any `*.jsonl` left in +this directory**. A branch written before the split brings its shard file back +on its next merge with `main`; `scripts/split-findings.py` converts the lines +into files, skipping those that already exist. It prints one line saying how +far `docs-wiki/bug-patterns/` has fallen behind these findings; see `make +digest-status` for the breakdown. ## On DuckDB -The files are newline-delimited JSON, which DuckDB reads in place over a glob — -no import, no schema, no build step. It is not vendored and not required: `grep` -plus `jq` covers the common lookup, and the guard above only needs `python3`. +DuckDB reads the files in place over a glob, with no import, schema or build +step: `select … from read_json_auto('.claude/skills/fix-issue/findings/*/*.json')`. +It is not vendored and not required: `grep` plus `jq` covers the common lookup, +and the guard above only needs `python3`. diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl b/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl deleted file mode 100644 index f78279461d..0000000000 --- a/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl +++ /dev/null @@ -1,173 +0,0 @@ -{"area": "cmd/mxcli", "date": "2026-07-30", "symptom": "`mxcli test` leaves the project mutated: Security Level changed, after-startup pointing at the deleted `MxTest.TestRunner`, and only a `Warning:` line about it. Or: an empty `MxTest` module accumulates after every run", "cause": "Three defects in one teardown. (a) `getAfterStartup` trimmed quotes *before* trailing punctuation, so a DESCRIBE SETTINGS line ending in `,` yielded `Module.Flow',` and the restore statement was unparseable; (b) cleanup dropped only the microflow, not the module it created; (c) the Security Level was forced OFF and restored to a hardcoded PRODUCTION", "file": "`cmd/mxcli/testrunner/runner.go` (`parseSettingValue`, `quoteMDLString`, `projectState`, `setupCommands`, `cleanupCommands`)", "insight": "Strip trailing `,;` before unquoting; re-emit via `quoteMDLString` (doubling embedded quotes, never backslashes); capture a `projectState` before the first mutation and restore from it; drop the module only when the run created it (a pre-existing `MxTest` is the user's); leave Security Level alone entirely; return cleanup errors instead of printing warnings, and fail the run when they occur. Command lists are pure functions so the restore is testable without a project or Docker. Issues #802/#803/#804", "refs": ["#802", "#803", "#804"]} -{"area": "cmd/mxcli", "date": "2026-07-31", "symptom": "`mxcli oql -p app.mpr \"…\"` fails against a `mxcli run --local` app — first `admin password required`, then (with the password) `OQL error: Action not found.` and 0 rows — even though the same query works under `mxcli docker up`", "cause": "Two-part gap. (a) The local runtime booted the JVM with no system properties, so `/dev/preview_execute_oql` was never mounted (docker mode passes `-Dmendix.live-preview=enabled -Dmendix.running.locally.by.studiopro=true` via docker-compose; the local boot did not). (b) `run --local` never printed or wrote the admin password, and `mxcli oql` errored instead of defaulting to it", "file": "`cmd/mxcli/docker/localboot.go` (`LocalRuntimeOptions.jvmArgs` — new) + `cmd/mxcli/docker/m2ee.go` (`resolveM2EEDefaults` token fallback) + `cmd/mxcli/docker/oql.go` (Action-not-found hint) + `cmd/mxcli/docker/runlocal.go` (banner)", "insight": "Always pass the two live-preview `-D` flags in `jvmArgs` (run --local is always DTAPMode=D, matching docker); default the oql admin token to `defaultLocalAdminPass` when nothing else supplies it (admin API is loopback-only); branch the \"Action not found\" hint to cover the `--local` case; print a `Query data: mxcli oql …` line in the run banner. Tests `TestJVMArgs`, `TestResolveM2EEDefaults_Defaults`. traceops #36", "refs": ["#36"]} -{"area": "cmd/mxcli", "date": "2026-07-08", "symptom": "CE0463 on an MDL-created **Combobox** (or other platform widget) at `mxcli docker build/check` time — but the SAME widget passes `mx check` on a project whose installed `.mpk` matches mxcli's embedded template (Mendix 11.6 / combobox 2.5.0). NOT the old incomplete-template bug (#112, fixed — a matching-version combobox is clean)", "cause": "Widget-VERSION mismatch: mxcli emits the embedded 2.5.0-shaped PropertyTypes, but the project has a NEWER combobox (e.g. 2.8.1) that reorders/regroups properties. `augmentFromMPK` patches presence + enum values but can't restructure the baseline; `GenerateFromMPK` is less faithful still (fails even on a matching version). The designed remediation is `mx update-widgets` (docker build/check runs it before `mx check`)", "file": "`cmd/mxcli/docker/check.go` + `build.go` (`updateWidgetsPathArg`) — the update-widgets *invocation*, not the widget emission", "insight": "The real trap was that `mx update-widgets ` **crashed** (`AddProjectDirAsAllowedPath` → `Path.GetDirectoryName(\"app.mpr\")` = \"\" → `System.ArgumentNullException`) and some mx builds exit 0 after printing it, so the migration silently no-op'd and CE0463 survived. Pass an **absolute** path to update-widgets (always has a directory component). `mx check` is unaffected. Diagnosis: run the bundled `mx update-widgets ` yourself — if it throws ArgumentNullException on AddProjectDirAsAllowedPath, the path lacks a dir. Faithful multi-version widget emission is the larger fix (#529). Issue #112; repro `mdl-examples/bug-tests/112-combobox-enum-ce0463-widget-version.mdl`", "refs": ["#112", "#529"], "ce": ["CE0463"]} -{"area": "cmd/mxcli", "date": "2026-07-31", "symptom": "`mxcli docker check`/`build` (or a bare `mx update-widgets`) **silently deletes `mprcontents/`** and rewrites an MPRv2 project into single-file v1 — `check` reports **0 errors** and looks successful, but the git working tree diverges from tracked files, a running `mxcli run --local` loop breaks (it watches `mprcontents/`), and Studio Pro may crash on open (`LibGit2RepositoryProvider.WriteBaseFile`). Triggered by following the CE0463 remediation on a `mxcli new` (always v2) project", "cause": "The pre-check `mx update-widgets` step (run to suppress false CE0463) **performs the conversion**: it inlines every unit into the `.mpr` (`Unit.Contents` column) and deletes `mprcontents/`. The `check` itself is read-only; `update-widgets` is the mutator. docker check/build invoked it with no storage-format protection", "file": "`cmd/mxcli/docker/update_widgets.go` (`runUpdateWidgets` / `snapshotStorageFormat`) — call sites in `check.go` and `build.go`", "insight": "Snapshot `.mpr` + `mprcontents/` to a temp dir before `update-widgets`, `defer restore()` after the check (restore removes the post-conversion single-file `.mpr` residue and puts the v2 tree back); MPRv1 projects need no protection. The check still runs against the widget-normalized model, so CE0463 stays suppressed — only the on-disk format is preserved. **Never tell an agent to run bare `mx update-widgets` on a v2 project** — the synced skills (`create-page.md`, `custom-widgets.md`, `migrate-design-prototype.md`, `download-marketplace-content.md`) + dev `debug-bson.md` route to `mxcli docker check`/`build` (v2-safe) instead. **Fix the operation, not the call site**: PR #764 wrapped the invocation inside `Check` only, and `Build` had a second bare copy — so `docker build`/`run`/`reload` kept converting projects for another 40 issues, until #808. The snapshot now lives in `runUpdateWidgets`, which is the only place that may exec `update-widgets`; grep for `\"update-widgets\"` should return exactly one hit. When a mutating external step is guarded, put the guard in a function that also *performs* the step, so a new caller cannot get it wrong. Issues #763 / PR #764, #808", "refs": ["#763", "#764", "#808"], "ce": ["CE0463"]} -{"area": "cmd/mxcli", "date": "2026-06-06", "symptom": "`mxcli -p \"C:\\\\path\\\\App.mpr\"` (Windows backslash path) fails with `unable to open database file (14)` and the echoed path is mangled (`C:\\temp` → `C:emp`, backslashes dropped); forward slashes (`-p C:/path/App.mpr`) work", "cause": "The CLI fabricates `CONNECT LOCAL ''` MDL from the flag value and re-parses it, so the lexer's `unquoteString` interprets `\\t`/`\\n`/`\\r` and escape backslashes in the path. The positional `.mdl` arg is a raw path, so it's unaffected", "file": "every `cmd/mxcli/*.go` `fmt.Sprintf(\"CONNECT LOCAL '%s'...\", projectPath)` site + `mdl/visitor/visitor_helpers.go`", "insight": "Never interpolate a raw filesystem path into MDL source. Wrap it in `visitor.QuoteString(projectPath)` (escapes `\\`→`\\\\`, `'`→`''` — the exact inverse of `unquoteString`) at every CONNECT-string site. Round-trip test in `mdl/visitor/visitor_connection_test.go`. Issue #644", "refs": ["#644"]} -{"area": "cmd/mxcli", "date": "2026-07-01", "symptom": "`mxcli oql` against a docker-deployed app returns **0 rows** for tables that clearly have data (false negative — tester concluded \"saving is broken\")", "cause": "NOT a database-config problem (that's a red herring — proven: aligning the runtime to Postgres with data still returned 0). The OQL preview servlet (`/dev/preview_execute_oql`) is registered by `com.mendix.basis.livepreview.LivePreviewRegistry` → `AppContainer.addDevelopmentServlet`, which **only mounts the servlet when the JVM system property `mendix.running.locally.by.studiopro=true` is set** (Studio Pro sets it; a deployed `bin/start` PAD doesn't). Without it the endpoint returns HTTP 200 `{\"result\":-5,\"message\":\"Action not found\"}`, which mxcli silently parsed as empty data. (Found by decompiling `com.mendix.mxruntime.jar` / `com.mendix.appcontainer.jar`.)", "file": "`cmd/mxcli/docker/templates/docker-compose.yml` (the `command`) + `cmd/mxcli/docker/oql.go` (`oqlDevError`)", "insight": "Add `-J -Dmendix.running.locally.by.studiopro=true` to the start command (each JVM opt needs its own `-J`), next to the existing `-Dmendix.live-preview=enabled` — this mounts the dev servlets and `mxcli oql` returns live data (verified end-to-end; DTAP mode is NOT required, DB config is irrelevant — the preview runs in-process against the runtime DataStore). Separately, make `oqlDevError` surface `{\"result\":<0,\"message\":…}` (not just `{\"error\":…}`) so the failure isn't swallowed as 0 rows. **Related startup bug found:** the generated `etc/Default` inherits the project's ports, which are `0` when unset → the runtime rejects port 0 and won't start; `patchRuntimePorts` (patch.go) forces `runtime.http { port = 8080 }` / `admin { port = 8090 }`. Verified with a full docker build/run/seed cycle"} -{"area": "cmd/mxcli", "date": "2026-07-25", "symptom": "`mxcli run` warm-loop ergonomics from Sudoku findings: (a #17) a **relative** `-p` fails with MxBuild's raw \"should be an absolute path\" + a Windows JSON sample; (b #15/#23) a **build failure in the watch loop** (incl. a SCSS compile error like `Expected expression. _x.scss 180:35`) printed only the generic `build failed: `, swallowing the real detail", "cause": "(a) `cmd_run.go` passed `-p` straight through without `filepath.Abs`; (b) the watch path at `runlocal.go` printed only `build.Message` while `build.Raw` (the full serve `/build` body, which the cold-build path already prints) held the compiler/model detail", "file": "`cmd/mxcli/cmd_run.go` + `cmd/mxcli/docker/runlocal.go` (watch build-failed branch)", "insight": "(a) `projectPath, _ = filepath.Abs(projectPath)` after reading the flag; (b) also print `strings.TrimSpace(string(build.Raw))` (indented) when it differs from `build.Message`, matching the cold path. **Diagnosis pattern**: when a dev-loop error is unhelpfully generic, check whether a `Raw`/full-body field is already captured and just not printed on that code path", "refs": ["#15", "#17", "#23"]} -{"area": "cmd/mxcli", "date": "2026-07-22", "symptom": "Editing `themesource/**/main.scss` (or `theme/web/main.scss`) while `mxcli run --local` serves on `:8080` keeps showing **old styles** — reads exactly like a stale compiled-CSS cache. `rm -rf theme-cache/ .mendix-cache/ deployment/` \"fixes\" it only because a restart came with it", "cause": "THREE distinct causes, none a CSS cache: (1) **no `--watch` = no watcher at all** — `mxbuild --serve` only rebuilds on a `/build` request (startup, or a watch tick), so a save changes nothing; (2) **stale process silently adopted** — a leftover serve/runtime on the ports answers the startup readiness probes (`waitReady`/`waitAdminReady` only check \"port answers\"), so a new run attaches to the OLD process and its own child is torn down by `defer`; a backgrounded `run --local` whose wrapping shell exited non-zero dies while its serve+runtime keep serving; (3) the theme source was **watched by nothing** — the `--watch` signal was model-only (`.mpr`+`mprcontents/`). The incremental theme step itself is FINE: one `/build` after an scss **content** edit does rewrite `theme-cache/web/theme.compiled.css` (verified), so there is no cache to clear", "file": "`cmd/mxcli/docker/runlocal.go` — `checkTargetPortsFree` (guard), `themeSourceMTime`/`sourceMTime` (watch signal), `watchAndApply` (generation log)", "insight": "(2) Refuse to boot when `:8080/:8090/:6543` already answer, with an actionable message (never auto-kill — user's call). (3) Add `theme/`+`themesource/` scss/css/js to the `--watch` mtime-poll signal (`sourceMTime` = max(model, theme)); poll-based so it's container-safe (unlike the rollup chokidar/inotify web-client watcher). Log a build-generation counter (`build #2`) so \"did it take?\" is answerable. Docs: `docs-site/src/tools/run-local.md` + skill `run-local.md` — \"SCSS needs a rebuild (`--watch` or clean restart), never a cache-clear; kill the old serve/runtime first\". Tests `TestThemeSourceMTime_WatchesThemeAndThemesource`, `TestCheckTargetPortsFree`", "refs": ["#2"]} -{"area": "cmd/mxcli", "date": "2026-07-26", "symptom": "`mxcli run --local`: when a page action throws, the browser shows the generic Mendix error dialog and there is nothing to correlate it against — the runtime's own stdout/stderr (server stack trace, microflow `LOG` output) is swallowed, so a server-side bug can't be told apart from a client one", "cause": "The runtime JVM was spawned with `cmd.Stdout=log; cmd.Stderr=log` where `log` is an in-memory `syncBuffer` surfaced only on a *startup* failure; during normal operation it goes nowhere on disk", "file": "`cmd/mxcli/docker/localboot.go` (`spawnAndConfigure`, `openRuntimeLog`, `LocalRuntime.logFile`, `LocalRuntimeOptions.RuntimeLogPath`) + `cmd/mxcli/docker/runlocal.go` (default `/.mxcli/runtime.log`) + `cmd/mxcli/cmd_run.go` (`--runtime-log`)", "insight": "Tee the JVM's stdout+stderr to `/.mxcli/runtime.log` via `io.MultiWriter(log, file)` (the in-memory buffer still backs startup-error reporting). Append across restarts with a `=== runtime start … ===` marker; close the handle on Stop/reopen. Default on; `--runtime-log ` relocates, `-` disables. Print the path at boot. Test `TestOpenRuntimeLog`. Findings #25", "refs": ["#25"]} -{"area": "cmd/mxcli", "date": "2026-07-26", "symptom": "Follow-up to the above (#25 re-test): `run --local` writes `runtime.log` but it stays **nearly empty** — the JVM tee captures startup/JVM output only; **application** logs (microflow `LOG`, server-side exception stack traces) never reach stdout, so a page-action error still can't be diagnosed", "cause": "A standalone runtime (launched via `runtimelauncher.jar`) attaches **no log subscriber** by default — unlike a Studio Pro / m2ee run, which calls `create_log_subscriber` **after** start. Mendix application logs flow to log *subscribers*, not stdout, so with none attached they go nowhere", "file": "`cmd/mxcli/docker/runtime_controller.go` (`RuntimeController.LogSubscriberFile`/`Stdout`, `attachFileLogSubscriber`, called at the end of `Start`) + `cmd/mxcli/docker/localboot.go` (`StartLocalRuntime` sets `ctrl.LogSubscriberFile` to the abs runtime-log path)", "insight": "After a successful `start` (and on every restart's `Start`, since each fresh JVM has no subscriber), call the `create_log_subscriber` admin action with `{type:\"file\", name:\"mxcli-run-local\", autosubscribe:\"INFO\", filename:, max_size:1GiB, max_rotate:0}`. **`max_rotate:0` is load-bearing**: the JVM stdout tee holds an fd on the same file, and a rotate-rename would detach it. Best-effort (a logging failure must not fail an up runtime — warn to Stdout instead). Pass an **absolute** path (the runtime's cwd is `/runtime`, not mxcli's). Tests `TestStart_AttachesLogSubscriber`, `TestStart_NoLogSubscriberWhenUnset`, `TestStart_LogSubscriberFailureNonFatal`. Findings #25 (round 2)", "refs": ["#25"]} -{"area": "cmd/mxcli", "date": "2026-07-26", "symptom": "Follow-up to round 2 (#25 re-test): the subscriber is registered but `runtime.log` **still** holds only the 4 JVM-banner lines — a probe microflow's `log info/warning/error` and a forced runtime exception produce **zero** lines. Isolation test: boot+subscriber → 0 probe lines; then call `start_logging` → 6 lines", "cause": "A standalone runtime boots with logging **not started**, so a registered subscriber sits **inert** — nothing is delivered until the logging subsystem is activated. `create_log_subscriber` alone is necessary but not sufficient", "file": "`cmd/mxcli/docker/runtime_controller.go` (`configureRuntimeLogging` — renamed from `attachFileLogSubscriber`; now also calls `start_logging`)", "insight": "After `create_log_subscriber`, call `CallM2EE(c.opts, \"start_logging\", nil)` in the same step (order: create subscriber → start_logging). Treat an \"already started\" response as success (`start` re-runs on the DB-update retry / restart paths on a still-running JVM). Still best-effort (warn to Stdout, never fail an up runtime). Test `TestStart_StartLoggingAlreadyStartedIsSuccess`; `TestStart_AttachesLogSubscriber` now asserts the `[start, create_log_subscriber, start_logging]` sequence. **Diagnosis pattern**: when a registered sink receives nothing, check whether the subsystem that feeds it is even *running* — registration ≠ activation. Findings #25 (round 3)", "refs": ["#25"]} -{"area": "cmd/mxcli", "date": "2026-08-01", "symptom": "`mxcli new --version X` prints \"Resolving MxBuild X...\" and then produces a project at a **different** Mendix version — silently. Every later step (init, mxbuild, runtime, `run --local`) follows the wrong version", "cause": "`ResolveMxForNewProject` delegated to `ResolveMxForVersion`, whose last resort is `AnyCachedMxPath()` — *any* cached mx, of any version. That fallback is fine when the project already exists and its version is a preference; for `new` the requested version **is** the output, because `mx create-project` stamps the project with the version of the binary that ran it", "file": "`cmd/mxcli/docker/check.go` (`localMxForVersion`, `ResolveMxForNewProject`) + `cmd/mxcli/cmd_new.go` (postcondition)", "insight": "Resolve **exactly** the requested version for `new` (exact Studio Pro install → exact versioned install path → exact download cache; **not** PATH, which carries no version guarantee), and download otherwise. Then check the postcondition: reopen the created `.mpr`, compare `ProductVersion` to `--version`, and fail loudly on a mismatch — resolution bugs are invisible without it. **Generalisable**: when a flag names the version/identity of the artifact being produced, a \"close enough\" local substitute is never valid, and the produced artifact should be verified against the request rather than the resolution trusted. Found while reproducing #812 in a browser — cost a full project rebuild before it was noticed", "refs": ["#812"]} -{"area": "cmd/mxcli", "date": "2026-08-11", "symptom": "`mxcli syntax ` teaches MDL that does not parse — `Binds:` (removed in favour of `Attribute:`, and hard-rejected by the parser), workflow decision outcomes without the `->` arrow, `ALTER WORKFLOW … SET DUE DATE = '…'` (no `=`), `INSERT AFTER ` (operands reversed), `IMAGE 'name'` (identifier, not string), a `BEFORE` that does not exist. Nothing failed, because nothing checked", "cause": "The registry (`cmd/mxcli/syntax/features_*.go`) is hand-maintained while the grammar moves underneath it, and every existing test checked *structure* — fields populated, aliases resolve, see-also targets exist — never whether the documented MDL is real. 26 of 120 entries had a non-parsing example", "file": "`cmd/mxcli/syntax/example_parses_test.go` (new guard), plus corrections across `features_page.go`, `features_workflow.go`, `features_integration.go`, `features_microflow.go`, `features_misc.go`", "insight": "Parse every `Example` and fail the build if it does not. Examples come in several legitimate shapes, so each blank-line-separated **block** is tried as a statement, a microflow activity, a page widget, a workflow activity, and a retrieve clause — a failure means it parses as none of them. Only `Example` is checked; `Syntax` carries metasyntax (`[OR MODIFY]`, ``) by design. **Generalisable — the shape to look for**: documentation that is *data in the binary* can be executed against the real parser, which turns \"the docs drifted\" from a review problem into a test failure. The same trick applies to any embedded example corpus (skills, `--help` text, README snippets extracted at build time). **Why it matters more than it looks**: the registry is the first surface an agent consults, so a gap there does not read as \"undocumented\", it reads as \"unsupported\" — a contact-management app built with mxcli worked around three features that already existed, including replacing a `SAVE_CHANGES CLOSE_PAGE` button with a bespoke microflow because only the two halves were listed separately. **Close the opt-out** — a guard that skips empty input is a guard you can silence by emptying the field; `TestFeatureFieldsPopulated` already rejects a blank `Example`, and the new guard additionally fails an example that is all comments"} -{"area": "cmd/mxcli", "date": "2026-08-06", "symptom": "SCSS written to **`themesource//web/main.scss` never reaches the app** — no error, no warning, the build succeeds and the rules are simply absent from `theme-cache/web/theme.compiled.css`. Looks exactly like an SCSS cache problem, so the usual reflex (`rm -rf theme-cache/`) wastes the session", "cause": "A theme source folder is only compiled when `` matches a **real module in the model**. mxbuild walks the model's modules and pulls each one's `themesource//web/main.scss`; it never globs the `themesource/` directory, so an invented folder (`themesource/my_theme/`) is silently skipped. Verified on 11.13: a probe rule in `themesource/myfirstmodule/` compiled, the identical rule in `themesource/mxcli_theme/` did not", "file": "`cmd/mxcli/theme/theme.go` (package doc records the compile order); the target paths live in `cmd/mxcli/theme/assets//files/`", "insight": "Put app-level styling in **`theme/web/`**, not in an invented theme source folder: `theme/web/main.scss` is compiled **last** — after Atlas Core *and* after every module theme source — so a partial imported from it overrides any Atlas rule without `!important`. Use a module's theme source only when the styling genuinely belongs to that module (it exports with the `.mpk`). **Generalisable — the shape to look for**: when CSS \"doesn't apply\", first prove the file is *compiled at all* (grep a unique probe selector in `theme-cache/web/theme.compiled.css`) before debugging specificity or caches — absent and overridden look identical in the browser. Note also that `theme/web/custom-variables.scss` is imported once **per module**, so it must hold declarations only; a rule there is emitted N times"} -{"area": "cmd/mxcli", "date": "2026-08-06", "symptom": "Text painted by an **Atlas topbar widget is invisible in a dark theme** — the language selector measures ~1.13:1 contrast, glyph pixels spanning 4 luminance values out of 255. A theme override exists and *names the right element*, so it looks handled", "cause": "Two separate mistakes stacked. (1) **Specificity**: Atlas's own rule is `.navbar-brand .widget-language-selector .current-language-text` at (0,3,0); a bare `.current-language-text` at (0,1,0) never wins, and only appears to on layouts that do not nest the selector under `.navbar-brand`. (2) **Wrong value**: `color: inherit` inherits *body ink*, which is dark, while the rail is dark in both palettes — so even at the winning specificity it measures 1.00:1. Atlas paints from `--bg-color-secondary` with a `#fff` fallback because it assumes a dark rail", "file": "`cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-atlas-map.scss` (the \"Atlas fixups\" block)", "insight": "Re-declare **Atlas's own selector shape** so the weights match and source order decides, and resolve the colour through the rail token (`var(--mxt-rail-ink-active, var(--mxt-rail-ink))`) rather than `inherit`. List the bare and the `.navbar-brand`-nested selectors together — each is matched at its own specificity, so one rule covers both layouts. **Generalisable — the shape to look for**: a guard that names the right element is not evidence it applies. Read the *winning* declaration (`CSS.getMatchedStylesForNode` in DevTools, or the computed value) instead of the one you wrote. **Measure contrast, not colour**: reading `getComputedStyle(el).color` once and seeing a plausible value proves nothing — compute the WCAG ratio against the first non-transparent ancestor background, which is what turns \"looks fine\" into 1.13 vs 19.47. Reported from the RssReader test build; tests in `cmd/mxcli/theme/theme_test.go`, verified in a browser at 17.79:1 light / 19.47:1 dark"} -{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "A themed app is on-palette everywhere except a few widget details — the **Data Grid 2 pager caption is invisible** (1.02:1 on a dark ground), row-select checkboxes stay stock Mendix blue, popovers cast light-mode shadows. Re-pointing tokens changes nothing, and the same widget's other parts (the pager *buttons*) are fine", "cause": "The theme source shipped by the **widget modules** (`themesource/datawidgets`, `atlas_web_content`) styles some things with Sass variables and literals — `datawidgets/web/variables.scss:18` is `$pagination-caption-color: #0a1325`. Sass resolves those at compile time, before any custom property exists, so the value is baked into `theme.compiled.css` and no `--mxt-*` can reach it. The parts that *do* work resolve `var(--gray-darker, …)` through Atlas: same bar, two mechanisms", "file": "`cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-widgets.scss` (the shared widget layer, imported after the theme partial)", "insight": "Add a CSS rule per baked declaration, resolving through a token so both palettes follow. **The obvious fix does not work**: each module's `main.scss` imports `theme/web/custom-variables` before its own `!default` vars, so `$pagination-caption-color: var(--mxt-ink-muted)` there *would* win — but (1) the names collide with Atlas Core's, which feeds them to Sass colour functions (`atlas_core/web/_variables.scss:20` computes `mix($brand-primary, #e7e7e9, 10%)`; handing `mix()` a `var()` is a compile error) and (2) the worst offenders are not behind a variable at all — `_three-state-checkbox.scss` writes `#264ae5` and `rgba(#264ae5, 0.4)` directly. **Generalisable — the shape to look for**: read the **compiled CSS, not the SCSS**, when deciding what to override. The sources are full of `var(--token, #fallback)` declarations that already resolve correctly; in one measured app `#264ae5` appeared in 46 declarations and **24 were harmless fallbacks**, so grepping the source would have produced twice the rules for no benefit. Reported from the Formula1 test build (§33); verified in a browser: pager caption 1.02:1 → 6.99:1 console dark, 6.39:1 light, 6.78/5.93 on signal", "refs": ["#0", "#264"]} -{"area": "cmd/mxcli", "date": "2026-08-06", "symptom": "`mxcli oql` silently omits a whole column: a `select A, B …` renders only `A`, and the JSON output has no `B` key at all — no error, no empty column. Reproduces whenever the value of `B` is **null in the first row**", "cause": "The runtime omits a null-valued column from a row's JSON object entirely, and `parseOQLFeedback` took the column list from `extractColumnOrder(rows[0])` — one row, chosen for its key *order*. Every later row was then projected onto that short list, so the column vanished from the result rather than showing NULLs", "file": "`cmd/mxcli/docker/oql.go` (`parseOQLFeedback`, `mergeColumnOrder`, `hasOnlyKnownKeys`)", "insight": "Union the keys of **all** rows, inserting each new key directly after the last key already known rather than appending — merging `[A, C]` with `[A, B, C]` must give `[A, B, C]`, not `[A, C, B]`, or a column that is null early in the result set jumps to the end of the table. The per-row re-scan is skipped when a row carries no unseen key, so the uniform case still costs one length check. **Generalisable**: any \"take the shape from the first element\" over a sparse encoding is a silent-wrong-answer bug, not a formatting bug — the output looks complete. Tests `TestParseOQLFeedback_ColumnUnionAcrossRows`, `TestMergeColumnOrder`; proven by stubbing the union back to first-row-only and watching the reported symptom return. sudoku #39, first half", "refs": ["#39"]} -{"area": "cmd/mxcli", "date": "2026-08-06", "symptom": "`mxcli test` cannot run at all in a container without a Docker daemon — parsing, runner generation, model injection and the **entire mxbuild build** succeed natively, then `docker up` fails with \"failed to connect to the docker API at unix:///var/run/docker.sock\". So microflow tests are unavailable in exactly the environment mxcli targets (Claude Code web containers ship `/usr/bin/docker` with no daemon)", "cause": "Only one step of the run was containerised — start the runtime and read its log — but it was wired directly to `docker compose`, with no seam for another way to run the app. `run --local` had all three pieces already (boot a standalone runtime, tee its log, restore)", "file": "`cmd/mxcli/docker/localapp.go` (new `StartLocalApp`), `cmd/mxcli/testrunner/runner_local.go` (new), `cmd/mxcli/testrunner/runner.go` (docker path extracted to `runDockerAndCapture`), `cmd/mxcli/cmd_test_run.go` + `main.go` (`--local`)", "insight": "Give the run a seam — `runLocalAndCapture` / `runDockerAndCapture` — so both modes share parse/inject/parse-results/cleanup and differ only in how the app is started. **Two traps, both found by running it rather than reasoning about it.** (a) The runner reports via an **after-startup** microflow, so its LOG output happens DURING the start action, before the runtime's log subscriber attaches; registering the subscriber early is not possible (the runtime answers `LoggingException` pre-start). What actually carries the output is the JVM console tee, live from spawn — verified by A/B running with the early attach removed. (b) A **failing** test makes the runner return false, which makes the after-startup action fail, which makes `start` return an error — the first version reported that as a broken run and dumped a stack trace instead of the test report. A failed boot whose log shows a verdict is a normal outcome. Local runs use their own ports (8081/8091) and a `_test` database so a `run --local` dev loop can keep serving. Verified end-to-end in a daemon-less container: 2 passed; then 1 passed / 1 failed with exit 1, project restored. sudoku #41", "refs": ["#41"]} -{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "A solution's apps are served on one host with different ports, so they share a cookie jar (cookies key on host name and **ignore the port**) — logging into one silently replaces the other's `XASSESSIONID`. Giving each app a host name in **App Settings -> Configurations -> Application root URL** appears to do nothing under `run --local`", "cause": "`runtimeConfigParams` builds the entire boot `update_configuration` payload from `LocalRuntimeOptions`, and `ApplicationRootUrl` was only ever populated from a `--hub` registration. The model's own value had no path into the payload — and since the admin action REPLACES rather than merges, nothing else could supply it either", "file": "`cmd/mxcli/docker/runlocal.go` (`configuredApplicationRootURL`, `applicationRootURLFrom`, `customHostRootURL`)", "insight": "Read the setting off the project at boot and use it when no hub URL was assigned (hub wins — that URL is the one actually serving the app). **The trap is that a blank Mendix app already ships `ApplicationRootUrl = http://localhost:8080/`**, so \"is set\" does not mean \"was chosen\": honouring every value would change behaviour for every existing project and, under `--app-port`, advertise a port the app is not serving on. Only a **non-loopback host** is passed through, and a port that disagrees with `--app-port` warns. Serving under the host name needs no flag at all — the runtime accepts any `Host` and the client uses relative URLs (verified: 200 via `/etc/hosts`, nip.io and localtest.me); the setting matters only for the **absolute** URLs Mendix generates. **Generalisable**: before defaulting from a model setting, check what a blank project already has in it — a non-empty default makes \"fall back to the model\" a behaviour change, not a no-op. Verified end-to-end on 11.12.1: with `backend.local` configured, boot prints `Application root URL from configuration \"Default\"` and the app answers 200 on both the host name and the listen address. Tests `TestApplicationRootURLFrom`, `TestCustomHostRootURL`"} -{"area": "cmd/mxcli", "date": "2026-08-07", "raw": "| The SessionStart hook `mxcli init` writes cannot survive an idle reap: it is guarded on `test -x ./mxcli`, and `.gitignore` excludes that binary (~85 MB) on purpose. The container is reclaimed, the repo re-cloned without it, the guard fails and the hook no-ops through `\\|\\| true` — the next session has no mxcli, no MxBuild cache, no database, and no message saying so | The hook inlined the whole bring-up in one shell line, so \"binary missing\" could only be expressed as \"skip\". A hook line cannot reasonably do OS/arch detection and a download; something committed has to | `cmd/mxcli/init_hook.go` (`bootstrapScriptTemplate`, `writeBootstrapScript`, `sessionStartHookCommand`, `sessionStartHookMarkers`) | Emit a committed `.claude/bootstrap-mxcli.sh` that resolves OS/arch, fetches the binary when absent (`MXCLI_TAG` to pin), then runs the setup; the hook becomes `sh .claude/bootstrap-mxcli.sh \\|\\| true`. **Changing the hook command breaks dedupe**, which matched on the old command string — so `addSessionStartHook` now recognises *any* known marker and **rewrites the entry in place**, migrating an old project instead of leaving it with two hooks that both run. **Generalisable**: a guard whose condition is something you deliberately do not commit is a silent no-op waiting for a fresh clone — make the guard able to satisfy itself. Verified by reproducing the reap: moved `./mxcli` out of the project, ran the hook command verbatim, watched it re-download (88 MB, new mtime) and finish with \"Setup complete … database ready\". Tests `TestAddSessionStartHook_MigratesLegacyCommand`, `TestEnsureSessionStartHook_WritesFile`. mxcli-todo #2 |", "refs": ["#2"]} -{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli exec -p app.mpr - <<'EOF' … EOF` fails with `Error reading file: open -: no such file or directory` — `-` is taken literally as a filename, so MDL cannot be piped or written as a heredoc and every ad-hoc script needs a temp file first", "cause": "`exec` (and `check`) called `os.ReadFile(path)` directly, with no case for the conventional stdin spelling", "file": "`cmd/mxcli/mdlsource.go` (new `readMDLSource`, `mdlSourceLabel`), `cmd/mxcli/cmd_exec.go`, `cmd/mxcli/cmd_check.go`", "insight": "One helper both commands share, so `check` gained the same spelling rather than only the reported one; `check` reports the source as `` instead of a bare `-`. Verified live: a heredoc through `exec` and a pipe through `check` both run. Tests `cmd/mxcli/mdlsource_test.go`. mxcli-todo #5", "refs": ["#5"]} -{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli syntax` documents spellings the parser rejects, so an agent following the reference writes MDL that fails — `TEXTBOX … (Binds: Attr)` (\"'Binds:' is no longer supported, use 'Attribute:' instead\") and `DataSource: MICROFLOW Module.MF()` (a zero-arg microflow datasource takes NO parens, unlike RETRIEVE/CALL)", "cause": "Nothing checks the `syntax` corpus against the parser. `make check-skill-mdl` validates MDL blocks in the skills and the docs site, but the `Syntax`/`Example` strings in `cmd/mxcli/syntax/*.go` are not covered, so a retired spelling can sit there indefinitely", "file": "`cmd/mxcli/syntax/features_page.go` (10 × `Binds:` → `Attribute:`, the datasource parens), `cmd/mxcli/syntax/retired_spellings_test.go` (new guard)", "insight": "Fix the text **and** pin it: a table-driven test fails if a retired spelling reappears in any topic's Syntax or Example. It is a spelling guard rather than a parse — the snippets are fragments (a DATAVIEW body, a property line) that do not stand alone as statements, so they cannot just be fed to the parser. Proven by reintroducing `Binds:` and watching the test name the topic and field. **A third claim in the same report did not reproduce**: `CONTAINER (OnClick: SHOW_PAGE M.P(Param: $currentObject))` parses fine on current main, so only the two verified ones were changed. mxcli-todo #8", "refs": ["#8"]} -{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli test … --local` (or any other `StartLocalApp` caller) fails with MxBuild's `the project file path should be an absolute path`, followed by a page of Windows sample requests, whenever `-p` is given a **relative** path", "cause": "`ServeServer.Build` forwarded `ProjectFilePath` verbatim. `mxcli run` had learned to absolutize at the CLI layer (findings #17), but that fix lived in `cmd_run.go`, not in the code that talks to MxBuild — so the next caller re-hit it", "file": "`cmd/mxcli/docker/mxserve.go` (`ServeServer.Build`)", "insight": "Absolutize `req.ProjectFilePath` in `Build` itself, the single place that talks to MxBuild, so no future caller can miss it; also resolve `LocalAppOptions.ProjectPath` in `applyDefaults` so `DeployDir` and the runtime log path are not derived from a relative value. Test by pointing a `ServeServer` at an `httptest` fake and asserting on the request body — the CLI-layer fix cannot be tested that way, which is part of why it did not generalise", "refs": ["#17"]} -{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli test --attach` fails with `reload_model failed: Authentication failed.` — after the test microflows have already been injected into the project", "cause": "The M2EE admin API and the test endpoint are **different secrets**. `attach` built its `RuntimeController` with `M2EEOptions{Token: hs.Token}` — the endpoint token — instead of the runtime's admin password", "file": "`cmd/mxcli/testrunner/runner_attach.go` (`attach`), `cmd/mxcli/testrunner/handshake.go` (`Handshake`)", "insight": "Carry `AdminPass` in the handshake alongside `Token` and pass that to `M2EEOptions`. The hosting `run --local` publishes it via `docker.LocalAppInfo` (the resolved value, not the package default, so a `--admin-pass` override still works). Whenever one process drives another's M2EE API, check which credential is being passed — `defaultLocalAdminPass` and any app-level token are unrelated"} -{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "\"Contrast is low and not everything uses the dark theme\" — and switching theme does not help, because `signal`, `ledger` and `console` all render the same defects. The worst of it is the **login page**: Atlas's stock photograph fills half the viewport on a dark app, and the Sign in button is green while the app's primary button is the brand colour", "cause": "The login page is served from `theme/web/login.html`, which loads the SAME compiled theme CSS (`{{themecss}}`) — so it IS themeable, but nothing themed it. Two Atlas rules do the damage: `.loginpage-image` layers a brand-tinted gradient over `url(\"./resources/work-do-more.jpeg\")`, and the submit button is `.btn-success`, so it follows the **success** colour rather than the brand. Separately, `--link-color` was mapped straight to the brand, and console's light-variant teal is 3.74:1 on white — under AA for body text", "file": "`cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-atlas-map.scss` (login block + `--mxt-link` indirection, all three copies stay byte-identical), `cmd/mxcli/theme/assets/console/.../_mxcli-console.scss` (light `--mxt-link`)", "insight": "Replace the photo with a token-built gradient and point the login button at the brand; give link TEXT its own `--mxt-link` token defaulting to the brand, because a link needs 4.5:1 as text while a brand used as a button FILL only needs 3:1 plus contrast against its own ink — darkening the brand everywhere would have been the wrong lever. Console light gets `#0f766e` (5.47 / 5.10 / 4.92 against surface / ground / surface-alt). **The project's own `theme/web/logo.png` is deliberately left alone** — it is the app's asset to replace, and hiding it would strip a real logo from apps that have one. **Verification boundary, stated plainly**: verified at the compiled-CSS layer (the overriding `.loginpage-image` is last and carries no photo; the button and link declarations resolve), NOT in a browser — raising the scratch project's security level to serve a login page surfaced pre-existing model errors there that block deploy. The `#999` empty-state label could not be reproduced: every `#999` in the compiled CSS is a Bootstrap default (popover arrows, modal border, print styles), so that one needs the reporter's app. mxcli-todo #19", "refs": ["#0", "#19", "#999"]} -{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "A login that is known to be correct starts failing on `run --local` — the page says only **\"Sign in failed\"**, as if the password were wrong. `--screenshot-user` silently produces screenshots of the login page instead of the requested page", "cause": "The local runtime is unlicensed, and an unlicensed runtime caps concurrent sessions. Past the cap it refuses the sign-in and logs `Maximum number of sessions exceeded! (You are currently using a trial license)` to `.mxcli/runtime.log` only; the page reports the refusal identically to a bad password. mxcli's own login helper never noticed either — it filled the form, waited, and saved whatever session it had", "file": "`cmd/mxcli/docker/screenshot_login.go` (`loginScript` failure detection, `loginFailureHint`, `readLogTail`), `cmd/mxcli/docker/runlocal.go` (passes `RuntimeLogPath`), `.claude/skills/mendix/run-local.md`", "insight": "Not an mxcli defect in the model layer — but mxcli held both halves and joined neither. Mendix answers a rejected sign-in by re-rendering the same form, so **the username field still being present after the click is the signal** that login did not complete; on that signal read the tail of the runtime log and name the cap. **Generalisable**: when a browser-driven helper \"succeeds\" with a degraded result, look for a server-side log that already says why — the page is often the least informative witness. Sessions are released by restarting `run --local`; a browser-driving script should sign out at the end, or the fifth or sixth run is the one that fails and it looks like a regression. Tests `TestLoginFailureHint`, `TestReadLogTail`. mxcli-todo #16", "refs": ["#16"]} -{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "A fresh clone of a project created by `mxcli new` goes dirty the first time anyone builds it: ~50 **tracked** files modified that nobody edited — every `javascriptsource/*/actions/*.js` gains a banner, `import { Big } from \"big.js\"` and `export async function`, plus the matching `javasource` stubs. In a cloud session with a stop-hook git check it reads as \"uncommitted changes\" at the end of clean work", "cause": "The template ships the generated action stubs in a slightly older shape and MxBuild rewrites them all on the first build. `mx check` does **not** — only a build does — so nothing before the first `run --local` could reveal it, which is after the user has already committed", "file": "`cmd/mxcli/docker/settle.go` (`SettleGeneratedSources`), `cmd/mxcli/cmd_new.go` (step 5/6, `--skip-build`), `cmd/mxcli/init.go` (`/theme-cache/` in the generated ignore list)", "insight": "Fix the *timing*, not the content: run the build while the project is still being created, so the settled form lands in the first commit. Do **not** reimplement the rewrite — it is mxbuild's generator and version-specific; run the real thing. Best-effort by contract (no JDK, no mxbuild, failed build → warning, never a failed creation), because a settled tree is a nicety and a usable project is the deliverable. The other half is gitignore: `theme-cache/` is a cache and says so. A/B on 11.12.1, both git-init'd then built: `--skip-build` → 50 dirty files, default → **0**. Tests `cmd/mxcli/docker/settle_test.go`. mxcli-todo #7", "refs": ["#7"]} -{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli init` run from a solution root (several app folders, no `.mpr` at the root) reports success and writes tooling that points at `project.mpr` — a file that does not exist. Nobody is told which project it picked, because it did not pick one", "cause": "`findMprFile` looks only in the target directory; an empty result fell through to a hardcoded `\"project.mpr\"` default and initialisation continued as if that were a real project", "file": "`cmd/mxcli/init.go` (`findMprFilesInSubdirs` + the three-way branch on the candidate count)", "insight": "Look one level down, then branch on the **count**: 0 → warn that generated paths will be placeholders; 1 → announce the project and initialise **that** directory; 2+ → refuse, list them, and print the exact command naming one. One level only — a Mendix app keeps its `.mpr` at its own root, and walking deeper starts finding deployment copies and backups. Candidates are sorted so the refusal and its suggested command are stable rather than directory-order dependent. **Generalisable**: a \"sensible default\" that names a file which does not exist is not a default, it is a silent wrong answer — count the candidates and let the count choose the behaviour. Tests `cmd/mxcli/init_discover_test.go`. mxcli-formula1 #3", "refs": ["#3"]} -{"area": "cmd/mxcli", "date": "2026-08-07", "raw": "| `ALTER MODULE X ADD JAR DEPENDENCY (…)` succeeds, `list jar dependencies` reports it, the build is **green** — and the runtime throws `SQLException: No JDBC driver found in app for URL`. `deployment/build.gradle` has no dependencies block and `find deployment -iname '**'` returns nothing | Not a bad write. Declaring and resolving are **separate steps**: the model records the coordinate, and `mx sync-java-dependencies ` is what downloads it into `vendorlib/`. Studio Pro runs that when you edit Module Settings; nothing headless was running it. Confirmed on 11.12.1 — a full `mxbuild --target=deploy` resolves nothing, and the sync command then fetches the jar | `cmd/mxcli/docker/javadeps.go` (`SyncJavaDependencies`, `UnvendoredJarDependencies`), `cmd/mxcli/cmd_sync_java_deps.go` (`mxcli sync-java-deps [--check]`), `cmd/mxcli/docker/runlocal.go` (vendors before boot), `mdl/executor/cmd_modules.go` (`warnUnvendoredJarDependencies`) | **How to find the missing step**: the reporter's open question was \"does mxbuild skip Maven resolution, or does mxcli write it somewhere MxBuild cannot read?\" — neither. `strings mx.dll | grep -i dependenc` surfaced `ISyncJavaDependenciesRunner`/`SkipManagedDependencySync`, and `mx --help` listed `sync-java-dependencies`. When a model-level write \"works\" but the artefact never appears, check whether the **toolset** has a separate command for it before suspecting the write. Wired at three levels so the gap cannot stay silent: the executor says so the moment it writes an unvendored coordinate, `run --local` resolves it before boot, and `--check` exits non-zero as a build gate. Resolution needs network, so every call site is best-effort with an actionable message. Tests `cmd/mxcli/docker/javadeps_test.go`. mxcli-formula1 #12 |", "refs": ["#12"]} -{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli test tests/ -p app/App.mpr` fails with \"no such file or directory\" for a `tests/` that sits right next to the `.mpr`", "cause": "Test paths resolved against the process CWD only. Defensible in isolation, but mxcli otherwise encourages naming the project (`-p`) rather than standing in its directory, and project auto-discovery searches outward — so the two conventions collide and the failure looks like a missing directory", "file": "`cmd/mxcli/cmd_test_run.go` (`resolveTestPaths`)", "insight": "Fall back to project-relative **only when the CWD-relative path does not exist**: a `tests/` in both places must resolve to the one the user is standing in, since silently preferring the project's copy would run the wrong suite. A path that exists in neither is passed through unchanged so the error names what was typed, not a rewritten path the user never mentioned. Tests `cmd/mxcli/cmd_test_run_paths_test.go`. mxcli-formula1 #13", "refs": ["#13"]} -{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "A test annotated `@cleanup rollback` (or with no `@cleanup` at all — rollback is the documented default) still leaves its rows in the database; a misspelled strategy like `@cleanup rollbak` does the same, silently, while the run reports PASS", "cause": "`TestCase.Cleanup` was parsed and then used nowhere. The after-startup runner had no seam to implement it — tests run inside the startup action, so there is no context the runner owns. The test endpoint creates that seam: it builds the `IContext` each test runs on", "file": "`cmd/mxcli/testrunner/endpoint.go` (the handler's execute block), `cmd/mxcli/testrunner/cleanup_strategy.go`", "insight": "Wrap the call in `ctx.startTransaction()` … `ctx.rollbackTransaction()` in a **finally** (a throwing test is the one most likely to leave half-written data), gated on a `rollback=1` query parameter the client sends per test. Report `rolledBack`/`rollbackError` in the response and warn per test — a rollback that fails silently is worse than none. Reject an unknown `@cleanup` value at **parse** time so `--list` catches it too. Verify against the database, not the endpoint's own claim: run one test with rollback and one with `@cleanup none` in the same suite and query Postgres — the `none` row must be the only survivor"} -{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "A suite passes under `mxcli test --attach` and fails under `--local`, with assertions that depend on startup state (a loaded cache, seeded reference data) seeing zero rows", "cause": "The `--local` runner pointed after-startup at its own registration microflow and did **not** chain the project's own, so the app's startup logic never ran. It was a deliberate choice (a known baseline) but was invisible: the run printed only `After-startup set to MxTest.RegisterEndpoint`, never that the user's microflow had been displaced", "file": "`cmd/mxcli/testrunner/runner.go` (`runEndpoint`), `cmd/mxcli/testrunner/cleanup_strategy.go` (`describeStartup`)", "insight": "Capture project state **before** generating the endpoint MDL, and pass `state.afterStartup` to `GenerateEndpointMDL` so the generated flow chains it — the hosted `--test-endpoint` path already did this, and the mismatch between the two was the bug. Add `--skip-app-startup` for a deterministic empty baseline, and always print which of the two happened. Note the startup microflow's writes run at boot, outside any test transaction, so `@cleanup rollback` does not undo them. mxcli-formula1 findings #19", "refs": ["#19"]} -{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "`mxcli test tests/ -p app/App.mpr --list` fails with `stat tests/: no such file or directory` while the same command without `--list` runs fine", "cause": "The `--list` branch passed raw `args` to `ListTests`, bypassing `resolveTestPaths` — so a path relative to the project (rather than the working directory) resolved for execution but not for listing", "file": "`cmd/mxcli/cmd_test_run.go` (the `if list` branch)", "insight": "Pass `resolveTestPaths(args, projectPath)` there too. When a command has two entry points into the same input, check both go through the same path resolution. mxcli-formula1 findings #15", "refs": ["#15"]} -{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "An app themed dark still shows light-mode drop shadows under the datagrid's filter-operator popover and dropdown filter lists", "cause": "The generated widget layer re-pointed `.column-selectors` but not the four rules in `_datagrid-filters.scss` that bake the same two-layer shadow. Each already takes its *background* from `--bg-color-secondary`, so Atlas re-colours the panel and leaves the shadow — which is why it reads as a partial fix rather than an untouched widget", "file": "`cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-widgets.scss`", "insight": "**Read the selectors out of the shipped `themesource/`, not the bug report** — the fourth here is `.dropdown-container .dropdown-list`, nested under a parent the report did not name. **Verify in the compiled CSS, never the source** (the §33 rule): apply the theme to a real project, run `mxbuild --target=deploy --java-home=… --java-exe-path=…`, then grep `theme-cache/web/theme.compiled.css` and check the *line number* — the fix must land after the widget module's own rule (30794 vs 27765 here) or the cascade eats it. A theme-cache file older than the SCSS you just wrote is a stale compile, and mtime is the cheapest way to catch it. mxcli-formula1 #33 / suggested issue 4", "refs": ["#33"]} -{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "Every page in a locally-run app is a black screen. `mxcli check` passes, the build reports success, the runtime log is quiet, `curl /` returns 200 with a valid HTML shell, and every OData service answers — only a browser sees it. It also comes and goes between runs of the same command at the same version", "cause": "`run --local` bundles the browser client at step 5b, then the runtime boot runs Gradle `clean-custom-classes compile package`, whose package pass repopulates `deployment/web` and takes `dist/` with it — deleting the bundle 51 seconds after the same command wrote it. It only bites when Gradle has work to do (a new Java action, a full recompile), which is why the app boots fine for weeks and then stops", "file": "`cmd/mxcli/docker/webclient.go` (`WebClientBundled`, `EnsureWebClientBundle`, `ReportLostWebClientBundle`), `cmd/mxcli/docker/runlocal.go` (step 6b, after the boot), `cmd/mxcli/docker/localapp.go` (headless boots warn instead of paying ~30s)", "insight": "**A pre-condition established before a step that rewrites the same directory is not a post-condition** — verify after, not before. The guard is a `stat` when the bundle survived, which is what makes it affordable at every boot; re-ordering the bundle to after the boot instead would leave the app reachable-but-blank for ~30s on every cold start. **`curl /` returning 200 is not evidence the app renders**: the shell is served by the runtime, the client by a file the shell references. The one-line check is `curl -o /dev/null -w '%{http_code}' /dist/index.js`. **Do not silently pay for a repair on a path that does not need it** — `test --local` destroys the bundle too, but tests are headless, so it prints the loss and the remedy rather than spending 30s on a two-second loop. Tests `webclient_bundle_test.go`; both controls run (guard never fires → the wipe test fails; guard always fires → the survivor test fails). mxcli-formula1 §35"} -{"area": "cmd/mxcli", "date": "2026-08-08", "raw": "| `.ai-context/skills/` still carries the previous release's guidance after the mxcli binary is upgraded — binary rebuilt at 12:05, skills stamped the day before — with no warning that they disagree | The skills are embedded in the binary and written exactly once, by `mxcli init`. Nothing re-ran init on upgrade, and nothing compared what was on disk against what the binary carried | New `cmd/mxcli/init_skills_sync.go` (`syncAIContextSkills`, `reportSkillSync`), `--sync-skills` flag in `cmd/mxcli/init.go`, and a sync step in the SessionStart bootstrap in `cmd/mxcli/init_hook.go` | **Stale guidance is worse than missing guidance**, because an agent reads it with identical confidence either way — so the failure mode is silent and confident. Fix it where it is consumed, not where it is authored: the SessionStart bootstrap already runs on every session and can fetch the binary, so it is the one place guaranteed to execute immediately before an agent reads the files. Two properties keep an every-session job acceptable: **write only what differs** (an mtime that moves every session makes \"when did this last change\" unanswerable — a test asserts the mtime holds) and **stay silent when current**. Never fatal: a skills refresh must not block a session, hence `|| true`. A test asserts the bootstrap script actually calls it *before* the exec'd setup, since a step ordered after an `exec` never runs. Tests `init_skills_sync_test.go`; the control (write-once) leaves the stale file in place. mxcli-formula1 §16 |"} -{"area": "cmd/mxcli", "date": "2026-08-09", "symptom": "No way to see what a running app's subsystem is doing: everything logs at `INFO`, the detail is not in the log at all, and raising the whole runtime to `TRACE` is unusable on a busy app", "cause": "Nothing in mxcli drove the runtime's per-node log levels, though the M2EE admin API has exposed them all along", "file": "New `cmd/mxcli/docker/loglevels.go` (`GetLogSettings`, `SetLogLevels`, `NormalizeLogLevel`, `MatchLogNodes`), `cmd/mxcli/cmd_log.go` (`mxcli log list` / `log set`), documented in `.claude/skills/mendix/analyze-runtime.md`", "insight": "**Probe an undocumented API before designing the command around it.** Every fact here came from a live 11.12.1 runtime: `get_log_settings` requires one of `node`/`subscriber`/`sort`; `sort` accepts only `node` and `subscriber`; `set_log_level` takes `{\"nodes\":[{name,level}],\"force\":bool}`. **The HTTP response for an AdminException says only \"See logging output for details\" — the actual message (\"Please specify node, subscriber or sort option in params\", \"Unknown sort option: name\", \"Unknown LogNode X. Use the 'force' parameter…\") is in the runtime log**, so probe with the runtime log open or you learn nothing. `force` means \"allow a node that does not exist yet\" and **permanently registers the name**, so a typo becomes a real empty node — hence it is opt-in, and an unknown node is an error by default. **Resist the subsystem-specific command**: this was proposed as `mxcli odata trace`, but `set_log_level` takes a list of nodes, so the primitive is generic and the OData knowledge belongs in docs. **The node list is a property of the APP, not of Mendix** — a node appears only once something registers it. A first pass concluded \"there is no log node for a published OData service\" from a project that had none; adding one service turns 57 nodes into 58 and `OData Publish` (with a space) appears, logging the full incoming URI at TRACE — exactly the question that motivated the command. Enumerating capabilities against one sample app and generalising is the trap; `log list` against *this* app is the answer. Distinguish \"cannot reach the admin API\" from \"the runtime refused the request\" (`ErrAdminUnreachable`) or the wrong hint buries the real one. Tests `cmd_log_test.go`, `loglevels_test.go`; verified end-to-end against a booted runtime (both arg forms, multi-node, typo refused, `--force` accepted, unreachable port). mxcli-formula1 suggested issue 4"} -{"area": "cmd/mxcli", "date": "2026-08-09", "symptom": "A headless browser cannot log in to an app started with `--hub`, so every screenshot silently shows the login page and rendering defects survive every other check", "cause": "Under `--hub` the runtime boots with the public **https** root URL, so it marks session cookies `Secure` and prefixes them `__Host-`; a browser on a non-trustworthy http origin cannot store them", "file": "`cmd/mxcli/docker/screenshot_login.go` — the login browser context declares `X-Forwarded-Proto: http` when the target is http (Mendix 10.24+ lets that header override `ApplicationRootUrl`)", "insight": "**The reported cause did not reproduce as stated, and saying so is part of the fix.** On 11.12.1 an https root URL does *not* block a headless browser on `127.0.0.1`: loopback is a **trustworthy origin**, so Chromium accepts `Secure`/`__Host-` cookies there — the app rendered clean, no console errors, no failed requests. The mechanism is real only for a **non-loopback** http origin (a container hostname, a LAN address). Fix shipped anyway because the header is *accurate* rather than a workaround (the request genuinely is http), it costs nothing on an already-http root URL, and real users over https are unaffected. Verified at the layer the bug lives in: the captured Playwright storage state goes from `__Host-XASSESSIONID(secure=true)` to `XASSESSIONID(secure=false)`. **`curl` cannot see this class of bug and neither can a loopback browser** — when a report blames cookie flags, check whether the origin is trustworthy before believing the flags are the blocker. mxcli-formula1 §38 / suggested issue 7"} -{"area": "cmd/mxcli", "date": "2026-08-09", "symptom": "`mxcli run --local` refuses to boot — *\"port 6643 is already in use — a previous 'mxcli run --local' … is likely still serving on it\"* — and names a process the user already killed. The guard is right to refuse; the diagnosis is a guess, and the recovery it suggests is a three-command `pgrep` hunt whose own pattern (`pgrep -f 'mxcli run'`) matches the shell it is typed into", "cause": "Two different situations printed one message. mxcli **does** reap its children on Ctrl-C/SIGTERM (`procgroup_unix.go` puts mxbuild's JVM, the runtime and the bundler each in their own process group and signals the group), so a held port after a graceful stop is a *foreign* listener. But `kill -9`, a crash, or a reaped container runs no handler at all, and then it really is the previous run's orphan. The guard could not tell them apart because it never asked who held the port", "file": "`cmd/mxcli/docker/portowner_linux.go` (`listenerOnPort`, `parseListeningInodes`, `pidForSocketInode`), `portowner_other.go` (non-Linux stub), `portguard.go` (`portCulpritAdvice`), wired into `checkTargetPortsFree` (`runlocal.go`) and `checkLocalAppPortsFree` (`localapp.go`)", "insight": "**Resolve the port owner from `/proc`, never by shelling out to `lsof`/`ss`** — both are routinely absent from slim containers, and needing a subprocess to explain why a boot failed is its own failure mode. `/proc/net/tcp{,6}` gives the socket inode; scanning `/proc//fd` for `socket:[]` gives the pid. **Read `tcp6` as well as `tcp`**: a JVM binding \"localhost\" lands on `[::1]` or dual-stack `[::]` as often as `127.0.0.1`, and its local_address is a 128-bit hex blob, not the 32-bit one. **Filter on state `0A` (TCP_LISTEN)** — an ESTABLISHED socket on that port is a *client*, and naming it would tell the user to kill their own browser. **Report the two cases differently**: a leftover of a previous run gets `kill `, a foreign listener gets \"pick another port\" and no kill suggestion, because offering to kill a stranger's process is worse advice than the vague message it replaced. Keep the guard **detection-only** — reaping someone else's process stays the user's call. **Three test traps hit in a row here.** (1) The live-IPv6 test only ever *skipped* (no IPv6 in the container), so the tcp6 parser was effectively untested — assert against captured kernel output instead. (2) A parser test calling `parseListeningInodes` directly does not prove `listeningInodes` *opens* both files: deleting `/proc/net/tcp6` from the list left every test green. (3) Making the list a `var` so a test can point it at fixtures then means the test no longer covers the **shipped** default either — assert the default's contents before overriding it. Only the third version of that control failed. The best test in the file is the cheapest: bind a port in the test process and demand `listenerOnPort` return `os.Getpid()` — it cannot pass by accident. Tests `cmd/mxcli/docker/portowner_linux_test.go`. mxcli-formula1 suggested issue 8"} -{"area": "cmd/mxcli", "date": "2026-08-11", "symptom": "After `mxcli marketplace install -p app.mpr`, the project's `mprcontents/` folder is gone, the `.mpr` has grown from tens of KB to tens of MB, and `mxcli diff-local` no longer works. Git shows hundreds of deleted `.mxunit` files and one enormous binary", "cause": "`mx module-import` rewrites an MPR **v2** project as **v1** — silently, and one-way. Measured on a blank 11.12.1 app: 69,632-byte `.mpr` + 341 `.mxunit` → 14,295,040-byte `.mpr` + 0, with the `_Transaction` table dropped. Reproduced independently on 11.13.0, so it is not version-specific. mxcli's install command shells straight out to it against the **user's** project", "file": "`cmd/mxcli/cmd_marketplace_install.go` (`checkStorageFormatPreserved`, `isMPRv2`, `reportFormatChange`, `--allow-format-change`)", "insight": "**Refuse, do not warn** — the conversion cannot be undone (`mx convert` targets Mendix versions, not storage formats) and it destroys the per-document layout `diff-local`, `.mxunit` merges and ADR-0008's observable idempotence all rest on. **Detect the format the way the readers do** (presence of `mprcontents/`), not with a second definition. **Check the guard's negative case**: a test that only asserts \"v2 is refused\" also passes a guard that refuses everything — assert a v1 project still imports. **Do not guard every `module-import` call site**: `marketplace diff` runs one against a throwaway reference project, where the format is irrelevant (and the resulting v1 reference still diffs exactly against a v2 project, because DESCRIBE output does not depend on storage). Related, same family: `mx module-import` also refuses any module whose `Projects$ModuleImpl → IsThemeModule` is `true` (exit 112), which is the sole gate — flipping that one boolean makes an otherwise identical package import cleanly. Tests `cmd/mxcli/cmd_marketplace_install_test.go`. Reported in mxcli-formula1 FINDINGS §53"} -{"area": "cmd/mxcli", "date": "2026-08-11", "symptom": "A .mpr extracted from a `.mpk` (or copied anywhere beside an existing project) reads with the wrong unit contents: edits appear to succeed, `ListRawUnitsByType` returns plausible documents, and a write lands nowhere the file will carry. Downstream the operation fails exactly as if nothing had been attempted", "cause": "The MPR format test is **adjacency, not content**: an `.mpr` is treated as v2 when an `mprcontents/` directory sits next to it (`sdk/mpr/reader.go` `OpenWithOptions`). A package's v1 `project.mpr` unpacked into a work directory that already holds a scratch v2 project is therefore read as v2, and every unit resolves against the *other* project's `.mxunit` files", "file": "`cmd/mxcli/marketplace/theme.go` (`makeImportable` unpacks into its own `os.MkdirTemp` subdirectory)", "insight": "**Unpack any foreign .mpr into a directory of its own** — never beside a project, even a throwaway one. **The failure is silent in both directions**: the read returns real-looking documents (from the wrong model) and the write reports success, so nothing surfaces until a later step refuses. The tell is a fix that provably works in isolation (`mx module-import` accepted the hand-built package) but does nothing in situ — that gap is environment, not logic. Test `TestMakeImportable_UnpacksAwayFromTheScratchProject` asserts the unpack does not land in the work-directory root"} -{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "A marketplace module update leaves the app running the OLD widget code: `show modules` reports the new version, pages reference new widget definitions, and the binaries in `widgets/` are unchanged. Symptoms are whatever the version gap causes at render time, with no build error naming the cause", "cause": "The update moved only the **model**. A widget module's `.mpk` ships its widget binaries under `widgets/` inside the package, and copying units out of a reference project never touches them. Measured on DataWidgets 3.5.0 → 3.11.3: model updated cleanly, all 10 widget binaries still 3.5.0 (`Datagrid.mpk` project=216193 vs package=166933)", "file": "`cmd/mxcli/marketplace/update.go` (`InstallPackageWidgets`, called from `PerformUpdate`)", "insight": "**Take the widget files from the `.mpk`, not from the reference project** — the reference is a blank app plus the module, so its `widgets/` also holds the template's widgets and copying those overwrites widgets the update has nothing to do with. **Skip zero-size directory entries** in the zip or you write a stray file (the 3.11.3 package has 10 `widgets/` entries and 9 real files). **A module with no widgets cannot expose this**: Administration updated end-to-end and looked completely correct, which is why the gap survived a full slice. Pick a second subject with a different shape before believing an update path works. Related but distinct: after any headless module install `mx update-widgets` is needed (CE0463); and a newer module can want a newer Atlas than the project has (29 × CE6083 on DataWidgets 3.11.3), which `rename-design-properties` does NOT fix — that is a dependency, not a resync", "ce": ["CE0463", "CE6083"]} -{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "After a marketplace module update, `mx check` reports `CE6083` \"Design property X is not supported by your theme\" on the updated module's own widgets, and it survives BOTH `mx update-widgets` and `mx rename-design-properties` (which renames 0)", "cause": "The update copied only the model and `widgets/`. The design properties are declared in the module's **own** `themesource//web/design-properties.json`, which was still the old version's copy — so the model referenced properties the shipped theme file did not define", "file": "`cmd/mxcli/marketplace/update.go` (`InstallPackageFiles` replaces `InstallPackageWidgets`)", "insight": "**Copy everything the package ships**, excluding only `project.mpr` and `package.xml` — enumerating the directories that seem to matter is what produced two bugs in a row (widgets, then themesource). **CE6083 surviving `rename-design-properties` is the tell**: that command renames properties between Atlas generations, so 0 renamed means the declaration is missing entirely rather than renamed. **Do not diagnose this as a cross-module Atlas dependency** — that was the wrong call here; the declaring module was the one being updated. Guard against `..` in package paths while copying. Measured: DataWidgets 3.5.0 → 3.11.3, 49 files replaced, 29 errors → 0", "ce": ["CE6083"]} -{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "A project that was MPR v2 is v1 after a marketplace install or update: `mprcontents/` is empty or gone, the `.mpr` jumped to tens of MB, `mxcli diff-local` stops working. The user followed the command's own \"Next:\" hint, or the docs", "cause": "The hint said to run bare `mx update-widgets ` to clear the CE0463 a headless install leaves behind. That command performs the resync **and** rewrites a v2 project as v1 — measured on 11.12.1: 369 `.mxunit` files → 0, a 69,632-byte index → 14,405,632 bytes. Same family as the `mx module-import` collapse, arriving through advice rather than through a call", "file": "`cmd/mxcli/cmd_marketplace_install.go` + `cmd_marketplace_update.go` (the \"Next:\" lines now name `mxcli docker check`), `docs-site/src/guides/marketplace.md`, `.claude/skills/mendix/download-marketplace-content.md`", "insight": "**`mxcli docker check` is the v2-safe resync** — it runs the same `mx update-widgets` under a storage-format snapshot (`cmd/mxcli/docker/update_widgets.go`, #808), so the check sees the resynced model and the project keeps its format. Verified: 369 `.mxunit` / 69,632 bytes before and after. **The resync is then not persisted** — the stored model still holds the old widget definitions and a later `mx check` reports CE0463 again; Studio Pro's \"Update all widgets\" persists it in v2, and `mxcli widget sync` is the partial headless equivalent (7 of 40 on the reference fixture). Say that rather than implying the resync sticks. **A guard in the code does not cover a string in the help text**: #808 already protected every call site mxcli owns, and the collapse came back as a sentence telling the user to bypass it", "refs": ["#808"], "ce": ["CE0463"]} -{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "`mxcli marketplace install -p app.mpr` fails with `mx module-import failed: exit status 117` — \"The package could not be imported, because it was created with a newer version of Mendix Studio Pro\" — after downloading the package and building a reference project, and the message is buried under the command's full flag list", "cause": "The marketplace publishes new releases against the newest Studio Pro patch within days, and `install` with no `--version` resolves to the latest, so on any project not on the very newest patch the **default** version is the one that cannot be installed. Measured 2026-08-12 on an 11.12.1 project: the latest release of all six agent-stack modules required 11.12.2, published five days earlier. Every version the API returns already carries `minSupportedMendixVersion` — nothing consulted it", "file": "`cmd/mxcli/marketplace_compat.go` (`checkMendixCompatibility`, `newestCompatibleVersion`, `mendixVersionOf`), called from `cmd_marketplace_install.go` and `cmd_marketplace_update.go`; `SilenceUsage`/`SilenceErrors` on install/update/diff", "insight": "**Check what the API already told you before spending a download and a reference build on it** — the refusal was always going to happen, three layers lower and in mxcli's voice rather than Mendix's. **Name the version to use, not just the problem**: `--version 4.1.0 (the newest release built for 11.12.1 or older)` is the whole fix. **Skip, do not refuse, when the check cannot evaluate** (no project version, no published minimum) — same rule as `checkFeature`. **`SilenceErrors` belongs with `SilenceUsage`**: `main()` already prints what `Execute` returns, so silencing only usage leaves every refusal printed twice. Tests `cmd/mxcli/marketplace_compat_test.go`, including the negative case — a check that refuses everything passes the refusal test"} -{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "After a headless module install, `mx check` reports project-level `CE6087` \"Design properties have been renamed in your theme and need to be updated\" with an empty location, and no mxcli command clears it — or the same for `CE0463`, which `mxcli docker check` makes pass without actually fixing", "cause": "The module ships its own design properties; Mendix's fix is `mx rename-design-properties`, which mxcli never ran. Measured on 11.12.1: it does real work (149 design properties across 41 documents) **and** collapses MPR v2 — 1,865 `.mxunit` files to 0, a 249,856-byte index to 39,895,040 bytes. Third member of the family, after `mx module-import` and `mx update-widgets`", "file": "`cmd/mxcli/docker/harvest.go` (`RunToolPreservingFormat`), `cmd/mxcli/cmd_fix.go` (`mxcli fix design-properties` / `fix widgets`)", "insight": "**The `update-widgets` snapshot trick does not transfer** — that one restores the pre-run storage because the resync only has to hold for the duration of the check, whereas these renames must **persist**, so restoring undoes the fix. **Harvest instead**: let the tool convert the project, read every unit back out of the converted file, restore the v2 storage, then write the changed units into it through the writer — which is also where `canon.Reconcile` elides the units the tool rewrote without changing (ADR-0008), so a second run writes 0. Safe for the same reason a module transplant is: no binary `$ID` crosses a unit boundary. **Restore on every failure path after the tool has run**, or a failed command leaves a half-converted v1 file. **Print the storage count before and after** — the collapse this prevents shows up there as a zero, and a success message without its own evidence is how it shipped the first time. **`mprcontents/` is not all units**: it also holds an `mprname` file, so a naive file count is one high and reads as a dropped unit. Measured: 203 errors (202 CE0463 + 1 CE6087) to 0 on a vanilla 11.12.1 app, MPR v2 intact at 1,868 units, reproduced from a restored pre-fix snapshot. Tests `cmd/mxcli/docker/harvest_test.go` — each verified to fail with the reported symptom when the fix is stubbed out", "ce": ["CE0463", "CE6087"]} -{"area": "cmd/mxcli", "date": "2026-08-11", "symptom": "`mxcli new` into a deep output directory dies with `System.IO.PathTooLongException` and leaves the directory holding ~259 files and no `.mpr` — output that looks like a project until you try to open it", "cause": "MxToolset refuses any full destination path over **259 characters** (its own Windows-compatibility limit, not the filesystem's) and aborts extraction PART WAY THROUGH. mxcli pointed `mx create-project` straight at the user's output directory, so the abort happened there", "file": "`cmd/mxcli/newproject_paths.go` (new: `stagedProjectDirs`, `longestRelativePath`, `warnIfPathTooLongForStudioPro`, `moveProject`), wired into `cmd/mxcli/cmd_new.go` step 2", "insight": "**Stage the work somewhere safe and move it in, rather than validating a path you could just avoid** — creating in a short temp dir and renaming makes ANY depth work (POSIX allows 4096) instead of merely failing politely, and the destination is never partially populated because nothing is written there until creation succeeded. Check the relocation is safe first: `grep -rl ` returned **0 files**, so a fresh Mendix project embeds no absolute paths. **Bisect for the real threshold instead of trusting arithmetic** — 77 characters creates a project on 11.13.0, 78 fails leaving 259 files, which pins `len(dest) + 1 + longest ≤ 259` exactly. **Measure the template, don't hardcode it**: the longest relative path is 181 on 11.13.0 and 182 on 11.12.0, so walk the staged tree and use the real number, or the reported budget drifts a character per release. **Warn, don't refuse, when the finished path is over budget** — the project works on POSIX, so refusing would block a machine where it is fine; but Studio Pro on Windows would not open it, so silence would be worse. Cross-device staging needs an `os.CopyFS` fallback: `os.Rename` cannot cross filesystems. Tests `cmd/mxcli/newproject_paths_test.go`. upstream #825", "refs": ["#825"]} -{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "Widgets silently go backwards: a project on DataWidgets 3.11.3 ends up running 3.4.0 widget code after updating an unrelated module (Atlas_Web_Content, FeedbackModule). `mx check` stays clean, `show modules` shows the new module version, and nothing mentions widgets", "cause": "A module `.mpk` bundles a copy of every widget its pages use, pinned at its author's release time — and different modules pin different versions of the **same** widget. `InstallPackageFiles` copied everything the package shipped, so the last module installed decided the widget versions. Measured on the published packages: Atlas_Web_Content 4.3.0 ships 5 Data Widgets at 3.4.0 against DataWidgets 3.11.3's 3.11.3", "file": "`cmd/mxcli/marketplace/widgetversion.go` (`widgetVersionInMpk`, `versionLess`, `unpackedTwinOf`), `update.go` (`InstallPackageFiles` returns `[]SkippedFile`), `cmd_marketplace_update.go` (`reportSkippedFiles`)", "insight": "**Read ``, not ``** — the root attribute is the manifest schema and is `1.0` on every widget ever published, so comparing it makes every version look equal, which is exactly as broken as not comparing. **An unparseable version must never count as \"older\"** — the default is to install, and a wrong \"older\" verdict silently withholds a file the package shipped. **Skipping must be reported**: a silent skip is the same class of bug as the silent overwrite it replaces. **Test the negative half** — a guard that skips every widget also passes \"the newer one survived\"; assert that a newer *and* an equal bundled widget still install. Same file handles FINDINGS §18: a package that ships a widget as both `.mpk` and unpacked tree (FeedbackModule 5.0.0, both 12.0.4) installs only the `.mpk`, but an unpacked widget with **no** packaged twin must still install. Verified against the real packages: 5 kept, 13 twin entries skipped. Reported in mxcli-chat FINDINGS §14/§18"} -{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "`mxcli marketplace update`/`diff` fail with `version \"6.0.0\" not found` on a module the project actually has, and `--force` does not bypass it", "cause": "Both download the **installed** version to build the local-edit baseline, and that version had been unpublished. A blank 11.13 app ships NanoflowCommons 6.0.0 while the 6.x line now starts at 6.1.1 — so the module most in need of updating is the one whose baseline cannot be built", "file": "`cmd/mxcli/cmd_marketplace_update.go` (`--no-baseline`, and the refusal now names it)", "insight": "**`--force` was never going to work**: it overrides a *finding*, and here the comparison never ran, so there is no finding. Needing a second flag is the signal that these are two different decisions — \"I accept losing edits I have been shown\" vs \"I accept not being shown\". **Say what the flag costs in the flag's own message** — with no baseline, local edits go without being named, so the honest instruction is to commit first. Reported in mxcli-chat FINDINGS §15"} -{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "A constant set with `alter settings constant 'M.C' value '…' in configuration 'Default'` has no effect on the running app. The statement succeeds, `describe settings` round-trips it, `mx check` is clean, and the app behaves as if the constant were still at its default", "cause": "mxbuild writes `deployment/model/config.json` with each constant's **default** value, and that map is what `run --local` hands the standalone runtime as `MicroflowConstants` — the configuration's overrides are not in it, and nothing read them. An app ran for hours with an empty encryption key while the model said otherwise", "file": "`cmd/mxcli/runconstants.go` (`resolveConstantOverrides`, `reportConstantOverrides`), `cmd_run.go` (`--configuration`), `cmd/mxcli/docker/localboot.go` (`mergeConstantOverrides`)", "insight": "**A silent no-op is the worst shape a bug can take** — every layer reported success. The fix therefore prints in *every* case, including \"no overrides applied\", so silence stops meaning \"your value is in effect\". **Merge, never replace**: `config.json` carries the defaults for constants the configuration is silent about, and replacing the map drops them (the app 530s on the first microflow that reads one) — which is exactly the shape `--runtime-setting MicroflowConstants={…}` has. **A private override has no value in the model at all** (a `Settings$PrivateValue` marker; the value lives on the workstation), so applying it would blank the constant — skip and name it. **Do not guess between configurations**: with several and no `Default`, applying one silently could push production's API key into a local run. Tests `cmd/mxcli/runconstants_test.go`, `cmd/mxcli/docker/localboot_constants_test.go`; verified live — the run now prints `Applying 1 constant value(s) from configuration \"Default\"`. Reported in mxcli-chat FINDINGS §33"} -{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "`mxcli marketplace diff` reports local edits on a project nobody has edited — typically an Atlas snippet and a building block. `--save-edits` then writes MDL that would *destroy* the element if replayed (an empty `create or modify snippet X (Folder: 'Web') { }`)", "cause": "The comparison is on DESCRIBE output, and `Describable()` only asked whether describe *errored*. Some types describe successfully into output that says nothing: an empty `{ }` body, or a building block under \"Building blocks are read-only; they cannot be created via MDL\". Two such renderings can still differ — one unresolved name is enough — and the difference was reported as a user edit", "file": "`cmd/mxcli/marketplace/snapshot.go` (`Element.Conclusive`, `declaredReadOnly`, `emptyBody`), `compare.go` (`classify`), `update.go` (`SaveEdits`)", "insight": "**Check equality BEFORE conclusiveness** — identical text is solid evidence of \"unchanged\" whatever the type, and inverting the order marks every Atlas building block unknown and drains `verified` of meaning. **Judge the output, not the type**: a type list goes stale the moment a describe handler improves, while \"this text contains nothing to compare\" stays true by construction — match the handler's own read-only wording so the two stay in step. **A rescue file must never be a deletion**: the same rule gates `--save-edits`, including `OnlyInstalled` findings, which reach it without passing through `classify`. **Test that a real edit is still Modified** — a guard that returns unknown for everything passes every \"not evidence\" test. Tests `cmd/mxcli/marketplace/inconclusive_test.go`. Reported in mxcli-chat FINDINGS §16"} -{"area": "cmd/mxcli", "date": "2026-08-13", "symptom": "A `.test.mdl` asserting on something a **constant** feeds passes under `mxcli test --attach` and fails under `mxcli test --local` (or the reverse), with nothing in either output to explain the difference", "cause": "`--local` boots an app of its own through `StartLocalApp`, whose options had no `ConstantOverrides` field — so it ran with each constant's **default** from `deployment/model/config.json`, while `--attach` runs against an app `run --local` booted, which applies the configuration's shared overrides. A constant resolving to the wrong value is not an error, so both runs reported success and only the assertion differed", "file": "`cmd/mxcli/docker/localapp.go` (`LocalAppOptions.ConstantOverrides`, `runtimeOptions`), `cmd/mxcli/testrunner/localapp_options.go` (new, shared by both `--local` runners), `runner.go` (`RunOptions.ConstantOverrides`), `cmd/mxcli/cmd_test_run.go` + `main.go` (`--configuration`)", "insight": "**A boot path that takes options needs one mapping function, not an inline struct literal per caller** — the two `--local` runners each built their own `LocalAppOptions`, so a field added for one would not reach the other, and neither had the constants. Both now go through `localAppOptions`, and the `LocalAppOptions`→`LocalRuntimeOptions` step is `runtimeOptions()` so the forwarding is assertable without booting anything: a dropped field there is otherwise invisible until an app runs with the wrong configuration. **Resolve in one place**: `cmd/` decides precedence and the runner only carries the map, or \"which configuration wins\" gets two answers. **Only report what this run actually uses** — `--attach` inherits the constants of the app it attached to, so resolving and printing them there would be a confident lie. Verified at the layer the symptom lives in (`.claude/skills/verify-in-runtime.md`): the same suite, one project, `--local` and `--attach` must agree, with the reverted-wiring control run showing the constant's default. Unit tests `cmd/mxcli/testrunner/localapp_options_test.go`, `cmd/mxcli/docker/localboot_constants_test.go`. See `docs/11-proposals/PROPOSAL_constant_values.md` slice 1"} -{"area": "cmd/mxcli", "date": "2026-08-13", "symptom": "A constant value that must not be committed has nowhere to live: a constant's default and a shared configuration override both go to git, and Mendix's own **private** configuration value — the correct slot — is encrypted per user account by Studio Pro from 10.9, so nothing headless can read or write it", "cause": "Not a defect so much as an absent layer. mxcli mirrors the concept with a store it can actually reach: `/.mxcli/constants.json`, mode 0600, gitignored, sitting between `--constant` and the configuration", "file": "`cmd/mxcli/constantstore/` (load/save), `cmd/mxcli/cmd_constant.go` (`constant set/unset/list`), `cmd/mxcli/constant_gitignore.go` (`ensureStoreIgnored`), `cmd/mxcli/constants_resolve.go` (`layerMachine`)", "insight": "**The promise has to be made true, then checked — asserting it is not enough.** `mxcli init` writes a `.gitignore` only when the project has none, and a Mendix project usually already has one, so the entry the whole layer rests on could simply be absent. `constant set` appends it *and then asks git*, refusing to write the value on \"not ignored\": a store that leaks is worse than no store. **Which rule defeats an ignore entry is not guessable** — `!.mxcli/**` does NOT re-include anything (git cannot re-include a file whose parent directory is excluded), while `!.mxcli` does; the test was corrected against measured git behaviour rather than the assumed case. **A corrupt store is an error, a stale entry is not**: an unparseable file means values the author set are about to be silently absent (fatal), while an entry naming a constant the project dropped is the user's own file and is skipped-and-named, or every run fails until it is hand-edited. **An empty store is deleted, not written as `{}`** — a file that configures nothing should not exist. Tests `cmd/mxcli/constantstore/store_test.go`, `cmd/mxcli/constant_gitignore_test.go`; verified live that the value reaches a running app and that `git status` never sees the file. See `docs/11-proposals/PROPOSAL_constant_values.md` slice 3"} -{"area": "cmd/mxcli", "date": "2026-08-13", "symptom": "A constant value changed on disk (machine store, or the project configuration) does not reach an app that is **already running** — a `mxcli run --local` keeps serving the old value until it is restarted", "cause": "Constants are a boot-time layer: mxcli sends them once, in the `update_configuration` call at start. Changing them live needs the M2EE admin API, and needs BOTH of its calls", "file": "`cmd/mxcli/docker/localboot.go` (`ApplyConstants`, `LocalRuntime.BootConfig`), `cmd/mxcli/devloop_handshake.go`, `cmd/mxcli/cmd_constant.go` (`--apply`)", "insight": "**`update_configuration` is STAGED, not applied** — measured on 11.12.1: the running app keeps its old values, and the call still answers `result:0`. Only the next `reload_model` applies them, so a version sending just the first call reports success and changes nothing. That is why the two calls live in one function rather than at the call site. **Re-send the WHOLE payload**: the admin action has no read-back (`get_configuration`, `get_current_configuration`, `runtime_config`, `get_current_runtime_status` are all \"Action not found\"), so anything not sent is simply gone from the configuration afterwards — which is why the dev loop publishes what it booted with rather than a second process guessing. **A handshake from a dead process is refused, not used**: its ports may since have been taken by something else, and pushing a configuration change into the wrong process is worse than reporting no dev loop. **Say plainly what cannot be confirmed** — with no read-back, two successful calls are the strongest evidence available, so the command names what would actually confirm it (a microflow that returns the constant) instead of claiming success. **Failure to apply is not failure to set**: the value is already on disk and the next boot uses it, so `--apply` reports and returns rather than exiting non-zero over a half-succeeded command. Tests `cmd/mxcli/docker/localboot_constants_test.go`, `cmd/mxcli/devloop_handshake_test.go`. See `docs/11-proposals/PROPOSAL_constant_values.md` slice 4"} -{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "`mxcli run --local --ensure-db` fails on a freshly built `mxcli init` dev container — there is no PostgreSQL service to start and no `postgres` superuser — even though `psql` is on PATH", "cause": "The generated Dockerfile installed `postgresql-client` **only**. `EnsureDatabase` starts a local service (`service postgresql start`) and provisions the role + database through `sudo -u postgres psql`, both of which need the **server** package. `psql` being present makes the container look correctly provisioned", "file": "`cmd/mxcli/tool_templates.go` (`generateDockerfile`)", "insight": "Install `postgresql` alongside `postgresql-client`. **Generalisable**: when a feature shells out to a service, assert the *server* package in the image template, not the client that happens to satisfy a `LookPath` check — the CI/web image having the server is what hid this (`/usr/lib/postgresql/16` is present there but not in the generated dev container). Guarded by `TestGenerateDockerfile_PostgresServer`, which asserts the server package for both the docker and podman variants"} -{"area": "cmd/mxcli", "date": "2026-08-15", "raw": "| `mxcli test` reports **PASS for an assertion that must fail** — `@expect 1 = 2`, `@expect length($result) = 999`, `@expect find($result, 'Z') >= 0` with the needle absent. Nothing in the output distinguishes a real assertion from a vacuous one, so a suite certifies work as verified while asserting only that the microflow did not throw. Mutation testing is what exposes it: mutants that return an obviously wrong value survive the suite | The `@expect` annotation was matched with a regex for one shape — `@expect $var (=|<>) ` — and `FindStringSubmatch` returning nil produced **no assertion at all** rather than an error. A test with zero assertions passes if its body completes. So the narrow support was not the defect; the silence was | `cmd/mxcli/testrunner/parser.go` (`expectPattern`, `parseAnnotations`), `cmd/mxcli/testrunner/expect.go` (new — `ParseExpect`, the validating parser), `cmd/mxcli/testrunner/generator_endpoint.go` + `generator.go` (emit the condition, not a rebuilt equality), `cmd/mxcli/testrunner/results.go` (`expectErrorResult`) | Capture the **whole** annotation body and hand it to a validating parser; anything it cannot compile becomes an `ExpectErrors` entry, the test is not generated at all, and the runner reports `StatusError` (which `FailCount` counts, so the exit code is non-zero). The parser is a strict recursive-descent pass over `exprcheck.Lex` — **not** `mdl/exprcheck`'s own parser, which recovers and emits hints, exactly the wrong behaviour here. Two measurements pinned the emitted expression against mxbuild 11.6.6: `<>` really is CE0117 (so the rewrite to `!=` is load-bearing, not cosmetic) and a wrong-typed comparison really is caught (`$result = 3` → CE0117), which is what makes the 0-error run on the 11 generated shapes mean something. **Generalisable**: when a pattern-matching parser can match *less* than its input, the non-match branch is a silent-failure path — audit every `if m := re.FindStringSubmatch(...); m != nil` whose else-branch does nothing. Repro `mdl-examples/bug-tests/expect-vacuous-assertions.mdl`. mxcli-sudoku FINDINGS #46 |", "refs": ["#46"], "ce": ["CE0117"]} -{"area": "cmd/mxcli", "date": "2026-08-16", "symptom": "A test suite's green is unreadable: a test that asserts **nothing** prints the same `PASS` as one with six assertions, and `@verify` — documented as an OQL post-condition — is parsed and evaluated by nothing at all. After @expect started failing closed, the cheapest way back to green is to delete the assertion, and the output cannot tell that apart from a repair", "cause": "Two silent-absence paths rather than the silent-drop path fixed in the row above. `TestResult` carried no assertion count, so nothing downstream could report one; and `TestCase.Verify` was populated by `parseAnnotations` and read by nothing but `--list` — `grep -n '\\.Verify' cmd/mxcli/testrunner/*.go` returns the parser and the lister, no runner", "file": "`cmd/mxcli/testrunner/results.go` (`TestResult.Assertions`/`SourceFile`, `newResult`, `vacuousResult`, `resultNote`, `VacuousCount`), `cmd/mxcli/testrunner/parser.go` (`AssertionCount`, `AssertionErrors`, the @verify rejection), `cmd/mxcli/testrunner/junit.go` (`junitClassName`, assertions property), `cmd/mxcli/main.go` + `cmd_test_run.go` (`--require-assertions`)", "insight": "Count assertions on the test case and carry them onto every result through **one** constructor (`newResult`) — the previous code built `TestResult` literals at five sites, which is exactly how a new field gets populated in one path and silently missing in another. Report the count on the ordinary result line, not behind `--verbose`: the whole lesson of #46 is that the *default* output must distinguish a test that asserted from one that did not. Vacuous tests warn by default and error under `--require-assertions`, because a smoke test is legitimate but an indistinguishable one is not. **Generalisable**: when auditing an annotation/config field for dead ends, grep for *readers*, not writers — a field with a parser and no consumer is a feature the docs promise and the code does not deliver, and it fails silently by construction. mxcli-sudoku FINDINGS #46 (follow-up)", "refs": ["#46"]} -{"area": "cmd/mxcli", "date": "2026-08-17", "symptom": "`@verify` cannot fail. No OQL — well-formed or not, true or not, against a real entity or an invented one — makes a test fail: `@verify select count(*) as n from Mod.Game = 999999` reports PASS on a table with one row, and so does `this is not a query`", "cause": "The annotation was parsed into `TestCase.Verify` and read by nothing but `--list`. Same silent-absence class as the dropped `@expect`, in the annotation covering the *harder* half of Mendix testing: most microflows are side effects, so asserting on rows written is the only way to test them", "file": "`cmd/mxcli/testrunner/verify.go` (new — `ParseVerify`, `scalarOf`, `compare`, `runVerifies`), `cmd/mxcli/testrunner/parser.go` (`checkVerifyCleanup`), `runner_endpoint.go` + `runner_attach.go` + `watch.go` (`adminOptions` on `testTarget`), `runner.go` (`rejectVerifyOnLegacyRunner`)", "insight": "Run the OQL over the admin API after the microflow returns, compare against a literal, and keep three outcomes apart: holds → unchanged, does not hold → FAIL with the observed value, cannot be evaluated → ERROR. Three things are enforced rather than left to trip you up, each learned by running it: **`@cleanup rollback` is refused** (the default undoes the writes before the query could see them, so it would assert against the pre-test state); **the result must be one row × one column** (picking a cell out of a table is a guess); and the **legacy after-startup runner refuses the whole suite**, since its tests run during boot with no seam to query at. Split the expected value off at the **last** comparison operator outside quotes and parens, then require the RHS to be a literal — without that check the split silently eats a `where x = 1` and sends a truncated query. **Generalisable**: unit tests with a stubbed endpoint proved the logic and would have shipped it broken — the first real run against a booted app found the OQL endpoint unreachable on that Mendix and the alias requirement, neither of which any stub could show. mxcli-sudoku FINDINGS #48", "refs": ["#48"]} -{"area": "cmd/mxcli", "date": "2026-08-17", "symptom": "Running `mxcli test` dirties the project: `git status` reports the `.mpr` modified after a run that changed nothing, so a \"run the tests, assert the tree is clean\" CI step fails and a pull request carries a meaningless diff. `mprcontents/` is byte-identical; only the `.mpr` differs, and a `.mpr` diff is opaque", "cause": "The runner injects an `MxTest` module and takes it back out. Cleanup restores the model, but every unit write stamps a fresh UUID into the `.mpr`'s `_Transaction.LastTransactionID` (`updateTransactionID`, in both engines' writers) **and** the insert/delete cycle relays SQLite's pages. Restoring the row alone is measurably insufficient: three consecutive runs then hold the id stable and still produce a different file hash each time", "file": "`cmd/mxcli/testrunner/snapshot.go` (new — `projectSnapshot`, `documentTreeDigest`), `cmd/mxcli/testrunner/runner.go` (`captureProjectState`, `restoreProjectFile` at all three cleanup sites), `cmd/mxcli/testrunner/host.go` (`HostedEndpoint.Remove`)", "insight": "Snapshot the whole `.mpr` before injecting and write it back after a successful cleanup — byte-exact by construction rather than by enumerating what might have changed. Two refusals are load-bearing: **cleanup failed** and **the `mprcontents/` tree moved**. In either case the project is not in the state the snapshot describes, and restoring would turn a visible harmless discrepancy into an invisible misleading one — a tree that reads as clean while the model is not. Write through a temp file + rename so an interrupted restore cannot truncate the `.mpr`. **Generalisable**: a tool that mutates a project to do its work owes a restore that is byte-exact, not semantically equivalent — every consumer downstream of it compares bytes. And when the obvious cause is a single row, measure the file hash before concluding it is the only one. Verified with the reporter's own harness: 3 runs, `.mpr` and `mprcontents` hashes unchanged throughout. mxcli-sudoku FINDINGS #47", "refs": ["#47"]} -{"area": "cmd/mxcli", "date": "2026-08-17", "symptom": "`run --local --watch` deploys a **half-applied model** when an `mxcli exec` is running, and there is no way to recover: re-running the script writes nothing (it is byte-idempotent), so nothing re-triggers the watcher and the stale build stands", "cause": "`watchAndApply` (`cmd/mxcli/docker/runlocal.go`) rebuilt on the **first** mtime bump. An exec rewrites the `.mpr` and many `mprcontents/*.mxunit` over seconds, so the build snapshots the tree mid-write. There was a settle for the web bundler and nothing for the model. Two behaviours each correct alone: idempotency removed a *recovery path* nobody had written down as one — \"just run it again\" was load-bearing", "file": "`cmd/mxcli/docker/runlocal.go` (`settleSource`, `sourceSettleWindow`, the `case <-ticker.C` branch)", "insight": "Wait for the source mtime to stop advancing (two poll intervals of quiet) before building. The wait is unbounded on purpose — a long exec is the case it exists for, and building late beats building mid-write — but it still honours the interrupt channel so Ctrl-C is not swallowed. `touch` on the `.mpr` remains the force-rebuild hatch, now documented. **Generalisable, twice over**: (a) a change *signal* is not a change *event* — anything polling mtime must debounce or it samples a writer mid-flight; (b) when adding an optimisation that skips work, ask what informal recovery procedure depended on that work happening. **Test trap hit while fixing it**: the first version asserted the file count *after* `wg.Wait()`, which holds against a `settleSource` that returns immediately — the assertion has to read the writer's state at the moment it returned. mxcli-sudoku FINDINGS #45", "refs": ["#45"]} -{"area": "cmd/mxcli", "date": "2026-08-18", "raw": "| `mxcli check script.mdl -p app.mpr` prints an unqualified `Check passed!` having resolved **nothing** against the project — a misspelled icon, entity or page name sails through the command that was handed the model. Reported as \"check does not validate icon names\", which it does | `cmd_check.go` gated the whole reference pass on `--references`, so supplying `-p` alone ran only the model-free rules. `validate_icon_refs.go` already existed and works (it names the offending icon and the `describe icon collection` command); it returns early when `!ctx.Connected()`, so it simply never ran | `cmd/mxcli/cmd_check.go` (`checkRefs = checkRefs || projectPath != \"\"`, the qualified pass message, help text), `cmd/mxcli/main.go` (flag help) | Make `-p` imply reference resolution — someone who hands the command a project has said what they want — and keep `--references` accepted so existing invocations survive. A run *without* a project now qualifies its verdict by naming what was not resolved. **Generalisable, and the third instance this month**: a clean report that cannot distinguish *checked and clean* from *not checked* is the same defect as a vacuous test assertion. When a feature needs two flags to do its job, the second one is usually a bug. Before adding a validator, check whether one exists and is merely unreachable. mxcli-dbreplication F5 |"} -{"area": "cmd/mxcli", "date": "2026-08-18", "symptom": "A skill pack mxcli ships teaches a guard that cannot fire: `references/patterns.md` shows a splice caller doing `IF $Q/Rejected THEN -- fail the request`, and a project wrote and then deleted a Java action for that branch", "cause": "The pack's **own** `QueryObject.java` throws `IllegalArgumentException` when `r.rejected && rejectUnsupported` **before** `Core.instantiate`, so a caller that receives a Query at all always has `Rejected = false`. The code is right and the documentation is wrong — and two further places repeated the claim (`SKILL.md`'s field table said `both`, `failure-modes.md` said \"Pass `true`: `Rejected` comes back set\")", "file": "`.claude/skills/packs/mendix-odata-pushdown/references/patterns.md`, `SKILL.md`, `references/failure-modes.md` (embed under `cmd/mxcli/skillpacks/` is regenerated by `make build`)", "insight": "Delete the dead branch from the pattern and say why it cannot run, quoting the throw; mark `Rejected` as a **bind**-caller field. **Generalisable**: when a doc and the code it documents disagree, grep the whole pack — the claim is rarely in one place, and the two secondary mentions were the ones that would have re-taught it. A documented safeguard that silently is not one is the same defect class as a vacuous assertion. mxcli-owid #30", "refs": ["#30"]} -{"area": "cmd/mxcli", "date": "2026-08-17", "symptom": "`mxcli oql` fails on every Mendix older than 11.11 with *\"Action not found ... upgrade mxcli\"*, and separately reports a **rejected query as `0 rows`** rather than as an error", "cause": "Two independent misreadings of the runtime's replies. (1) The 11.11+ REST route `/dev/preview_execute_oql` does not exist on older runtimes, and the admin API does not 404 it — it dispatches the POST as an ordinary admin request, finds no `action` field, and answers **HTTP 200** with `{\"result\":1,\"message\":\"Action not found\"}`. The fallback to the legacy M2EE action keyed on the 404 alone, so the legacy action — which works fine there — was never tried. (2) That legacy action reports a bad query as `{\"feedback\":{\"error\":\"...\"},\"result\":0}` — inside the feedback, with a **success** result code — so `M2EEError()` (which keys off the result) says nothing and the error body parses as an empty result", "file": "`cmd/mxcli/docker/oql.go` (`legacyOQL`, `oqlDevErrorKind`, the `error` field in `parseOQLFeedback`'s envelope)", "insight": "Fall back to the legacy action on *either* absence signal, and surface `feedback.error` regardless of the result code. Measured on 11.6.6: before, `mxcli oql` could not run any query; after, `select count(*) as n from Mod.E` returns a row, and a bad query is an error instead of `0 rows`. **Generalisable**: when a fallback is keyed on one specific failure signal, check what the *other* end actually sends — an HTTP-level 404 and an application-level \"not found\" are different wires, and a success code next to an error message is common enough to assume it happens. Found while wiring @verify (FINDINGS #48); related to #39", "refs": ["#39", "#48"]} -{"area": "cmd/mxcli", "date": "2026-08-14", "symptom": "Windows Defender flags the mxcli **Windows** release binary as `Trojan:Script/Sabsik.EN.A!ml`; enterprise EDR (Defender for Endpoint, CrowdStrike, SentinelOne) blocks it harder. Not the generic unsigned-Go-binary false positive of #185", "cause": "The binary genuinely embedded **chisel**, a dual-use tunnelling/pivoting tool (SSH over WebSocket), on every platform — although the tunnel only ever runs inside a Linux container. `run --hub` linked `chisel/client`, `tunnel-hub` linked `chisel/server`, so windows/darwin carried 32 packages incl. the whole `x/crypto/ssh` stack for a feature they cannot use", "file": "`cmd/mxcli/docker/tunnel_linux.go` + `tunnel_other.go` (client seam), `cmd/mxcli/tunnelhub/control_linux.go` + `control_other.go` (server seam), `scripts/check-tunnel-deps.sh` (guard)", "insight": "**Never obfuscate, pack or rename to dodge the scanner** — attacker tradecraft, and it makes the binary less trustworthy, not more. **Code signing does not fix this class**: a signed binary containing chisel is still flagged behaviourally; signing only addresses #185's generic false positive. The fix is to stop shipping the capability where it is unused: one interface per seam, `_linux.go` impl + `!linux` stub, commands still registered everywhere but failing with an actionable message. **Prove absence three ways, and know that `go tool nm` is not one of them** — release ldflags `-s -w` strip the symbol table, so nm reports \"no symbols\" whether or not the code is linked and would give a false pass; use `go list -deps`, `go version -m`, and `strings` (nm only on a deliberately unstripped build). **Guard against the transitive path, not the name**: match the module list (`x/crypto/ssh`, `gorilla/websocket`, `armon/go-socks5`, `jpillora/*`) so re-entry without the word \"chisel\" still trips it, and assert a **positive control** (chisel IS in the linux graph) so the check cannot pass vacuously. Verified by re-adding the import and watching the guard fail on all four windows/darwin targets. Result: -13.5 MB (-14.7%) on windows+darwin, linux unchanged. See ADR-0009", "refs": ["#185"]} -{"area": "cmd/mxcli", "date": "2026-08-19", "symptom": "`run --local` / `test --local` on macOS (or Windows) die with a raw `fork/exec ~/.mxcli/mxbuild//modeler/mxbuild: exec format error`, while `setup mxbuild` and `docker build` correctly use Studio Pro's binary. `--mxbuild-path` does not help", "cause": "`MxBuildCDNURL` branches on **GOARCH only, never GOOS** — both URLs are Linux tarballs (Mendix ships macOS mxbuild inside Studio Pro, not on the CDN), so an arm64 Mac caches a Linux *aarch64* ELF: the arch matches, so nothing notices until exec. `docker build` resolves via `resolveMxBuild` (PATH → Studio Pro → known locations → cache) and `setup mxbuild` via `NativeMxBuildForSetup`, but the local loop called `DownloadMxBuild` directly at `runlocal.go` and `StartServe` looked only at the cache. `LocalRunOptions.MxBuildPath` was documented as an override and never read for the serve binary, so there was no workaround either", "file": "`cmd/mxcli/docker/mxbuild_platform.go` (new: `ResolveMxBuildForLocal`, `binaryOS`, `verifyRunsHere`) + `runlocal.go` (the `DownloadMxBuild` call) + `mxserve.go` (`StartServe`)", "insight": "Reuse the rule the codebase already had rather than inventing one: `NativeMxBuildForSetup(goos, version)` returns Studio Pro's path, or \"\" meaning \"Linux, download is fine\", or an error plus guidance. Order: explicit path → (non-Linux) Studio Pro → cache/CDN. **Do not stop the download on Windows** — the cache holds a Linux binary there deliberately, for Docker builds; the fix belongs at resolve/exec time. Guard exec with a magic-byte check (ELF / Mach-O incl. fat / PE) and let an unrecognised format through, since a shell wrapper has no magic and refusing on a guess would block a working setup. **Inject `goos` into the helpers** (`resolveMxBuildForLocalOn`, `verifyRunsOn`): the whole bug is platform-specific and is otherwise untestable from a Linux runner. Repro without a Mac: plant a Mach-O magic at the cache path and call `StartServe` — reproduces the reporter's message verbatim. Tests `TestBinaryOS`, `TestVerifyRunsOn_LinuxBinaryOnMac`, `TestResolveMxBuildForLocal_*`; reverting makes `ExplicitPathWins` fail **by downloading from the CDN for 34s**, which is the ignored override made visible. **Unverified:** no macOS host was available, so the Studio Pro discovery path itself (`resolveStudioProDirMacOS`) is exercised only by its own existing tests. Issue #916", "refs": ["#916"]} -{"area": "cmd/mxcli", "date": "2026-08-19", "symptom": "Windows: `run --local` behaves as though Java were undetected, and a local run can fail looking for Gradle", "cause": "Two separate things. (a) **The `.exe` suffix**: `isJDK21` appended it, every CONSUMER did not — `--java-exe-path` handed to mxbuild and the path `exec.Command` runs to boot the runtime were both `\\bin\\java`, so a correctly-detected JDK was passed on in a form that need not resolve. Five sites built it by hand. (b) **Gradle is not mxcli's**: it ships inside the mxbuild bundle (`modeler/tools/gradle`, 8.5) and mxbuild invokes it — mxcli never calls gradle, so \"Gradle missing\" indicates a foreign/incomplete mxbuild bundle, usually #916", "file": "`cmd/mxcli/docker/javaexe.go` (new `JavaExePath`) used from `mxserve.go`, `build.go`, `settle.go`, `localboot.go`, `detect.go`; `jdkSearchPathsFor` in `detect.go`", "insight": "One helper, not five hand-built joins — the sixth call site would have repeated the bug. **Check the platform's own docs before adding a search path**: \"add Studio Pro's JDK\" turned out to be a non-task, because Mendix's install guide says Studio Pro installs **Eclipse Temurin 21** rather than bundling a JDK, so the existing Adoptium glob already IS Studio Pro's JDK; what was genuinely missing was the per-user `%LOCALAPPDATA%\\Programs` install location. Inventing a `Mendix\\\\jdk` path would have been dead code. Make the not-found error list what was searched — \"JDK 21 not found\" alone sends a user reading mxcli's source. Inject `goos` (`jdkSearchPathsFor`, `javaExeName`): the only Windows CI job is the tunnel seam, scoped with `-run`, so it compiles Windows code and executes almost none of it — which is exactly how a missing `.exe` survives. **Unverified**: no Windows host; the fix is code-level with OS-injected tests. Reported via a user relay, not an issue", "refs": ["#916"]} -{"area": "cmd/mxcli", "date": "2026-08-20", "symptom": "A CLI flag is accepted but does nothing: `mxcli test tests/ -p app.mpr --require-assertions` exits 0 on a suite where every test asserts nothing. `--help` lists the flag, it parses, and the summary line even prints \"1 test(s) asserted nothing\" — so the run looks like it noticed and chose to pass", "cause": "The flag was consulted in exactly one of the two result-assembly loops. `vacuousResult` had a single caller, in the endpoint runner's loop (`--local`, `--attach`); the after-startup runner (Docker default, and `--local --legacy-runner`) assembles results in `ParseLogResults(logReader, suite)`, whose signature had no parameter the flag could arrive through — it could not honour it even in principle", "file": "`cmd/mxcli/testrunner/results.go` (`ParseLogResults`, `preRunResult`), `cmd/mxcli/testrunner/runner_endpoint.go` (`runSuite`), `cmd/mxcli/testrunner/runner.go` (the `ParseLogResults` call in `runAfterStartup`)", "insight": "Give the pre-run verdicts — the ones decided by the *parsed test case* rather than by the run — **one** function (`preRunResult`) and route both loops through it, then pin that it is their only caller (`TestPreRunVerdictsHaveOneCallSite`). Same lesson as the `newResult` constructor two rows up: a decision reachable from one path and not the other is the defect, and a single call site is what prevents the next one. **Do not reach for the refusal precedent without checking it applies**: `rejectVerifyOnLegacyRunner` refuses `@verify` on this runner because it genuinely cannot evaluate it (its tests run during boot, there is no seam to query the app at). `AssertionCount()` is a static property of the parsed test case, known before any runner starts — refusing there would have denied a capability the runner has. **Generalisable**: when auditing a flag for dead ends, grep for its *consumers* and count them against the number of code paths that should honour it; a unit test on the helper (`TestRequireAssertionsMakesVacuousTestsErrors` called `vacuousResult` directly) proves the helper works and says nothing about whether any runner calls it. Issue #926", "refs": ["#926"]} -{"area": "cmd/mxcli", "date": "2026-08-21", "symptom": "`mxcli test`: (a) a `.test.mdl` file whose first test is preceded by a **file-level `/** … */` header** reports one test fewer — the first one vanishes with no pass, no fail and no error, and every later test renumbers over it; the workaround (a `--` header) re-triggers it the moment that header's **prose spells out `/**` and `*/`**. (b) `@expect count($Var) = N` on a bare `retrieve` block reported **PASS unconditionally** on v0.18.0, for any N, against an empty table", "cause": "Two independent \"substring instead of structure\" defects in `cmd/mxcli/testrunner`. (a) `extractDocAndBody` took the FIRST `/**`…`*/` in a `/`-separated chunk as the test's doc comment. A header is not separated from the test below it by a `/`, so both are one chunk: the header became the doc, carried no `@test`, and the chunk — real test included — was skipped. The delimiters were found by `strings.Index` with no notion of comment or string state, which is why writing them inside a `--` comment recreated the fusion. Test IDs were numbered by **chunk index**, so a skipped chunk left a gap (`test_2` as a file's only test) and the line number came from searching the whole file for the chunk's first 20 bytes (wrong chunk when two start alike; **panics** on a chunk shorter than that). (b) `count()` is not a Mendix expression *function* — counting a list is an Aggregate list **activity** — so `ParseExpect` could not compile it. Pre-0bf9382 that dropped the annotation, and a test with no assertions passes as long as its body does not throw", "file": "`cmd/mxcli/testrunner/parser.go` (`splitTestBlocks`, `extractDocAndBody`, `scanDocComments`, `skipMDLString`), `expect.go` (`expectAggregate`, `parseAggregate`, `collectAggregates`), `generator_endpoint.go` (`writeExpectAggregates`), `generator.go` (`renameExpect`)", "insight": "Scan the chunk (whitespace / `--` / `/* */` / `'…'`) instead of searching it: the doc is the **last** comment of the leading run, the body starts after it, and the line travels with the chunk from `splitTestBlocks`. Number tests, not chunks. **Two `@test` docs in one chunk is refused**, not resolved — either resolution runs one and drops the other, which is the bug. For (b), hoist the aggregate into the activity the author would write (`$mxtest_count_X = COUNT($X);`) ahead of the decision, and refuse `sum`/`average`/`minimum`/`maximum` (they need an attribute an assertion cannot supply) with the helper-microflow workaround named. In the monolithic runner the generated variable must be renamed per test like the body's own, or two tests counting same-named lists declare it twice. **Verify (b) at the Mendix layer, not the parser's**: `mxcli check` accepts plenty that mxbuild rejects — the generated flows were exec'd into a real 11.13.0 project and `mx check` run with a pristine-copy baseline (1 pre-existing error either side). **Controls**: pre-fix binary on the repro file finds 2 of 4 tests; stubbing `isListAggregate` to false restores \"count() is not a Mendix expression function\". Repro `mdl-examples/bug-tests/927-test-mdl-leading-comment-and-count.test.mdl`. Issue #927", "refs": ["#927"]} -{"area": "cmd/mxcli", "date": "2026-08-21", "symptom": "A `.test.mdl` test gains an assertion, a cleanup strategy or an expected error **nobody wrote**, because a sentence in its doc comment quotes the annotation — writing \"`@expect $x = 1` in a sentence\" gives the test that assertion, and \"@cleanup none would apply here\" changes its cleanup strategy. The invented assertion usually fails to compile, so the test reports an ERROR whose message quotes prose", "cause": "The annotation patterns in `parseAnnotations` were unanchored (`@expect\\s+(.+)`), so they matched anywhere in a line rather than at its start. An annotation is a javadoc **tag**, and a tag opens its line", "file": "`cmd/mxcli/testrunner/parser.go` (the six patterns in the `var (…)` block)", "insight": "Anchor every pattern with `^`. The leading `*` and its indentation are already stripped before they run, so the real spellings — at any javadoc indentation, and the one-line `/** @test x */` form — are unaffected; keep a control asserting that, or a fix that simply stops reading annotations passes. Found while writing the #927 repro file: its header explained the bug, and explaining it created an assertion that then errored against a test nobody had written. **Generalisable**: the sibling of #927's own root cause — a delimiter means nothing outside the structure it delimits, whether that structure is a comment or a line", "refs": ["#927"]} -{"area": "cmd/mxcli", "date": "2026-08-21", "symptom": "A test carrying both `@throws` and `@expect` reports making two assertions and makes one: the `@expect` is never evaluated, silently. It cannot fail, whatever it claims", "cause": "`@throws` selects a different generated shape in **both** generators (`writeThrowsFlowBody` / `writeThrowsTestBlock`, where the verdict starts as a failure and only the error handler clears it) and neither emits the `@expect` checks — but `AssertionCount` still counted them. Same silent-absence class as the dropped `@expect` and the unimplemented `@verify`, in the one combination where the assertion cannot be made to work at all: the body was expected not to produce a result to assert on", "file": "`cmd/mxcli/testrunner/parser.go` (`checkThrowsExpect`, called from `parseAnnotations` beside `checkVerifyCleanup`)", "insight": "Move the expects to `AssertionErrors` and clear them, so the test is an ERROR and gets no microflow — the annotation pipeline's fail-closed rule. Keep the `@throws` itself: it is the assertion that still stands. **Prove it end to end, not on the struct**: the visible symptom is `AssertionCount` and the generated MDL, so the test parses a file, checks the count, and greps the generated flow for the microflow's absence. Sibling of the `@verify`-under-rollback refusal, which is the precedent for the message shape (name the annotation, name the one-line change)"} -{"area": "cmd/mxcli", "date": "2026-08-22", "symptom": "A bundled skill renders with a stray `---` fence and a duplicated `name:` at the top of its body, and `mxcli init` ships it that way. `TestEmbeddedSkillsCarryAgentSkillsFrontmatter` passes — the frontmatter it checks is valid", "cause": "The #906 migration **prepended** a `name`/`description` block to every skill, including the one file that already had one (`custom-widgets`, under its pre-rename name `mendix-custom-widgets`). Everything after the FIRST `---` block is body, so the old block became visible text. The test's `frontmatter.FindSubmatch` matches the first block and validates it correctly, which is precisely why it is blind to a second one", "file": "`.claude/skills/mendix/custom-widgets/SKILL.md` (two blocks merged into one, keeping the old block's trigger words — child slots, `.def.json`, engine internals); guard added to `cmd/mxcli/init_skills_standard_test.go`", "insight": "**A first-block check cannot see a rename-and-prepend**, and that is the exact shape of a migration that renames things — so a bulk frontmatter edit needs a guard against a SECOND block, not a better check of the first. **Measure the pre-state before a bulk prepend**: 1 of 68 files already had frontmatter, which was known and written down, and the migration script still did not branch on it. **Do not attribute a defect to the change you are looking at** — this was reported against PR #222, whose diff touches only the `description:` inside the first block and shows the stray `---` as unchanged context; `git log -S 'name: mendix-custom-widgets'` settles it in one command. Two adjacent traps from the same report: `make test` did not depend on `sync-all` while `make build` did, so a bare `go build` left the generated `cmd/mxcli/skills` stale and six tests failed — and the failure message named the `go:embed` directive, which was fine, instead of the missing build step. A **differential across refs is only as sound as the procedure both sides share**: the same stale build made main fail 4 and the branch fail 6, which looked like a regression and was a test-count difference. mxcli-formula1 finding 68", "refs": ["#222", "#906"]} -{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "`mxcli run --ensure-db` fails to start PostgreSQL when no service manager becomes ready (e.g. Arch): `exec: \"pg_ctlcluster\": executable file not found in $PATH`, though `initdb`/`pg_ctl`/`psql` are present", "cause": "`startLocalPostgres` only knew the `service`/`pg_ctlcluster` helpers (the latter a placeholder that could never run), and role/database provisioning assumed `sudo -u postgres`, which a user-owned cluster does not need", "file": "`cmd/mxcli/docker/ensuredb.go` (`startLocalPostgres`, `startUserCluster`, `resolveSuperuser`)", "insight": "Fall back to a user-owned `initdb`/`pg_ctl` cluster under `~/.mxcli/postgres`, but never initialize a competitor when the requested TCP port is already owned by a service-started server that is still recovering. Enforce the passwordless-superuser boundary instead of assuming it: explicitly tighten an existing socket directory to `0700`, set the socket itself to `0700`, and persist `listen_addresses`/`port`/`unix_socket_directories` in `postgresql.conf` so a plain later `pg_ctl start` stays safe. Refuse, with a cleanup path, a cluster made by the earlier development revision if its host authentication is still `trust`. Reuse a running cluster only when both the port and socket directory in `postmaster.pid` match. Keep `sudo -u postgres` for system clusters, but make both sudo and psql non-interactive, retain peer authentication over the system Unix socket at the exact requested port, and force SCRAM password storage for pre-14 defaults. Normalize the endpoint once and return it to the runtime; parse the bracketed form before any last-colon IPv6 compatibility fallback. Tests stub every external tool and exercise slow service startup, legacy-cluster refusal, permissions, persisted settings, endpoint propagation, sudo arguments, and repeated/running cases. Issue #823", "refs": ["#823"]} -{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "`mxcli test` **panics** — `slice bounds out of range [:-1]` in `extractDocAndBody` — on a `.test.mdl` saved with **CRLF** line endings; the identical file with LF works. Separately, **one unparseable file aborts the whole directory**: `mxcli test tests/` (and `--list`) reports the bad file and runs none of the others", "cause": "Two independent defects behind one report. (1) The line-number estimate searched the **raw file bytes** for the first 20 characters of a block that had already been rebuilt by joining scanner lines with `\"\\n\"` — under CRLF the needle can never match, `strings.Index` returns -1, and `content[:-1]` panics. Fixed as a side effect of #927's block/line pairing (`testBlock` carries its own `Line`), which left it **untested**. (2) `ParseTestDir`/`parseTestFiles` returned on the first file that failed to parse, so an unrelated malformed file cost every other test in the directory", "file": "`cmd/mxcli/testrunner/parser.go` (`TestSuite.FileErrors`, `FileError`, `ParseTestDir`), `cmd/mxcli/testrunner/runner.go` (`parseTestFiles`, `Run`, `dispatchRun`, `reportFileErrors`, `suiteFileErrorResults`), `cmd/mxcli/testrunner/watch.go`; tests `cmd/mxcli/testrunner/parser_crlf_test.go`, `cmd/mxcli/testrunner/parser_file_isolation_test.go`", "insight": "**Never search reconstructed text inside the original bytes** — normalising line endings on the way in and then indexing the raw content is the whole bug, and it is silent on LF machines. **A CRLF test must write the bytes at runtime** (`os.WriteFile`), not live in a checked-in fixture: git's line-ending handling would normalise it and the test would pass vacuously. **Assert on line NUMBERS, not just \"does not panic\"** — the defect lived in a line estimate, so a smoke test would not have detected a wrong answer; the LF file is the control. For (2), the fail-closed rule this package is built around is **ERROR, not abort**: an uncompilable `@expect` becomes a `StatusError` result counted with the failures, and an unparseable file now gets the same treatment — red exit code, file named, other tests still run", "refs": ["#927"]} -{"area": "cmd/mxcli", "date": "2026-08-25", "raw": "| `mxcli lint --format json \\| jq .` fails with `Invalid numeric literal at line 1`; `--format sarif > results.sarif` writes a corrupt file. stdout begins `Connected to: …` / `Loading cached catalog…` / `✓ Catalog ready` before the payload | The executor is constructed with `executor.New(os.Stdout)` via `newLoggedExecutor`, so every progress line shares the stream the payload is written to. Independent of which formatter runs — the chatter is emitted during connect and catalog build, before the formatter is reached | `cmd/mxcli/output_stream.go` (`isMachineReadableFormat`, `progressSink`), `cmd/mxcli/main.go` (`newLoggedExecutorTo`), `cmd/mxcli/cmd_lint.go`, `cmd/mxcli/cmd_report.go` | Pick the executor's writer from the resolved format: stderr when stdout carries a machine-readable payload (json/sarif/html/markdown), stdout otherwise. Route to **stderr, not io.Discard** — the progress is wanted on a terminal and in CI logs, it was merely on the wrong stream. `mxcli report` needs the extra condition that `-o ` frees stdout, so progress goes back to stdout there. Text mode must be left alone; assert that, or the fix silently degrades interactive use. Two commands share this because both build a catalog before formatting; any future command that prints a payload to stdout needs `progressSink` too. Issue #904 (residue) |", "refs": ["#904"]} -{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "A brand palette hand-edited into `theme/web/custom-variables.scss` works once, and then `mxcli theme apply` reports the file as `skipped` and refuses to touch it — the project has silently taken the theme out of mxcli's hands to change one colour", "cause": "The theme registry was embed-only: `List`/`Get` read `assetsFS` and nothing else, so there was nowhere to put a fourth theme and the generated block was the only place to put a palette", "file": "`cmd/mxcli/theme/source.go` (the two-source registry), `cmd/mxcli/theme/create.go` (`theme create`), `cmd/mxcli/theme/tokens.go`", "insight": "**A digest fence that refuses an edit is a signal that the thing being edited should have been authorable.** Project-local themes live in `theme/mxcli-themes/` — committed (`.mxcli/` is gitignored by `mxcli init`) and not compiled (mxbuild's entry point is `theme/web/main.scss`; it does not glob `theme/`, verified against a real 11.13 build that nothing under it reaches `deployment/`). Scaffolding **must** rename the identifiers built from the theme name (`@mixin mxcli--`, the `@import`) — a plain copy collides the moment both themes exist. Verified end to end: seeded palette → `mxbuild --target=deploy` → measured `--brand-primary` in a real browser in both variants. Tests `cmd/mxcli/theme/create_test.go`, `cmd/mxcli/cmd_theme_test.go`"} -{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "A design-derived `--mxt-*` token is written into a theme, the theme applies with every file reporting `created`, and the app renders exactly as before — indistinguishable from the design never having been applied", "cause": "Nothing reads an unrecognised custom property, so a typo (`--mxt-brand-color` for `--mxt-brand`) is a no-op with no error anywhere in the chain", "file": "`cmd/mxcli/theme/create.go` (`validateTokens`, `knownTokens`)", "insight": "**Validate a token vocabulary at authoring time or the failure has no symptom.** `theme create --from` refuses a `--mxt-*` name the base theme does not declare and names it; `mxcli theme show ` prints the vocabulary. Related trap: seed only the *scoped* extras into the alt-palette mixin — a variant-independent token like `--mxt-radius` belongs in the palette, and copying it into both means retuning it twice"} -{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "A theme's SCSS fails to compile with `Error: Expected expression` pointing at a `$…-scope:` line, and every Go test is green — `mx check` says nothing either, because the failure is in mxbuild's Sass pass, before any model validation", "cause": "A selector was assigned to a Sass variable bare (`$s: :root` or `$s: a, b`). Neither is a Sass expression; only a **quoted string** is, and `#{}` drops the quotes at the point of use", "file": "`cmd/mxcli/theme/theme.go` (`expand`, the `{{SCOPE}}` substitution quotes; `{{PALETTE_SCOPE}}` does not, because that file is plain CSS)", "insight": "**An asset-driven feature whose correctness depends on a compiler the tests never run needs one real compile in the loop.** This shape passed every unit test and broke the build on the first `mxbuild --target=deploy` — the same class as recurring-finding #20. Two placeholders that look interchangeable are not: one lands in plain CSS, one in a Sass expression", "refs": ["#20"]} -{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "Applying several themes as a set is never idempotent — `mxcli-fonts/OFL.txt` reports `updated` on every run, forever — and the installed licence covers the wrong fonts", "cause": "Every theme vendored a different font family and wrote its SIL OFL text to the *same* path. With one theme installed the last apply won and it settled; with a set, each theme overwrote the previous one on every pass", "file": "`cmd/mxcli/theme/assets/*/files/theme/web/mxcli-fonts/OFL-.txt` (renamed per theme)", "insight": "**Two themes writing different content to one verbatim path is a licensing bug before it is a churn bug** — the churn is just the symptom that made it visible. `TestThemesDoNotClaimTheSameVerbatimPathWithDifferentContent` hashes every non-fenced file across every theme and fails on a collision; controlled by renaming them back, which reproduces the original failure"} -{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "A JavaScript action with a parameter throws ` is not defined` the first time a user clicks it. `mx check` reports 0 errors, `mxcli check` says nothing, the build is clean, and the generated file cannot be patched — it is rewritten on every build", "cause": "mxbuild **lowers the first letter** of a parameter when it generates the wrapper: `Skin: String` reaches the body as `skin`. A body written with the modelled spelling is a ReferenceError, and nothing between MDL and the click can see it — Mendix treats the body as opaque user code", "file": "`cmd/mxcli/theme/switcher.go` (both parameterised actions); the same rule applies to any `create javascript action` with parameters", "insight": "**Model the parameter capitalised, read it lowercased.** Verified against a real 11.13 build (`export async function SetAppSkin(skin)`) and in a browser by importing the generated module and calling it — `ReferenceError: Skin is not defined` before, the correct return after. The guard walks *every* parameter of *every* generated action, so a new one cannot reintroduce it. Note the trap in writing that guard: a regex anchored on `javascript action` also matches `call javascript action` in the nanoflows, which made the first version run past a real declaration and pass against the very defect it was written for — anchor on `create or modify javascript action` and assert the match count. ako/mxcli-ledger #135", "refs": ["#135"]} -{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "`mxcli theme create` lists `theme/web/custom-variables.scss` and `theme/web/main.scss` as `created`, on a project that has hand-written versions of both. Nothing was touched", "cause": "The reported paths were relative to the scaffold, not to the project, so they collided exactly with the app paths — printed by the one command whose whole purpose is to write into `theme/`", "file": "`cmd/mxcli/theme/create.go` (`CreateResult.Files` carries scaffold-root-relative paths), `cmd/mxcli/cmd_theme.go` (prints them under the scaffold root)", "insight": "**A path in output is a claim about a location.** Two files in different trees sharing a relative path means the output has to carry the root or it asserts the wrong one. `theme.json` sits at the scaffold root and everything else under `files/`, so the result carries that distinction rather than the printer re-deriving it. ako/mxcli-ledger #139", "refs": ["#139"]} -{"area": "cmd/mxcli", "date": "2026-08-26", "symptom": "A test written with a bare `@throws` fails on the very exception it was written to expect, reporting \"exception during execution\" and \"no assertions\"", "cause": "`mxcli test` treats a test as a throws-test only when the annotation carries a NON-EMPTY message (`generator_endpoint.go`, `if tc.Throws != \"\"`), so a bare `@throws` is silently ignored and the exception propagates as a failure. The message itself is **never compared** — `@throws 'ZZZ-NOT-THE-REAL-MESSAGE'` passes against any error — so it documents intent rather than asserting it", "file": "`cmd/mxcli/testrunner/generator_endpoint.go` (`writeThrowsFlowBody`, `rewriteBodyForThrows`)", "insight": "Write `@throws ''` and give the test its meaning with a CONTROL — the same call with input that must NOT throw — because the annotation alone only proves something was raised, not what. Two separate defects to fix: a bare `@throws` should be a parse error rather than silently dropped, and the message should either be matched or documented as unmatched (the annotations reference shows `@throws 'validation failed'`, which reads as a match). Found while asserting #261's `or error` at run time", "refs": ["#261"]} -{"area": "cmd/mxcli", "date": "2026-08-27", "symptom": "`mxcli run --local` on a Mendix 11.14 app dies at `bundling web client: no rollup.config.mjs in …/deployment/web (run a serve Deploy build first)` — and a serve Deploy build HAS just run", "cause": "11.14 bundles the browser client itself. `BuildWebClient` exists to close a gap that 11.13 and earlier left (client source + a rollup config, but no bundler run), and its gate tested for the CONFIG, so it failed on the absence of a file whose purpose had been served. Measured on a blank app, same build target: 11.13 `rollup.config.mjs` PRESENT / `dist/index.js` ABSENT; 11.14 exactly inverted", "file": "`cmd/mxcli/docker/webclient.go` (`BuildWebClient`), called fatally at `runlocal.go:664` and `:1091`", "insight": "**Gate a compatibility step on the GAP it closes, not on the shape one version happened to leave behind.** Skip when `WebClientBundled(deployDir)` is already true; keep requiring the config when it is not, because then rollup is the only thing that can produce a bundle. Do NOT make it an unconditional no-op when `dist/` exists — `runlocal.go:1091` re-bundles when the file is present but not SERVED, and a blanket skip would break that recovery. The doc comment called it \"no-op-safe\" and it was not; it is now. Affects EVERY 11.14 app, so a version-gated workaround is wrong — the condition is about the bundle, not the version. ako/mxcli-ledger #146", "refs": ["#146"]} -{"area": "cmd/mxcli", "date": "2026-08-27", "symptom": "A project made by `mxcli new --version 11.14` fails its very first build with `error: release version 25 not supported`, and installing a JDK 25 moves the failure to boot (`UnsupportedClassVersionError … class file version 69.0`)", "cause": "Mendix 11.14's `mx create-project` writes `JavaVersion = 25`, but mxcli builds and launches the runtime on **JDK 21** and validates it (`resolveJDK21` refuses any other `JAVA_HOME`). Compiling for 25 and running on 21 cannot work in either direction — and 11.14's own runtime bundles are still Java 21 class files (major version 65)", "file": "`cmd/mxcli/cmd_new_java.go` (`alignJavaVersion`, `runtimeMaxJavaVersion`), called from `cmd_new.go` **before** the first build; the launcher side is `cmd/mxcli/docker/detect.go` (`resolveJDK21`)", "insight": "**Lower the project, do not raise the JDK.** Set `JavaVersion` to what the runtime launcher actually uses, and only when the project asks for MORE — so it is a no-op on 11.13 and disappears by itself when the launcher becomes version-aware. Order matters: align BEFORE `SettleGeneratedSources`, or that build is the thing that fails. Mendix then warns \"Java versions below 25 are deprecated\", which is a deprecation on a project that builds rather than an error on one that does not. An unparseable version is left alone rather than overwritten. `runtimeMaxJavaVersion` and `resolveJDK21` must move together — there is a test asserting it. ako/mxcli-maintenance #1, #3", "refs": ["#1", "#3"]} -{"area": "cmd/mxcli", "date": "2026-08-27", "raw": "| A Mendix 11.14 project will not build (`error: release version 25 not supported`) or, if built elsewhere, will not boot (`UnsupportedClassVersionError … class file version 69.0`) | mxcli resolved a JDK **21 unconditionally** and refused any other `JAVA_HOME`. Studio Pro 11.14 supports Java 25 and its blank app asks for it, so the Java release is now a per-project variable (Settings > Model > JavaVersion) rather than a constant | `cmd/mxcli/docker/javaversion.go` (`ProjectJavaMajor`, `ParseJavaMajor`), `cmd/mxcli/docker/detect.go` (`resolveJDK`, `isJDK`, `jdkSearchPathsFor`); the four call sites are `build.go`, `settle.go`, `mxserve.go`, `localboot.go` | **Resolve the JDK from the project, not a constant** — and use the SAME major for the build and the runtime, because class files from a newer release cannot be loaded by an older JVM. Two traps. (1) The version glob is matched against `java -version` text, so the major must be followed by a dot, whitespace or the closing quote — a bare `21` also matches `210`; there is a test. (2) Zero must mean the default, or every options struct that forgets the field silently changes behaviour. The control worth running: boot a Java **21** project with `JAVA_HOME` pointing at a JDK **25** — it must ignore it and find the 21, and `ps` per-process is the only reliable way to check, since a stale JVM from an earlier run will fool `ps | head -1`. ako/mxcli-maintenance #1 |", "refs": ["#1"]} -{"area": "cmd/mxcli", "date": "2026-08-27", "symptom": "CI's `build-and-test` fails on every open PR with a **compile** error in a `_test.go` file — e.g. `undefined: resolveJDK21` — while `make build`, `make test` and `make lint` are all green locally, and the failing job burns ~30 minutes before reporting it", "cause": "The file is behind `//go:build integration`. A bare `go vet ./...` and `go test ./...` do not compile it, so a rename can leave it uncompilable and the entire fast gate stays green. The break lands on **main** and every open PR inherits it, which makes it look like N separate PR failures", "file": "The stale call site (here `cmd/mxcli/docker/build_integration_test.go`), and the gate that missed it: `Makefile` (`vet`)", "insight": "**Fix it on a branch off `main`, not in the PRs** — they all inherit it, and fixing it in one leaves the others red. Then close the gap: `make vet` now vets once per build-tag set (`\"\"` and `integration`), which type-checks the tagged files in seconds without needing mx, a JDK or Docker. Grep for stale references with `go vet -tags integration ./...` before renaming anything an integration test touches. Where the renamed symbol became version-aware, do NOT just pass the old constant back in — `resolveJDK(21)` would reinstate exactly the assumption the rename removed; resolve from the project (`ProjectJavaMajor`), which for a skip guard means moving it after the fixture is scaffolded."} -{"area": "cmd/mxcli", "date": "2026-08-27", "raw": "| A `mxcli test` case written with a bare `@throws` reports **FAIL … exception during execution** — it fails on the very error it was written to expect — and separately, `@throws 'ZZZ-NOT-THE-REAL-MESSAGE'` reports **PASS** | Both generators keyed a throws test on `tc.Throws != \"\"`, and a bare annotation leaves `Throws` empty — the same value an ABSENT annotation leaves, so the two states were indistinguishable and the bare form was dropped in silence. The message half was decorative: the generated `ON ERROR` handler cleared the pre-set failing verdict for **any** exception, so a stale or simply wrong expectation kept passing against a completely different error. The regexp required the quotes, so an unquoted message matched nothing and removed the `@throws` entirely | `cmd/mxcli/testrunner/parser.go` (`parseThrows`, `TestCase.ExpectsThrow`, `expectsThrow()`), `generator_endpoint.go` (`throwsHandler`), `generator.go` (`throwsFlagHandler`) | **`$latestError/Message` is available inside an `ON ERROR` handler and compiles** — measured at 0 errors on mxbuild 11.6.0 and 11.13.0, with `$noSuchThing/Message` as the control (**CE0109** \"Undefined variable\"). Match with `contains`, not equality: a real Mendix message carries an activity name or an object id the test cannot predict (`Could not find object of type 'RtMap.Person'`), so equality would make most expectations unwritable. Two fields can say \"this is a throws test\" and they must not disagree — read them through **one accessor** (`ExpectsThrow || Throws != \"\"`), or every existing construction site that sets only the message silently becomes a non-throws test. Prove it at RUNTIME, not on the generated MDL: the control (`if want == \"\"` → `if true`, rebuilt) reproduces both reported symptoms verbatim against `mdl-examples/bug-tests/mapping-runtime-proof.test.mdl` under `run --local --test-endpoint` + `test --attach`. That suite's own `@throws 'the object was not found'` was **wrong and passing** — a fixed matcher turns a wrong expectation red, so expect to correct existing tests. ako/mxcli#301 |", "refs": ["ako/mxcli#301"], "ce": ["CE0109"]} -{"area": "cmd/mxcli", "date": "2026-08-27", "symptom": "`mxcli test ` prints `Found 0 test(s) in 1 file(s)` then `no tests found in the provided files`, for a directory that visibly contains a `.mdl` test", "cause": "Discovery matches on the file NAME (`*.test.mdl` / `*.test.md`), and nothing said so. The `1 file(s)` was the DIRECTORY counted as a path, so it read as though a file had been opened and found wanting", "file": "`cmd/mxcli/testrunner/runner.go` (`emptySuiteError`, `parseTestFiles`), `cmd/mxcli/testrunner/parser.go` (`TestSuite.FilesRead`, `ParseTestDir`)", "insight": "**Count files read, not paths given** — one directory is one path and zero-to-many files. When the suite is empty, name the `.mdl`/`.md` files that were skipped for their name: a `.mdl` in a test directory is almost always the intended test misnamed, so listing it turns a dead end into a rename. Do NOT list unrelated extensions (`.txt`), and on a genuinely empty directory do NOT claim files were skipped — describe the format instead. Both branches have tests, and `FilesRead` has a positive control so it cannot pass by never being incremented. ako/mxcli-maintenance §5"} -{"area": "cmd/mxcli", "date": "2026-08-28", "symptom": "Under `run --local --watch`, after roughly ten hot-applies a page dies with the runtime's generic error dialog and `runtime.log` fills with `404 - file not found for file: dist%2Fchunks%2F.js`. The model is clean, `mx check` is clean, and a plain restart fixes it with no model change", "cause": "The browser bundle is internally inconsistent: `index.js` imports a content-hashed chunk that is not on disk. `ensureClientServed` probes **only `/dist/index.js`**, which is served with a 200 in this state — so the apply is reported as successful and the fault reads as an application bug", "file": "`cmd/mxcli/docker/webclient_chunks.go` (`danglingClientChunks`), called first from `ensureClientServed` in `runlocal.go`", "insight": "**Walk the import graph from `index.js`, and follow only chunks that EXIST.** An orphan left by an earlier bundle references old hashes, so scanning every file in `chunks/` reports a healthy bundle as broken. One level is not enough either — a real rollup bundle has `index.js` importing a single chunk with the rest hanging off it, so a one-level check misses almost every real breakage. The safety control matters more than the detection here, because this gates an automatic re-bundle: run it against several REAL built bundles and require zero findings, then delete one referenced chunk from a copy and require detection. ako/mxcli-maintenance-2"} -{"area": "cmd/mxcli", "date": "2026-08-28", "symptom": "A documented `~/.mxcli/mxbuild/*/modeler/mx check app.mpr` fails with `Verb '/root/.mxcli/mxbuild//modeler/mx' is not recognized`, and it looks like a broken mxbuild install", "cause": "The shell expands the `*` to every cached version and `mx` reads the second path as an argument. Both binaries are fine; it is purely the command line, and the failure mode is misleading enough that an earlier report recorded the wrong binary as broken", "file": "`cmd/mxcli/init_claudemd.go` (the generated project docs), plus ~15 `.md` files including three SYNCED skills (`manage-security`, `custom-widgets`, `alter-page`)", "insight": "**Document `mxcli docker check -p .mpr` first** — it resolves the version from the project, which is the whole problem. Where `mx` must be called directly, name a version (``), never glob. Note the split: `filepath.Glob` in Go (`cmd/mxcli/docker/detect.go`) is CORRECT and must not be \"fixed\" — it globs then picks, whereas a shell passes all matches through. This reaches every user, since anyone who has used two Mendix versions has two cached. ako/mxcli-maintenance-2"} -{"area": "cmd/mxcli", "date": "2026-08-28", "raw": "| A test fails only on CI and never locally — e.g. `TestWatcherDebounce`: `expected 1 debounced message, got 2` | The test ASSUMED a timing premise instead of checking it. A debounce test needs the whole burst of writes to land inside the debounce window; each write resets the timer, so a gap larger than the window splits the burst and it debounces as two. On a loaded runner one gap between five `os.WriteFile` calls exceeded 500ms | `cmd/mxcli/tui/watcher.go` (`newWatcherDebounced`, the window is a field not a constant), `cmd/mxcli/tui/watcher_test.go` | **Get the observed value out of the CI log before theorising** — `gh run view --log \\| grep `. I guessed \"count 0, event delivery too slow\" and built two controls for it; both passed, because the log said `got 2` and the real mechanism was the opposite one. Three fixes, each aimed at the measured failure: a window far longer than production (2s vs 500ms) so headroom is orders of magnitude rather than 2×; the premise **measured and skipped on** (`if burst >= debounce { t.Skip }`) so a machine too slow to emit a burst reports its own limitation instead of a product defect; and the message **polled for, not slept for**, so delivery latency cannot decide the outcome. The grace after the first message must be a FULL window — a split's second timer fires one window after the last write, so a shorter wait stops looking before the evidence arrives. Control the mechanism deterministically (100ms window, 150ms gaps → count 5) rather than trying to reproduce load, and verify the skip path actually fires (temporarily set the window to 1ns). This is the checklist item \"tests don't rely on `time.Sleep` for synchronization\" — the original even named the hazard in a comment without removing it. |"} -{"area": "cmd/mxcli", "date": "2026-08-29", "symptom": "CodeQL `go/incorrect-integer-conversion` (high) on `strconv.ParseUint(…, 16, 32)` narrowed to `int`", "cause": "The parse width was chosen from the *field's* width in the file, not the *type's*. A TCP port is 16 bits and the kernel writes it `%04X`, so a 32-bit parse accepts values the caller then compares as if they were ports; where `int` is 32 bits the conversion wraps (`FFFFFFFF` → `-1`, `ok=true`)", "file": "`cmd/mxcli/docker/portowner_linux.go` (`hexPort`)", "insight": "**Parse at the target type's real width and let the parser do the range check** — that is the whole fix, and it turns an unchecked narrowing into a rejection at the boundary. Establish reachability before rating it: the input here is `/proc/net/tcp`, written by the kernel, so no hostile value reaches it and the consequence of the wrap is a *missed* port match (the diagnostic falls back to its generic message), not memory unsafety. Two things make it worth fixing anyway — a range check that lives in the parser cannot be forgotten by the next caller, and the alert otherwise stays open forever. Measure the wrap rather than asserting it: a `GOARCH=386` binary **runs on an amd64 host**, so the 32-bit behaviour is directly observable, and the same run showed the repo does not currently build for 386 at all (`mdl/settingsoverlay` has `9007199254740992` as an untyped int constant) — worth knowing before claiming a 32-bit impact. A test written at 64 bits still catches it: at width 32 `hexPort` returns `4294967295, true` where it should refuse. Keep a 32-bit-safe test literal (`int(^uint32(0))`, not `4294967295`, which is not a valid `int` constant on 386)."} -{"area": "cmd/mxcli", "date": "2026-08-29", "raw": "| CodeQL `go/unsafe-unzip-symlink` on a tar extractor that already checks the joined path and the symlink target — looks like a false positive | It is not. A **chain of two symlinks** escapes: the guard resolves each link against its **lexical** parent while the OS resolves it against its **real** one. `sub/up -> ..` genuinely is the root (allowed, correctly); `sub/up/w -> ..` therefore really is `root/w`, pointing at the root's **parent**, and lexically looks like `root/sub`. Then `sub/up/w/pwned` contains no `..` and passes every check | `cmd/mxcli/docker/download.go` — one `extractTarGzInto` on `*os.Root` behind both `extractTarGz` and `extractTarGzStrip1`, plus `tarEntryPath` / `symlinkStaysInside` / `pathWithin` | **Try to break it before rating it, and keep trying after the first six attempts fail.** Six single-step archives (absolute link, `../..` link, absolute name, link-to-root, …) were all correctly refused, which is exactly what makes \"false positive\" tempting; the seventh — two chained up-links — wrote a file into the extraction root's parent, and an eighth reached any sibling directory. The fix is `os.OpenRoot` + `Root.OpenFile`/`MkdirAll`/`Symlink` (Go 1.24/1.25), which hands containment to the kernel (`openat2` `RESOLVE_BENEATH`) instead of to string comparison — the failure is then `path escapes from parent` from the OS. Keep a **link-target** check as well and resolve its parent with `EvalSymlinks`: `os.Root` stops *mxcli* following an outward link, not mxbuild or the JVM that read the tree afterwards. Two things the rewrite fixed for free: `strings.Contains(name, \"..\")` silently dropped legitimate files (`foo..bar` never appeared, no error) — `filepath.IsLocal` is the right test, on path *elements* — and a `HasPrefix` containment test with no separator puts `/cache/runtime-evil` inside `/cache/runtime`. Settle \"would refusing break real downloads?\" by measurement, not caution: the 11.13.0 CDN tarballs are 3,646+279 (runtime) and 33,188+5,036 (mxbuild) entries with **zero symlinks**, listed with `curl \\| tar -tzv` without saving the archive. Control both directions — the ordinary-archive test must pass, and against the old extractor the chained cases must actually write outside (they do, in both the stripping and non-stripping variants). |"} -{"area": "cmd/mxcli", "date": "2026-08-30", "raw": "| `mxcli run --local`/`--hub` sits at several hundred percent CPU for hours while the app is gone; `ps -o stat` shows the runtime JVM as `Z` (zombie) under it, `curl localhost:` returns `000`, and the hub preview URL still answers | `cmd/mxcli/docker/localboot.go` (`watchExit`, `alive`, `stopProcess`), `cmd/mxcli/docker/runlocal.go` (`waitForInterruptOrExit`, `runtimeStoppedError`) | Two mechanisms. (1) **Nothing ever called `Wait()`** on the runtime process, so an exited JVM stayed an unreaped zombie — and `alive()` asked `Signal(0)`, which **succeeds on a zombie**. Measured (Linux 6.18, go1.26): proc state `Z`, `Signal(0)` → nil; after `Wait()` → \"process already finished\". So the liveness check reported a runtime that had terminated itself hours earlier as alive. `Signal(0)` is only a correct liveness test *because* something reaps — removing `watchExit` silently breaks that line, which is why the control is on the reaper, not on `alive()`. (2) **After boot, `run` waited on a signal and nothing else**, so a correct answer had no one asking; it now waits on the signal OR `rt.Exited()` and returns a **non-zero** error, because returning 0 after the app has gone is what let a supervisor conclude all was well. **Why it happens at all**: the local standalone runtime uses a development licence with a maximum run time and terminates *itself* (measured lifetimes 3h52m and 5h07m — not a fixed number, and shorter than a working session); `runtimeExitReason` lifts that from the runtime's own log, and reports nothing rather than guessing when it cannot tell. **Two waiters on one process deadlock**, so `stopProcess` consults the reaper's channel instead of taking its own `Wait`. The CPU spin itself was NOT reproduced and is not claimed fixed by name — it lived in the tunnel client, under a supervisor blind to its dead child; what is fixed is the state it occurred in, since mxcli now exits and takes the tunnel with it. **Generalisable**: a 200 from a tunnelled URL is not evidence the app is alive — the tunnel outlives the runtime. Reported as mxcli-formula1 FINDINGS §60 |"} -{"area": "cmd/mxcli", "date": "2026-08-30", "raw": "| A widget keyword the grammar accepts is absent from `mxcli syntax page widgets`, so it is concluded not to exist and worked around at length (reported for `tabcontainer`, which cost two days and five hand-rolled pages) | `cmd/mxcli/syntax/features_page.go`, `cmd/mxcli/syntax/widget_keywords_drift_test.go` | The lesson the reporter drew — \"absence from the documentation is not absence from the grammar\" — is true and is a bad thing for the docs to require. `TestEveryWidgetKeywordIsInAPageSyntaxTopic` makes it false instead: it reads the `widgetTypeV3` rule out of the **committed** `.g4` (only the *generated parser* is uncommitted, and the grammar is the authority the reporter was told to consult) and fails when a keyword appears in no `page.*` topic. It found **18**, not one. Exemptions go in `documentedElsewhere` **with the topic that owns them** — layout constructs (`scrollcontainer`, `region`, `navigationtree`, `menubar`, `placeholder`) and pluggable-widget object-list keywords (`group`, `series`, `marker`, …) are not page widgets; an entry with no home is the same defect. The guard carries its own vacuity control: a keyword that does not exist must not match, and one that does must. **Do not document a keyword without running it** — probing all 18 on 11.13 found four the parser accepts and the *default engine refuses* (`statictext`, `staticimage`, `dynamicimage`, `dropdown` → \"widget *pages.X not yet supported by the modelsdk engine\"), two refused on both engines (`referenceselector`, `legacydatagrid`), and one whose bare form emits **CE0463** (`image`). Reported as mxcli-formula1 FINDINGS §69 |", "ce": ["CE0463"]} -{"area": "cmd/mxcli", "date": "2026-08-31", "symptom": "Every open PR goes red at once on `build-and-test` with a failure in a package none of them touched — `--- FAIL: TestSessionLog_PersistAndPrune`, \"after reload+prune: 0 records, want 1\" — and the same test fails on a clean checkout of `main`", "cause": "A **time bomb in the test**, not a regression: the fixture pinned `base := time.Date(2026, 8, 1, ...)` against a 30-day retention window, and `NewSessionLogFile` prunes inside `load()` — *before* the test can assign `log2.now`, so the reload prune runs on the real `time.Now()` whatever clock is injected afterwards. It passed for 30 days and then failed permanently, on every branch simultaneously", "file": "`cmd/mxcli/tunnelhub/sessions_test.go` (`TestSessionLog_PersistAndPrune`), `cmd/mxcli/tunnelhub/sessions.go` (`load` → `pruneLocked` → `clock`)", "insight": "**First establish it is not yours**: run the failing test on a clean `origin/main`. Several PRs failing on one unrelated test is the signature. Then make the fixture relative — `base := time.Now().UTC()` — so the record ages, not the calendar, decide the outcome; the other tests in the file keep their fixed base legitimately, because they use `NewSessionLog` and inject the clock before recording. **A date fixture is only safe where no code path reads the real clock**; the moment a constructor prunes, expires or compares against `time.Now()` before the seam is in place, an absolute date has a fuse on it. Control the repair: stub `pruneLocked` to a no-op and confirm the test still fails (2 records, want 1), or the fix is just a test that stopped testing"} -{"area": "cmd/mxcli", "date": "2026-08-31", "symptom": "`mxcli test --local` cannot start a runtime for ANY project: `Error: local runtime: runtime admin API did not come up: runtime process exited during startup`, then `java.lang.IllegalArgumentException: Path '/.mxcli/deployment-test/model/bundles' cannot be resolved in base path '/.mxcli/deployment-test'`. The tree has `data/` and no `model/`", "cause": "**mxbuild writes the deployment to `/deployment` and has no option to move it** \u2014 measured, not inferred: `--target=deploy` on a project whose `deployment/` had just been deleted recreated it there, `mxbuild --help` lists no deployment-path flag, and `BuildRequest` carries none. So giving the test boot a `DeployDir` of its own moved where the RUNTIME reads and not where the BUILD writes, and it booted against an empty directory. `StartLocalApp` now refuses a `DeployDir` the build will not populate, naming mxbuild as the constraint. The \u00a762 blanking the scratch tree was meant to prevent is fixed the only way the constraint allows: `preserveWebClientBundle` copies `deployment/web/dist` aside before the boot and puts it back after (a few MB of copy, against the ~30s re-bundle that made warning the earlier choice), restoring only when the bundle is actually gone so a fresher one is never clobbered.", "file": "`cmd/mxcli/docker/localapp.go` (`checkDeployDirIsBuildable`, the `preserveWebClientBundle` call), `cmd/mxcli/docker/webclient_preserve.go` (new), `cmd/mxcli/docker/localapp_integration_test.go` (new), `cmd/mxcli/testrunner/localapp_options.go`, `cmd/mxcli/testrunner/runner_local.go`, `cmd/mxcli/testrunner/runner.go`", "insight": "**The lesson is in the tests that did not catch it**, and two reporting projects drew it independently (mxcli-ledger \u00a7150, mxcli-sudoku \u00a751): four unit tests asserted `DeployDir` was set, was under `.mxcli/`, was per-project and was not the dev loop's \u2014 all four passed against a build that could not start, because every one was about the OPTION while the symptom lived in what is on disk after mxbuild runs. A fix that redirects a consumer without redirecting the producer moves the failure rather than removing it. The guard is now an integration test asserting the artefact: after a real build, the directory the runtime boots against holds `model/bundles`. Note the reporters' prescribed fix \u2014 thread `DeployDir` through the build \u2014 is not available; mxbuild has no such parameter, which is why the shared tree is accepted and the bundle carried across instead. Controls, both end-to-end on a real 11.13 project: the pre-fix binary reproduces the reported JVM error byte for byte while the fixed one passes; and with `preserveWebClientBundle` stubbed the sentinel bundle is destroyed by a test run that still reports all tests passed. Reinstating the regression fails the new integration test naming both missing paths."} -{"area": "cmd/mxcli", "date": "2026-09-01", "symptom": "After `mxcli test --local`, a live `mxcli run --local` serving the SAME project starts answering **HTTP 200 with a zero-byte body** on every microflow-backed resource \u2014 not a 500, not an error page \u2014 while source-backed ones keep working, so half the app looks fine. The runtime log shows `java.lang.NoClassDefFoundError` on a project class. In a two-app solution it surfaces as tests failing in the OTHER app.", "cause": "The test run recompiles the project's Java into `deployment/run/bin`, which is the classpath the running JVM is holding open. Measured on a real 11.13 project: after one test run all 134 class files have **new inodes and byte-identical content** \u2014 every one deleted and rewritten. A JVM loads classes lazily, so one it has not reached yet can fail permanently. mxcli cannot prevent this: mxbuild's Gradle pass owns the compile and the deployment directory cannot be moved (ledger \u00a7150). So it warns instead, which is what was missing.", "file": "`cmd/mxcli/devloop_recompile_warning.go` (new \u2014 `warnIfDevLoopServing`, `recompileWarning`), `cmd/mxcli/cmd_test_run.go`; reads the existing `cmd/mxcli/devloop_handshake.go`", "insight": "**The mechanism already existed and a duplicate would have broken it.** `mxcli run --local` publishes `devLoopHandshake` at `.mxcli/run-local.json` for `mxcli constant set --apply` \u2014 same path, same pid-liveness staleness check, plus the `adminPass` and `bootConfig` that `--apply` and `--attach` depend on. A second state file was written at that path before this was noticed; it parsed fine (JSON ignores unknown fields) but its WRITER would have silently dropped those two keys. Grep the path before inventing a file. **The liveness check is the feature**: a `run --local` killed or ended by its development licence (\u00a760, measured lifetimes under six hours) leaves the file behind, and a warning driven by the file alone fires forever \u2014 one that is always wrong teaches the reader to skip it. It **warns rather than refuses**, since the warm loop exists so an app can stay up while you work and the reporting project runs two apps that way; neither `--attach` nor `--skip-build` builds, so neither warns. The finding's cost was diagnosis, not breakage \u2014 108 log lines and a wrong hypothesis about a different app, for something whose remedy is one restart \u2014 so the warning names the symptom (HTTP 200, empty body), the part nobody guesses. Controls, end-to-end against a real `run --local`: the warning carries that app's actual pid and port and its handshake still has adminPass and 9 bootConfig keys afterwards; with the loop stopped, and with a stale dead-pid handshake, the same command is silent. Reported as mxcli-formula1 FINDINGS \u00a781."} -{"area": "cmd/mxcli", "date": "2026-09-03", "symptom": "`mxcli brain check` reports an entry as MISFILED even though the entry is correct and its anchor points at a real document — the anchor's target is simply of a document type the catalog's `objects` view does not index", "cause": "Misfiling was decided by comparing the shard against the modules of *resolved* anchors. An entry whose only anchor came back NotIndexable had an empty resolved-module list, so the comparison found no match and reported it misfiled — reintroducing, through the misfiling axis, exactly the false staleness that the NotIndexable state exists on the anchor axis to prevent", "file": "`cmd/mxcli/brain/entry.go` (`MisfiledIn`)", "insight": "When a check has two axes, an 'unknown' outcome on one of them must not be read as a negative on the other. The fix is to make misfiling *undecidable* rather than false when nothing resolved: with no resolved anchor there is no evidence about where the entry belongs, and an anchor that truly names nothing is already a failure on its own axis. Caught in development by a table test whose control stubbed the guard to `if false` — a control that deletes the block instead fails to compile on unused variables, which is not a control", "refs": ["ako/mxcli#385", "PROPOSAL_project_brain.md A1"]} -{"area": "cmd/mxcli", "date": "2026-09-03", "symptom": "`mxcli brain check` exits 1 on a requirement that is simply not built yet — the entry is correct and current, and the check reports its anchor as NOT FOUND", "cause": "Requirements were recorded as ordinary brain entries, but an entry's anchor was assumed to point BACKWARD at something that exists. A decision's unresolved anchor means the decision is stale; a requirement's unresolved anchor means the work is not done. Same syntax, opposite meaning, and the store had no way to tell them apart", "file": "`cmd/mxcli/brain/entry.go` (`Kind`), `cmd/mxcli/brain/check.go` (`checkSlice`)", "insight": "Before adding a record type to an existing store, ask what a FAILED validation means for it — not just what it looks like. Requirements and decisions share the anchor syntax exactly, which is what made them look like the same thing; they differ only in the direction the anchor points, and that difference is the whole lifecycle. Measured before designing: one unbuilt requirement filed as a decision took `brain check` to exit 1, which settled it in one command. The inversion then pays for itself — a requirement is 'built' when its anchors resolve, so `brain plan` reports progress derived from the model (measured 0/1 -> 1/0 after creating the microflow, with the plan file untouched) instead of a status column that goes stale silently", "refs": ["ako/mxcli#385"]} -{"area": "cmd/mxcli", "cause": "BuildResult parsed only status/restartRequired/message and left everything else in Raw, so a failed build was reported by dumping the whole response body; nothing looked at the per-problem severity/errorCode/locations mxbuild actually returns. The consistency error that stopped the build therefore arrived unmarked among the warnings, and no test was named.", "ce": ["CE0109", "CE0117"], "date": "2026-09-03", "file": "cmd/mxcli/docker/mxserve.go (BuildProblem/BuildLocation/Errors/ErrorSummary/BuildFailedError), cmd/mxcli/testrunner/build_attribution.go (new), wired at cmd/mxcli/testrunner/runner_endpoint.go", "insight": "**The serve /build response already carries everything needed to attribute a build failure, and nothing was reading it.** Measured on 11.13: `problems` is an OBJECT whose inner `problems` list holds each consistency message with severity, errorCode and locations[] {module, document, element} \u2014 the document being `Microflow 'Test_test_3'` WITHOUT its module. So an error in a generated test microflow maps back exactly. The ratio is the point: a failing blank app returns 18 problems of which 1 is the error, and printing the body meant 11,580 bytes in which nothing marked the line that mattered; filtering severity==Error renders it as one line naming the test AND the decision. **Do not guess a response shape \u2014 POST to the serve API and look.** `mxbuild --serve --host=127.0.0.1 --port=N` plus a curl to /build is the whole harness, and no fixture in the repo had ever recorded a FAILING build.\n\n**The abandoned half is the more useful lesson.** Catching these earlier \u2014 refusing an unbound variable in an IF condition at injection time \u2014 was built, passed 465 check-mdl scripts and the whole unit suite, and was WRONG. `mxcli test` execs its microflows, and the microflow validator's scope model tracks variables where they are ASSIGNED; reusing it to check READS refuses valid work. Two independent holes, both found only by `make test-integration`: `$latestHttpResponse` is a Mendix system variable that no MDL statement declares, and a loop iterator is registered only when the list's type is known (`if listType, ok := fb.varTypes[...]`). Both refuse a microflow `mx check` accepts at 0 errors. **Generalisable: a variable model built for checking writes is not a variable model for checking reads** \u2014 the write side only has to know the names being bound, the read side has to know every name that can legally be in scope, including ones the platform supplies. Before reusing any scope model in the opposite direction, enumerate what populates it and assume the list is incomplete.\n\n**Process: `make check-mdl` is NOT the over-reach guard for an exec-path change.** It runs `mxcli check` with no project, so it exercises syntax only; a new refusal on the exec path sails through all 465 scripts. `make test-integration` (what CI runs, and runnable locally with mxbuild cached) execs every doctype script against a real project and runs `mx check` on the result \u2014 that is the guard, and skipping it cost a red CI. Also note the exec/check validator split (#833): a validator fix wired only into ValidateMicroflowBody looks correct under `check -p` and does nothing under `exec`.", "refs": ["ako/mxcli-sudoku FINDINGS #46 follow-up"], "symptom": "`mxcli test --local`: an @expect that is syntactically valid but only fails inside mxbuild takes down the ENTIRE run \u2014 `Error: local runtime: build failed: The project cannot be deployed, because it contains errors.` No test results at all, valid tests in the same file never run, and the cause arrives as ~200 lines of mxbuild JSON in which the real error sits among dozens of unrelated Atlas warnings."} -{"area": "cmd/mxcli", "date": "2026-09-11", "symptom": "`mxcli marketplace update --to -p App.mpr` exits 0, reports \"N units copied, N element identities preserved, N role grant(s) restored\", and leaves the project at **CE0066** \"Entity access is out of date. Please update security by clicking the 'Update security' button in the domain model editor\" at \"Domain model of module ''\". Its \"Next, repair what a headless update leaves behind\" block names CE0463 and CE6087 only, so the error reads as needing Studio Pro — reported as \"no headless fix available\", breaking unattended upgrade pipelines (mendixlabs/mxcli#1085, against 0.21.0 on Mendix 11.12.4, UserCommons 2.1.3 -> 2.2.0). `marketplace install` has the same gap.", "cause": "A transplant is the ONE mxcli write path that does not reconcile as it writes. `TransplantModule` copies the incoming module's units in verbatim through the raw-unit writer, and `RestoreRoleGrants` (cmd/mxcli/marketplace/grants.go) runs its statements with `ex.Execute(stmt)` in a loop rather than `ExecuteProgram`, so `finalizeProgramExecution` — the only thing that calls `ReconcileMemberAccesses` outside the entity/grant/association handlers — never runs. An access rule that does not cover every member of its entity therefore arrives exactly as the package shipped it. Fixed by `marketplace.ReconcileModuleSecurity` (new, cmd/mxcli/marketplace/security.go) called at the end of `PerformUpdate` and `PerformInstall`, non-fatally (the module is already replaced; aborting there would leave the project mid-update), with the count reported by `reportSecurityReconcile` and the failure path naming `update security `.", "file": "cmd/mxcli/marketplace/security.go (new, ReconcileModuleSecurity), cmd/mxcli/marketplace/update.go (PerformUpdate, PerformInstall, UpdateResult.RulesReconciled/SecurityErr), cmd/mxcli/cmd_marketplace_update.go (reportSecurityReconcile), cmd/mxcli/cmd_marketplace_install.go; tests cmd/mxcli/marketplace/security_test.go; example mdl-examples/bug-tests/marketplace-1085-security-after-transplant.mdl", "insight": "**The control is what localises this one, and it is cheap: transplant the module UNCHANGED.** On the vendored 11.6.6 app (Administration 4.3.2, mxbuild 11.14.0) an unchanged update checks at 0 errors, and removing ONE member entry from the incoming version's rules gives exactly the reported CE0066 — so the machinery does not create the stale rule, it transmits one, and the fix belongs at the point of arrival rather than anywhere in the copy. Three things worth carrying. (1) The reporter's premise was half wrong in a way that matters more than the bug: a headless repair already existed (`UPDATE SECURITY`, #1047), but it appears in NO user-facing surface — not docs-site, not MDL_QUICK_REFERENCE.md, not `mxcli syntax`, not the update's own output — only in a skill and the CHANGELOG. An undiscoverable capability and a missing one are the same bug report. (2) Their binary could not have used it anyway: v0.21.0 (2026-09-06) has the System-skip fix but NOT the inherited-attribute reconcile that landed 2026-09-07 (`collectAttrs` is absent), so where the missing member is inherited from a same-module generalization it would have printed \"All entity access rules are up to date\" over a broken model. Check the reported version against the fix DATE, not against the fix existing. (3) Reconciling unconditionally would be wrong, and the second test is the one that says so: a module whose rules are already complete must come through untouched, because rewriting a marketplace module's domain model makes `marketplace diff` read it as a local edit — and a local edit is what makes the NEXT update refuse. The repair cannot be demonstrated by an .mdl script either: every MDL write path reconciles as it writes, so the stale state has to be built by editing stored BSON in the test.", "ce": ["CE0066"], "refs": ["mendixlabs/mxcli#1085", "mendixlabs/mxcli#1047"]} -{"area": "cmd/mxcli", "date": "2026-09-10", "symptom": "`mxcli run --local --watch` cannot start a Mendix 11.14 app: `starting web client bundler: no rollup.config.mjs in \u2026/deployment/web (run a serve Deploy build first)` \u2014 while plain `run --local` on the SAME project works. A serve Deploy build has just run", "cause": "The identical defect as ako/mxcli-ledger #146, in the SECOND copy of the gate. 11.14 bundles the browser client itself and emits no rollup config; #146 moved the gate onto the gap the step closes, but only in `BuildWebClient`. `StartWebClientWatch` carries its own independent `os.Stat(rollup.config.mjs)` check that the fix never touched \u2014 so the non-watch path started working and `--watch` kept dying on the same absent file, one call earlier. #146's own commit message claimed \"both call sites are fatal\"; the two it meant were both BuildWebClient's", "file": "`cmd/mxcli/docker/webclient_watch.go` (`StartWebClientWatch`), called fatally at `runlocal.go:659`", "insight": "**When a gate is duplicated, `grep` for the predicate before declaring the fix done** \u2014 the sibling here was one `os.Stat` of the same filename in the same package, and the commit that fixed one asserted it had fixed both. Fix shape: return a **nil watcher**, not an error, when `WebClientBundled(deployDir)` \u2014 there is no bundler to keep hot because mxbuild's serve build writes web/dist. That is only half the fix: the watch loop calls `Generation`, `WaitForRebuild` and `Log`/`Stop` with no nil branch, so the methods must be nil-receiver-safe or the startup refusal is merely traded for a SIGSEGV on the first file change (the control test panics against the unfixed build, which is what proves it). Keep a control asserting the 11.13 path still LAUNCHES the bundler, or the fix degrades to \"never watch anything\"", "refs": ["#146"]} -{"area": "cmd/mxcli", "date": "2026-09-10", "symptom": "`mxcli run --local --watch` on a Mendix 11.14 app: the FIRST build succeeds and the app boots, then every rebuild fails inside mxbuild \u2014 `Could not find a part of the path '\u2026/deployment/web/pages/.js'` when the change touched a page, or `Compilation of the app bundle failed` / `ERR_MODULE_NOT_FOUND \u2026 web/rollup.config.mjs` when it did not. Reads as a corrupt `deployment/`", "cause": "**mxbuild 11.14's own defect, not mxcli's.** The first build in an `mxbuild --serve` process does not leave the deployment in a state its own incremental build can continue from: neither the bundler config (`web/rollup.config.mjs`) nor the per-document client dirs (`web/pages/`, `web/layouts/`) survive it, and the next build needs both. Which one it dies on is just how far it gets \u2014 model changed \u2192 page export, model untouched \u2192 the bundler. Note `--serve`, `--host` and `--port` are in `mxbuild --help` but NOT on docs.mendix.com/refguide/mxbuild/, so this is an undocumented interface with no compatibility promise", "file": "`cmd/mxcli/docker/webclient_legacy_paths.go` (`legacyClientBuildHint`), called from the build-failure branch of `watchAndApply` in `runlocal.go`", "insight": "**Drive the vendor's API directly before blaming or absolving your own wrapper.** Three controls placed this in mxbuild in about ten minutes: (1) POST the SAME `/build` request twice over curl with the model untouched \u2014 Success then Failure, no mxcli in the picture; (2) run one-shot `mxbuild --target=deploy` TWICE into the same deployment dir \u2014 BUILD SUCCEEDED both times, so the 11.14 deployment shape is not the trigger, the serve process's own state is; (3) flip `EnableRspackBundler` in the model and repeat \u2014 identical failure naming `rspack.config.mjs`, which kills 'switch bundlers' as a workaround AND disproves the tempting 'it falls back to the legacy Rollup pipeline' story. Without (3) the diagnosis was plausible, quotable and wrong. mxcli cannot fix it, so the deliverable is a hint that fires on the failure's own SHAPE (message names the artifact AND the artifact is genuinely absent) rather than on a version number \u2014 so a fixed mxbuild goes quiet by itself, and it says what does NOT work (`rm -rf deployment/`, switching bundlers), because both are what a user tries first || FOLLOW-UP: the tempting workaround does NOT work, and it is worth knowing why before anyone tries again. The deleted config is recoverable \u2014 it sits on disk ~1.5s mid-build and carries nothing model-specific (input is just index.js; page discovery is delegated to a rollup plugin at build time), so it can be harvested the way `mxcli fix widgets` harvests mxbuild's output. Restoring it makes builds 2 and 3 succeed **with the model unchanged**, and fail the moment a page actually changes (missing web/pages/*.js); adding the missing dirs too moves it to a pluggable-widget export failure on two widgets that ship with a blank app, with web/dist/index.js never moving off its cold-build timestamp. So there are TWO regressions and only the visible one is recoverable. **Test a workaround against the case the feature exists for, not the case that is easy to set up** \u2014 an unchanged-model rebuild is exactly what a warm loop never does, and a green result there would have shipped a fix that helps nobody", "refs": ["#146"]} -{"area": "cmd/mxcli", "date": "2026-09-11", "symptom": "The record-narrated-demo skill MANDATES a mobile pass ('Recorded at both a desktop viewport and a real mobile device profile') and its own `take.js` cannot perform one: the 'mobile' take films the DESKTOP app in a narrow window. Separately, the caption plate made any control it covered unclickable \u2014 Playwright retried 30s against 'div.text from div#demo-narration subtree intercepts pointer events' and failed the take", "cause": "`openTake` called `browser.newContext({viewport, recordVideo})` and nothing else, but `userAgent`/`isMobile`/`hasTouch`/`deviceScaleFactor` are CONTEXT options \u2014 and **Mendix picks its navigation profile from the user agent, not the viewport**, so shrinking the viewport never routes to the phone profile. The plate had no `pointer-events: none` (only `#demo-spot` did) while being a full-width bar pinned to the bottom \u2014 exactly where Mendix puts a page footer's buttons", "file": "`.claude/skills/mendix/record-narrated-demo/take.js` (`openTake`), `narrate.js` (`OVERLAY_CSS`); embed mirror under `cmd/mxcli/skills/` is regenerated by `make sync-skills`", "insight": "**When a skill mandates a verification pass, check that the tool it ships can actually perform it** \u2014 this one could not, and nothing said so: the take succeeded, produced a plausible film, and simply filmed the wrong thing. A verification step that cannot fail is worse than no step, because it is counted as done. The fix is a `contextOptions` passthrough spread FIRST so `viewport`/`recordVideo` still win (a device preset carrying its own viewport would letterbox every take). For the plate, `pointer-events: none` makes a covered control CLICKABLE and `body { padding-bottom }` makes it VISIBLE \u2014 the film needs both, since a click landing under an opaque caption is the same dead beat by another route. Reported by ako/ChipCoV1, which shipped an app whose phone walk was impossible while seven browser tests stayed green", "refs": ["ako/ChipCoV1 FINDINGS.md"]} -{"area": "cmd/mxcli", "date": "2026-09-11", "symptom": "Two opposite `brain plan` failures with one root cause. A requirement anchored at a bare MODULE (`@Maintenance`) reports BUILT the moment the module exists, with none of its work done. A requirement anchored at a MODULE ROLE (`@Maintenance.Coordinator`) reports PLANNED forever, even once the roles exist \u2014 `describe` refuses it ('no describable document named \u2026') and `mxcli refs` finds nothing", "cause": "`catalogResolver.Resolve` in `cmd/mxcli/cmd_brain.go`. A module has a row in the catalog's `objects` view, so it resolves immediately \u2014 fine for a decision (anchors point backward) and useless for a requirement (anchors point forward, so resolution IS the progress signal). A module role is in NEITHER the objects view NOR `FindDocumentUnit`, because it is not a document \u2014 so both lookups miss and it falls through to NotFound, which for a requirement means 'not built yet' permanently", "file": "`cmd/mxcli/cmd_brain.go` (`catalogResolver.Resolve`, `moduleRoleExists` via `GetModuleSecurity`), `cmd/mxcli/brain/entry.go` (`requirementAnchorsArePlannable`)", "insight": "**A resolver's vocabulary has to cover what people actually anchor at, and the two failure directions need opposite fixes.** The module-role gap is a LOOKUP gap \u2014 fixed by consulting `GetModuleSecurity`, case-insensitively because Mendix treats role names that way and an anchor is hand-written. The module-anchor gap is SEMANTIC and cannot be fixed by lookup: the anchor resolves correctly and is still useless, so it is refused at capture time with the alternative named (an author told only 'no' deletes the anchor, which loses the signal entirely rather than fixing it). Keep the control in BOTH directions: a module anchor stays legal on a decision and on a question, or the fix degrades to 'refuse every module anchor' and the negative test still passes. Verified end to end: `@MyFirstModule.User` \u2192 1 anchor 1 resolved; `@MyFirstModule.NoSuchRole` \u2192 NOT FOUND, exit 1, so the resolver did not simply become permissive. Reported by ako/ChipCoV1", "refs": ["ako/ChipCoV1 FINDINGS.md"]} -{"area": "cmd/mxcli", "date": "2026-09-11", "symptom": "`.claude/bootstrap-mxcli.sh` (the SessionStart hook) re-downloads ~85 MB of mxcli on EVERY fresh session in an environment where mxcli is already installed on PATH", "cause": "The script gated only on `[ ! -x ./mxcli ]` and never consulted PATH, while the bootstrap skill tells you to `rm -f /mxcli` after moving the project to the repo root \u2014 so in a session image with mxcli pre-installed the guard could never be satisfied by the binary already present, forever", "file": "`cmd/mxcli/init_hook.go` (`bootstrapScriptTemplate`)", "insight": "**A guard that asks 'is the artifact HERE' rather than 'is it AVAILABLE' pays full cost for something already on the machine.** Hardlink a PATH copy in first (what `mxcli new` itself does), falling back to symlink across filesystems then to a copy, and only download when none of those work \u2014 every path leaves ./mxcli working, which the rest of the script and the generated project CLAUDE.md both assume. Keep BOTH controls when testing: nothing on PATH must still download (not silently no-op), and an existing ./mxcli must be left untouched. Reported by ako/ChipCoV1", "refs": ["ako/ChipCoV1 FINDINGS.md"]} -{"area": "cmd/mxcli", "date": "2026-09-11", "symptom": "`mxcli theme create --from ` seeds the palette and nothing else: the scaffolded brand theme still described itself in `theme list` as 'Cool slate, one teal signal colour' with Signal's six swatches, and vendored ~500 KB of IBM Plex woff2 the seeded --mxt-font never names, plus a SIL OFL licence for fonts it does not use. Separately, the primary button was never the brand colour: Atlas derives --btn-primary-bg from --brand-primary-600 = color-mix(brand, contrast 20%), so a brand blue #10069F rendered rgb(21,13,140)", "cause": "`manifest()` copied the base theme's Summary and Colorway verbatim and the walk copied every file unconditionally. The Atlas map pinned `--btn-primary-color` to `--mxt-brand-ink` \u2014 an ink each theme picks to sit on `--mxt-brand` (console pairs near-black #04211d with bright teal #2dd4bf) \u2014 while leaving the background to Atlas's derivative, so the pairing the theme designed for was never the pairing that rendered. The map's own comment already called it 'a brand-filled button'", "file": "`cmd/mxcli/theme/create_seeded.go`, `create.go` (`manifest`, the scaffold walk), `assets/*/files/theme/web/_mxcli-atlas-map.scss`", "insight": "**Inheriting a statement ABOUT the base theme into a theme whose palette is no longer the base's is a confident lie; derive it or drop it.** Two traps in the font half. (1) The decision must be made BEFORE the walk: it is taken by reading the partial and applied to files elsewhere in the tree, so doing it inline depended on WalkDir's lexical order putting `_mxcli-.scss` before `mxcli-fonts/` \u2014 true only because of the leading underscore. (2) It touches two halves \u2014 the @font-face rules and the woff2 files \u2014 and getting either alone wrong is silent: a surviving rule for a deleted file 404s, a surviving file nothing loads is the dead weight being removed. Unit tests on each half cannot catch a mismatch; the guard is an integration assertion that a scaffolded theme ships exactly the fonts it loads (control: stubbing the file half fails it with 'X is shipped but no @font-face loads it'). Reported by ako/ChipCoV1", "refs": ["ako/ChipCoV1 FINDINGS.md"]} -{"area": "cmd/mxcli", "date": "2026-09-13", "symptom": "`mxcli run --local --ensure-db` cannot provision PostgreSQL in a non-root devcontainer (Debian/Ubuntu base, remoteUser vscode): reported as a bare \"PostgreSQL did not become ready at 127.0.0.1:5432 within 20s\", or as `exec: \"initdb\": executable file not found in $PATH`. Fires on every fresh Claude Code session in an initialized project, since `mxcli init` wires `run --local --setup --ensure-db` into the SessionStart hook", "cause": "THREE independent defects on one path, each sufficient to block it. (1) `service postgresql start` ran unelevated: Debian's /etc/init.d/postgresql runs under `set -e` and calls create_socket_directory FIRST, which chmods /var/run/postgresql — refused for a non-root user, so the script aborts before it looks at a single cluster. (2) The #823 user-owned-cluster fallback was INERT on Debian/Ubuntu: postgresql-common wraps only the CLIENT tools (psql, pg_isready, pg_ctlcluster) into /usr/bin, while initdb/pg_ctl live in /usr/lib/postgresql//bin — so the safety net for a failed service start could never deploy on the platform that needs it most. (3) `resolveSuperuser` used `sudo -n -u postgres psql`, but mcr.microsoft.com/devcontainers/base grants its user sudo to root ONLY (`vscode ALL=(root) NOPASSWD:ALL`, confirmed in devcontainers/features main.sh) — so the target is refused even though the user is effectively an administrator. Plus a diagnostic defect: the 20s readiness timeout discarded the service-manager output the package had already collected", "file": "`cmd/mxcli/docker/ensuredb.go` (`serviceStartAttempts`, `postgresServerBinDir`/`postgresTool`, `superuser.viaRoot`, `withServiceDiag`)", "insight": "mxcli GENERATES the broken environment — `generateDockerfile` emits that exact base image, installs postgresql, and runs as vscode — so this was not user misconfiguration, and fixing it in code (not the template) also repairs projects already scaffolded. Root may target any account, so `sudo -n -- sudo -n -u postgres` reaches postgres under a root-only sudoers policy; try the direct form first and nest only on refusal. Resolve initdb and pg_ctl from the SAME bin directory and rank majors NUMERICALLY — a lexical sort puts \"9\" above \"16\", and a data directory made by one major cannot be started by another. **Measurement trap that cost the most time**: reasoning about which error the user would see is unreliable here — four plausible code paths produce four different messages, and the reported wording was reproducible by none of them on Ubuntu 24.04/PG16. What settled it was building a harness that calls `EnsureDatabase` directly and running it as a real non-root user with the real sudoers rule, then isolating each defect with a one-variable control (widen sudoers to `(ALL)` and nothing else changes → provisioning succeeds; prepend /usr/lib/postgresql/16/bin → the fallback completes). Each of the four fixes was reverted individually and its test re-run: two controls initially failed to COMPILE rather than reproducing the symptom, which proves nothing — they were redone faithfully before being believed", "refs": ["mendixlabs/mxcli#984", "#823"]} -{"area": "cmd/mxcli", "date": "2026-09-13", "symptom": "`build-and-test` fails in CI on `TestSettleSourceReturnsPromptlyForOneChange` \u2014 \"a quiet source took 196.975373ms to settle, want under 100ms\" \u2014 while the SAME tree passes in another run of the same workflow minutes earlier", "cause": "The test bounded elapsed wall-clock time as a multiple of the poll interval (`poll * (sourceSettleWindow + 3)`, 100ms against a nominal 40ms). settleSource waits on `time.After(poll)`, which guarantees AT LEAST the duration and nothing about the upper bound, so a loaded runner blows the budget with no defect present.", "file": "cmd/mxcli/docker/runlocal.go (settleSourceWith, the injected tick), cmd/mxcli/docker/runlocal_settle_test.go", "insight": "The property being guarded was a POLL COUNT, not a duration \u2014 'a quiet source costs one extra poll' \u2014 so the fix is to make polls countable (inject the timer) rather than to widen the budget, which only moves the flake threshold. Diagnosis shortcut worth reusing: the same workflow ran twice on the same tree, once from the push event and once from the pull_request merge commit, and disagreed \u2014 two runs of one tree is direct evidence of nondeterminism and cheaper than reading the test. Two things the controls settled that reasoning did not: (1) the assertions are written in terms of `sourceSettleWindow`, so WIDENING that constant leaves both tests green \u2014 they assert the loop honours whatever window is declared, never the number itself, and the real control is a loop that costs one poll MORE than it declares (both fail). (2) Each tick call must return a freshly-armed channel; returning one shared channel makes the multi-file test HANG rather than miscount, so the re-arm is load-bearing and not a style choice. The seam also made a previously untestable guarantee expressible: the window must be sourceSettleWindow CONSECUTIVE quiet polls, and dropping `quiet = 0` from the change branch was green against every pre-existing test in the file.", "refs": ["ako/mxcli#449"]} -{"area":"cmd/mxcli","date":"2026-09-15","symptom":"Porting cmd/mxcli/docker off sdk/mpr moved two WRITE paths (ensureDemoUsers, applyHarvest) onto the codec backend. A baseline diff of `docker check` showed the project byte-identical across 421 files — which proved nothing, because the run had not written anything.","cause":"docker check's widget-update harvest is a no-op on an already-clean fixture, so an output+filetree diff against a pre-port binary exercises only the READ paths. Coverage then showed ensureDemoUsers at 0.0% — a write path the port touched that no test in the package ran.","file":"cmd/mxcli/docker/build.go","fix":"Added TestEnsureDemoUsers_CreatesAdminWhenNoneExist and _SkipsWhenUsersExist, plus a clearDemoUsers helper that establishes the precondition. Coverage 0.0% -> 76.5%. The read paths keep the baseline-diff evidence; applyHarvest was already at 76.9% via TestRunUpdateWidgets_RestoresV2AfterConversion.","insight":"A byte-identical baseline diff is strong evidence for a READ port and near-worthless for a WRITE port, because the natural control (nothing changed) is also what a no-op produces. The two need different instruments, and the cheap way to tell which you have is `go test -coverprofile` + `go tool cover -func` grepped for the functions you touched: it answers 'did my port's code even run' in one command, where a passing suite does not. Here it separated applyHarvest (76.9%, genuinely exercised including its UpdateRawUnit) from ensureDemoUsers (0.0%) inside the same package, so the gap was specific rather than a general absence of tests. Second trap, hit while fixing it: the shared v2 fixture ALREADY HAS two demo users, so the create-path test skipped and the idempotence test asserted the wrong count. Skipping on an unmet precondition is the #808 shape — set the precondition up instead (RemoveDemoUser in a helper, then assert the helper actually emptied it before proceeding). Third: read back through a FRESH connection, since asserting on the value the writer still holds passes against a write that never reached disk."} -{"area":"cmd/mxcli","date":"2026-09-15","symptom":"Porting the last cmd/mxcli readers off sdk/mpr, cmd_extract_templates.go compiled with a type error (RawType/RawObject are bson.D on sdk/mpr, any on types.RawCustomWidgetType). Casting past it would have compiled — and broken the command at runtime, because FindCustomWidgetType is UNIMPLEMENTED on the codec backend.","cause":"mdl/backend/modelsdk/unimplemented_gen.go carries FindCustomWidgetType; measured at runtime it returns 'FindCustomWidgetType is not implemented on the model engine. This should be unreachable'. cmd_extract_templates.go was calling it through a concrete *mpr.Reader, so it was reachable only by NOT going through the backend.","file":"cmd/mxcli/cmd_extract_templates.go","fix":"Left this one file on sdk/mpr with a comment saying why and what would fix it (implement FindCustomWidgetType on the codec backend), and ported the other five. cmd/mxcli is otherwise clean; importers 13 -> 8.","insight":"The type error was the lucky part. A compile error is the ONLY reason this did not ship as a runtime failure — the cast that silences it is one line, and nothing else would have objected. When a port hits a type mismatch at a backend boundary, check whether the backend method is implemented at all before reconciling the types: `grep -n '' mdl/backend/modelsdk/unimplemented_gen.go` answers it in one command, and a runtime probe (connect read-only, call it, log the error) confirms it in under a minute. Note the direction of the trap: the unimplemented method's own error says 'This should be unreachable', and porting a caller to the backend is precisely what MAKES it reachable — so the #477 census blind spot (callers holding a concrete reader are invisible) cuts both ways. Second, smaller measurement trap in the same slice: a baseline diff of `check --post-migration` showed 50 lines vanishing, which looked like a regression and was not — the FIRST run built and cached a catalog inside the project, so the second run reused it. Two binaries must each get their own fresh copy of the fixture, exactly as for a write port; a command that caches into the project directory makes consecutive runs non-independent even when nothing is being written on purpose."} -{"area": "cmd/mxcli", "date": "2026-09-16", "symptom": "mendixlabs/mxcli#1103: a RETRIEVE with LIMIT inside a .test.mdl block was reported as `mismatched input 'LIMIT' expecting {GROUP_BY, SELECT, HAVING}` — the OQL follow set — on the statement `mxcli syntax microflow.retrieve` prints as its own example. The reporter concluded the test-block path routes microflow statements into the OQL parser.", "cause": "It does not. The generated MxTest.Test_* microflow parses fine (measured end-to-end against a real 11.6.6 project: the flow was created with the LIMIT intact). The message came from `mxcli check`/the LSP being pointed at the .test.mdl file itself, which they parsed as top-level MDL. A test block is a MICROFLOW BODY: DECLARE is not a top-level statement, the parser resyncs, RETRIEVE is a NON-RESERVED keyword so it is swallowed as an identifier, and the leftover `FROM …` starts oqlQueryTerm's FROM-first alternative (mdl/grammar/domains/MDLCatalog.g4), whose follow set is exactly {GROUP_BY, SELECT, HAVING}.", "file": "cmd/mxcli/testrunner/check_source.go", "fix": "testrunner.CheckSource renders each block as the microflow it becomes, padded so every body keeps its SOURCE line numbers (wrapper fragments go on the lines the doc comment and the '/' separator occupied). cmd_check.go and lsp_diagnostics.go translate before parsing, so all downstream rules apply unchanged and no diagnostic needs remapping. .test.mdl files joined `make check-mdl`; `.fail.test.mdl` names one whose annotations are deliberately unusable.", "insight": "Two lessons. First: the reporter's diagnosis was precise, confident and wrong, and the fastest way to find that out was to run the pipeline rather than read it — dumping GenerateTestFlows' output and feeding it to visitor.Build took one throwaway test and settled in seconds what an hour of grepping had not. Their error message was real; the command that produced it was not the one they named. Second, the general shape: a tool that OWNS a file format must not hand that format to a parser for a different one. The VS Code extension binds MDL to `.mdl`, which `.test.mdl` matches, so every test file in the editor was a wall of squiggles — 9 of this repo's 10 test files reported errors, one of them 392, and nobody had noticed because nobody runs `mxcli check` on a test file. When adding a derived file format, check what the EXISTING tooling makes of it; the answer is rarely 'nothing'. Line-preserving padding is what makes the translation honest: render into a slice of the source's own length and place wrapper fragments only on lines the original spent on comments or separators, and a diagnostic's line:col is the author's without a mapping table to drift."} -{"area": "cmd/mxcli", "date": "2026-09-16", "symptom": "mendixlabs/mxcli#1104: `mxcli test --attach` reported only 'build failed: The project cannot be deployed, because it contains errors.' on an injection failure, and afterwards EVERY later run of ANY test file failed the same way until a leftover document was found by hand.", "cause": "Two independent defects. (1) The parsed problems were in hand and discarded: runner_attach.go and LocalApp.Rebuild both built their error with `fmt.Errorf(\"build failed: %s\", build.Message)`, and Message is identical for every failing build. Attribution (build_attribution.go, BuildResult.ErrorSummary) existed but was wired only into the --local BOOT, so --attach and every --watch rebuild lost it. (2) Generated names are positional — MxTest.Test_test_1, _2, … from the test's index in its file — and every test file reuses them, while cleanup dropped only the CURRENT suite's names. A run with fewer tests than the last one therefore left the surplus behind, and under --attach the MxTest module always pre-exists (the dev loop installed it) so the whole-module drop never fires.", "file": "cmd/mxcli/testrunner/cleanup_leftovers.go", "fix": "buildFailure()/resultsForBuildFailure() shared by both runners; cleanup keys on what the project HOLDS (SHOW MICROFLOWS IN MxTest, filtered by the generated prefix) with the suite only as a fallback; reportCleanup names every surviving document and prints its DROP.", "insight": "Measured, not reasoned: planted one bad MxTest.Test_test_2 in a project, ran a known-good one-test suite, and watched it fail and leave the leftover in place — then after the fix watched run 1 fail and CLEAN, and run 2 pass. A self-healing sequence is the control that 'cleanup works' cannot be argued into. The general rule for generated artefacts: derive what to remove from what EXISTS, never from what you intended to create. Keying on the suite was wrong in both directions at once — it missed leftovers AND issued DROPs for flows a part-way injection never created, and those failures made cleanup report 'the project has been left modified' for a project it had just cleaned, which is a false alarm that sends the reader hunting for damage. Positional names (index-in-file) guarantee collisions across files and are worth avoiding, but as long as they exist the prefix is the only safe key. Also worth pinning: once BuildFailedError.Error() renders the errors, a hint that repeats them prints everything twice — assert on the message the READER sees, not on the hint in isolation."} -{"area": "cmd/mxcli", "date": "2026-09-17", "symptom": "On macOS with the matching Studio Pro installed, `run --local` and `test --local` fail in the build step: either `Project version '11.12.2' does not exactly match MxBuild version '11.14.0'` or `mxbuild at ... is a linux binary and cannot run on darwin`. `mxcli setup mxbuild` prints Studio Pro's path and changes nothing, so the remedy every error suggests is a loop", "cause": "Two resolvers answered 'which mxbuild', and the wrong one won. The local entry points call `ResolveMxBuildForLocal` (#916) and then pass the result ONLY to the web-client bundler; both `ServeOptions` literals omitted `MxBuildPath`, so `StartServe` re-resolved from `~/.mxcli/mxbuild/` and, on a miss, took `AnyCachedMxBuildPath()` \u2014 the NEWEST cached version, unrelated to the project's. `test --local` was worse: it never resolved at all, still calling `DownloadMxBuild` directly, the exact call #916 removed from `runlocal.go` and left in `localapp.go`", "file": "`cmd/mxcli/docker/mxserve.go` (new: `serveOptionsFor`, `resolveServeMxBuild`, `noServeMxBuildError`) + `runlocal.go` + `localapp.go` (the two ServeOptions literals)", "insight": "The half-populated cache directory that defeats `CachedMxBuildPath` is **mxcli's own work, two steps earlier in the same command**: `ensureMxBuildRuntimeSibling` does `MkdirAll` on `~/.mxcli/mxbuild//` to plant the runtime symlink, and on macOS nothing ever puts a `modeler/` beside it because mxbuild comes from Studio Pro. So this is not a user with a damaged cache \u2014 it is the guaranteed state for every macOS project whose version was not already downloaded, which is why it reads as 'run --local has never worked here'. Repro without a Mac: plant a cache entry for one version and call `StartServe` asking for another. **The control matters more than usual**: asserting only that the call FAILS proves nothing, because the old code also failed \u2014 one exec later, with `exec format error` \u2014 so the assertion has to be that the refusal names the missing version and happens before any exec. A first draft of the modeler-less test passed against unfixed code for exactly that reason. Fix shape, per docs-wiki/bug-patterns/duplicate-resolver-drift.md: remove the second answer rather than synchronise them \u2014 one `serveOptionsFor` both call sites go through, and a resolver that refuses to substitute a near-miss version instead of handing mxbuild a binary it will reject minutes later, after a cold model load. Issues #1122, #1124", "refs": ["#1122", "#1124", "#916"]} -{"area": "cmd/mxcli", "date": "2026-09-17", "symptom": "`mxcli run --local --mxbuild-path /x -p app.mpr` answers `Error: unknown flag: --mxbuild-path`, while the shipped run-local skill, runlocal.go's own comment and two resolution error messages all tell the user to pass it", "cause": "The flag was never registered on `runCmd`; it exists only on the four `docker` subcommands. Everything BEHIND it was already wired \u2014 `LocalRunOptions.MxBuildPath` is declared and `ResolveMxBuildForLocal` honours it \u2014 so the gap was one missing `Flags().String` and one missing field assignment, invisible to every test because nothing exercised the command's flag set", "file": "`cmd/mxcli/cmd_run.go` (flag registration + LocalRunOptions.MxBuildPath) + `.claude/skills/mendix/run-local/SKILL.md` + `docs-site/src/tools/run-local.md`", "insight": "The interesting defect is not the missing flag, it is that **the error messages recommending it were the only documentation of it** \u2014 guidance naming an option the command does not accept is worse than no guidance, because it reads to the user as their own mistake, and on macOS it was the only advertised way out of a platform mismatch. The regression test to write is therefore not 'the flag exists' but the invariant: scan the resolution sources for `--flag` strings they tell users to pass, and assert `run` registers each one (`TestErrorGuidanceNamesAFlagThatExists`). Watch the parse test \u2014 `-p` is PERSISTENT on rootCmd, so `runCmd.Flags().Parse` rejects it with `unknown shorthand flag: 'p'` and the test fails for a reason unrelated to the fix; resolve through `rootCmd.Find` to reproduce the reporter's line honestly. Issue #1125", "refs": ["#1125", "#916", "#1122"]} -{"area": "cmd/mxcli", "date": "2026-09-17", "symptom": "`mxcli run --local` on a project with Settings > Web UI > OptimizedClient = No exits 1 after the cold build with `no rollup.config.mjs and no bundle at .../web/dist/index.js ... the build did not produce a client` \u2014 about a deployment whose client is sitting in the same directory. `mxcli docker run` works", "cause": "The client gate tested for exactly two shapes (a rollup config to run, or a bundle mxbuild already wrote) and treated everything else as a failed build. A classic (Dojo) deployment is a legitimate third shape with NEITHER: there is no bundling step for the classic client in any Mendix version. Nothing in the local loop read UseOptimizedClient, so the mode was invisible", "file": "`cmd/mxcli/docker/webclient_plan.go` (new: `planWebClient`, `isClassicWebClient`, `noWebClientError`) + `webclient.go` (`BuildWebClient`, `ensureWebClientBundle`) + `webclient_watch.go` (`StartWebClientWatch`) + `runlocal.go` (`ensureClientServed`)", "insight": "Measured on one blank 11.12.2 app built twice, changing only the setting: **mxbuild swaps which client lands in `deployment/web/` and parks the other beside it** \u2014 OptimizedClient=Yes gives `web/` the React client (+`rollup.config.mjs`) and parks Dojo in `dojo-web/`; =No gives `web/` the Dojo client (`index.html` loading `mxclientsystem/mxui/mxui.js`, no rollup config, no dist) and parks React in `react-web/` WITH its own rollup config. So detect from the DEPLOYMENT, not from the model's setting: the deployment is what gets served, the two disagree exactly when the setting has just changed, it needs no plumbing through the five call sites, and it covers MigrationMode without predicting what that mode emits. Detect on POSITIVE evidence (the entry point names its client) \u2014 inferring classic from the absence of the React shapes would make every genuinely broken deployment look classic and silently skip the bundle, which is the black screen the gate exists to prevent; keep a control test that a clientless deployment still fails. **The gate had FIVE consumers, not one**: boot, the `--watch` bundler, the post-boot re-bundle guard, and `ensureClientServed`, which probes that `/dist/index.js` is *served* \u2014 measured 404 on a classic app, so fixing only the boot moves the failure to every applied change under `--watch`. Two of them carried hand-copied duplicates of the same gate and had already drifted once (the 11.14 fix, ako/mxcli-ledger #146, landed on `BuildWebClient` only, so `run --local` worked on 11.14 and `run --local --watch` did not) \u2014 so the fix collapses them into one `planWebClient` rather than adding a third copy. Repro from Linux with no Mac and no Studio Pro: `mxcli new` an 11.12.2 app, flip `UseOptimizedClient` to `No` on `Forms$WebUIProjectSettingsPart` (note the `Forms$` prefix, not `Settings$`), build, and keep the unflipped copy as the control. Patching that BSON with a Go `map` corrupts the file \u2014 mxbuild refuses it with `Expected '$ID' as the first property of a storage object` \u2014 because map iteration loses key order; use `bson.D` throughout. Verified in a browser, not just at the gate: `mx` global present, real page content, zero console errors. Issue #1123", "refs": ["#1123", "ako/mxcli-ledger#146"]} -{"area": "cmd-mxcli", "date": "2026-09-17", "symptom": "`mxcli new --version 10.24.25` (and `mxcli setup mxbuild --version 10.24.25`) dies with `HTTP 404 from https://cdn.mendix.com/runtime/mxbuild-10.24.25.tar.gz`. Every 9.x and 10.x version probed 404s while 11.6.0/11.12.1/11.13.0 return 200 from the same host and path, which reads as 'Mendix 10 is no longer on the CDN'.", "cause": "Mendix 9 and 10 publish FOUR-part artifact names carrying a build number the release notes never mention: the release called 10.24.25 is `mxbuild-10.24.25.122571.tar.gz`. Mendix 11 publishes three parts. `MxBuildCDNURL` interpolates whatever string it is handed and nothing resolved a partial version, so a hand-typed 10.x version named no artifact at all. Project-driven paths were never affected — the MPR's `_ProductVersion` already carries all four parts (`10.24.25.122571`) and `parseVersion` takes the first three for major/minor/patch while the full string goes to the URL.", "file": "`cmd/mxcli/docker/version_resolve.go` (ResolveCDNVersion, highestBuild, CDNReleasesFor); wired at the two entry points where a user types a version, `cmd/mxcli/cmd_new.go` and `cmd/mxcli/setup.go`. Tests `cmd/mxcli/docker/version_resolve_test.go`.", "insight": "**A uniform 404 across a whole major version is evidence about the NAME, not about availability.** The conclusion drawn from it — 'Mendix 10 cannot be downloaded here' — blocked a verification for an entire session, and the fix was one listing call: the CDN is an S3 bucket that answers ListObjectsV2 (`?list-type=2&prefix=runtime/mxbuild-10.24.`), so what exists is enumerable rather than guessable. When a probe fails identically for every input in a class, question the query before concluding the class is empty. Three traps in the resolution itself, each a test: the `.sha256` sidecar beside every archive must not be picked as an artifact; the prefix needs its trailing dot or `10.24.2` swallows `10.24.20`..`10.24.26`; and build numbers are not zero-padded, so a text sort puts 99999 above 122571 and 10.24.9 above 10.24.26. Resolve at the entry point and thread the RESOLVED string onward — `mxcli new` checks the created project's stamp against the requested version, and `mx create-project` stamps four parts, so resolving late would fail that postcondition.", "refs": ["#1121"]} -{"area": "cmd/mxcli", "date": "2026-09-18", "symptom": "mendixlabs/mxcli#1025: `mxcli syntax` advertises `mxcli syntax workflow user-task targeting` in its own help and answers `Unknown topic: workflow user-task targeting`. Same for `workflow user-task` and `workflow parallel-split`, all of which `mxcli syntax workflow` lists as sub-topics; `--json` was the only route that reached them.", "cause": "The CLI built its path with `strings.Join(args, \".\")` and never split an argument, so a topic handed over as ONE string — a quoted copy-paste, a tool wrapper, `sh -c` — became the path `workflow user-task targeting`, which matches nothing. The REPL's `help` had resolved multi-word topics since it was written (`resolveHelpPath`, greedy hyphen-joining): one question, two answers, and the CLI held the weaker copy. The #955 segment-match fallback could not save it either — it passed the DOTTED path to `BySegmentMatch`, and no segment contains a '.', so that fallback was silently dead for every multi-word query.", "file": "cmd/mxcli/syntax/topic.go (new: Lookup, topicWords, resolvePath), cmd/mxcli/help.go, mdl/executor/cmd_misc.go (resolveHelpPath deleted), mdl/grammar/domains/MDLSettings.g4 (helpStatement, helpTopicWord), mdl/visitor/visitor_query.go (ExitHelpStatement); tests cmd/mxcli/cmd_syntax_test.go, cmd/mxcli/syntax/topic_test.go, mdl/executor/cmd_misc_test.go, mdl/visitor/visitor_help_topic_test.go; example mdl-examples/bug-tests/syntax-1025-topic-drilldown.mdl", "insight": "**The spaces in the reported error message were the whole diagnosis, and reading them as a paraphrase cost an hour.** The command prints the path it built, and the CLI joins on '.', so `Unknown topic: workflow user-task targeting` cannot come from the command as documented — it can only come from the topic arriving as a single argument. Every line of the report follows from that and nothing else does: `syntax workflow` works (one word), `--json` works (the flag is not part of the topic), the three multi-word forms fail. Take a quoted error message literally, character for character, before assuming the reporter retyped it. **The reported version is downloadable and settles it in one run**: `mxcli setup mxcli`'s own URL shape (`releases/download//mxcli-linux-amd64`, NOT the goreleaser `_Linux_x86_64.tar.gz` that 404s) fetched v0.20.0, where the unquoted command works and the quoted one reproduces the message verbatim — so 'fixed since' and 'never broken' were both wrong. **The guard that matters is not the three cases from the report** but `TestEveryRegisteredPathIsReachableBySpelling`: every registered path, tried dotted, as separate arguments, and as one string. The registry prints dotted paths and then tells the reader to drill down with words, so a spelling that does not resolve is the command contradicting its own output; a per-case test would have passed the day someone added a topic with a new shape. Control: stub the whitespace split in `topicWords` and it fails with the reported path, spaces and all. **The grammar half has a trap the CLI half does not, and only the EXISTING suite caught it.** `helpStatement: IDENTIFIER (identifierOrKeyword)*` is the grammar's catch-all — a statement that is just an identifier and some words — so whatever it can swallow, it swallows from the statement that should have had it. Widening it to `(DOT? helpTopicWord)*` to take `help workflow.user-task` made `Sec.ApiUser` a complete statement of its own, and `create module role Sec.ApiUser` then parsed, WITH NO PARSE ERROR, as CREATE MODULE (named \"role\") followed by a help topic — two statements, wrong types, six unrelated security tests red. `(helpTopicWord (DOT? helpTopicWord)*)?` — a topic word before any dot — leaves `.ApiUser` unconsumable and restores the old disambiguation. Bisect a grammar regression by SHAPE, not by reading the ATN: adding the unused rule alone was clean, the hyphen alone was clean, the leading optional DOT was the whole of it, and three regenerations said so in about a minute. **When widening a permissive rule, the test to add is not for the new spelling but for what the rule must still NOT swallow** (TestHelpRuleDoesNotSwallowATrailingQualifiedName).", "refs": ["mendixlabs/mxcli#1025", "#955"]} -{"area": "cmd/mxcli", "date": "2026-09-18", "symptom": "`mxcli report` scores a project against rules the team disabled in `lint-config.yaml`. `mxcli lint` honours the config, the report's SCORE does not move, so the score cannot be calibrated at all. Reported at 66/100 against a 99/100 blank-app baseline, where 61 of 86 findings were two deliberately-accepted rules", "cause": "`cmd_report.go` never called `linter.FindConfigFile`/`LoadConfig` — it went straight from `linter.New` to `BuildReport`. Separately it carried its own INLINE copy of the built-in rule list, one rule behind `builtinLintRules()` (missing MDL-FLOW01), so the two commands scored one project against two rule sets. One root cause: report re-implemented lint's setup instead of sharing it", "file": "`cmd/mxcli/cmd_report.go`, `cmd/mxcli/cmd_lint.go`, new `cmd/mxcli/lint_setup.go` (`projectLintRules`, `applyLintConfig`), `mdl/linter/linter.go` (`RuleEnabled`)", "insight": "Same class as #904 in the opposite direction: there a silently reduced rule set made the score falsely HIGH, here an unread config makes it falsely LOW — and both are invisible because a score carries no provenance. **A value test cannot guard the inline copy**: both commands build rules inside a cobra RunE, so nothing a unit test can call notices a second list being re-added. The guard is therefore structural — grep `cmd_report.go` for `lint.AddRule(rules.New` — with a POSITIVE CONTROL first (assert `builtinLintRules` still constructs rules) so it cannot pass vacuously, the same shape as `scripts/check-tunnel-deps.sh`. Take the LintContext out of `applyLintConfig`'s signature: `NewLintContext(nil, nil)` panics, and a nil-guard added only to make a test compile is how a helper acquires behaviour nothing needs", "refs": ["#525", "#904"]} -{"area": "cmd/mxcli/marketplace", "date": "2026-09-20", "symptom": "`mxcli marketplace install ... -p app.mpr` (project named by a RELATIVE path) fails with `install the package's bundled files: package entry \"manifest.json\" would write outside the project` \u2014 after the module has already been transplanted into the model. `SHOW MODULES` lists the module, `mx check` is clean, but no bundled file (themesource/, widgets/) landed and the command exited 1. Absolute `-p` paths work.", "cause": "`InstallPackageFiles` builds `dst := filepath.Join(projectDir, clean)` and then checks `strings.HasPrefix(filepath.Clean(dst), filepath.Clean(projectDir)+os.PathSeparator)`. With projectDir == \".\" (from `filepath.Dir(\"app.mpr\")`), Join drops the dot, so dst is `manifest.json` and the prefix is `./` \u2014 every legitimate entry fails the zip-slip guard. The guard was checking the joined path (the shape CodeQL recognises) but never anchored the project directory first.", "file": "`cmd/mxcli/marketplace/update.go` (`InstallPackageFiles`: `filepath.Abs(projectDir)` before the loop), test `cmd/mxcli/marketplace/install_relative_dir_test.go`", "insight": "A containment guard has two inputs and both must be canonical \u2014 the entry AND the root. The traversal tests only ever passed an absolute t.TempDir(), so the root was canonical by accident and the relative case had no coverage; the first real CLI invocation with `-p app.mpr` hit it. Worse, the transplant runs BEFORE the file step, so the failure lands on a half-installed module: the model has it, the disk does not, and the exit code says failure. Order the steps so the cheap, reversible file copy can be validated before the model write, or at least say in the error that the model was already changed. Prove-by-revert done: the new test fails on the unpatched function with the exact reported message.", "refs": []} -{"area": "cmd/mxcli", "date": "2026-09-22", "symptom": "ARCH002 and ARCH003 never report anything. `mxcli lint` runs them, prints no error and lists them under --list-rules, yet a project whose entities have no business key and no data-change microflow comes back clean.", "cause": "Both rules guard with `if entity.entity_type != \"PERSISTENT\": continue`. The catalog column stores PERSISTENT, but LintContext.Entities normalizes it (CASE WHEN 'PERSISTENT' THEN 'Persistent') before Starlark sees it, so the guard is true for every entity and the body never runs. The write-lint-rules skill documented a third spelling again, \"persistent\", in both its field table and its worked example.", "file": ".claude/lint-rules/entity_business_key.star, .claude/lint-rules/data_change_microflows.star, mdl/linter/context.go", "insight": "A Starlark rule that compares a field to a value that never occurs fails open: no error, no output, and --list-rules still shows it, so it looks like a project with nothing to report. The literals a rule may compare against are worth asserting in a test that loads the shipped rule file itself; measured on a generated app, fixing one spelling took ARCH003 from 0 findings to 5.", "refs": ""} -{"area": "cmd/mxcli/docker", "date": "2026-09-22", "symptom": "`windows-process-regression` fails intermittently on TestKillProcessGroup_ReapsGrandchildAndUnblocksWait: `cmd.Wait() did not return after killProcessGroup`, ~20.5s (the select deadline), and the GitHub runner then logs `Terminate orphan process: pid (NNNN) (PING)`. killProcessGroup reports no error. Reruns pass, so it reads as 'Windows CI is flaky' and gets attributed to whatever PR happened to be red.", "cause": "The test's readiness marker named the wrong process. The `spawn` helper mode started `cmd /c ping -n 60 127.0.0.1` and wrote `grandchild-started` immediately after `gc.Start()` returned — but Start() only guarantees `cmd.exe` was CREATED; `ping.exe`, which is what ends up holding the inherited stdout pipe, does not exist yet. The test raced ahead to killProcessGroup, `taskkill /F /T` enumerated a tree `ping.exe` had not joined, returned 0, and ping survived holding the write end, so cmd.Wait() never saw EOF.", "file": "`cmd/mxcli/docker/procgroup_windows_test.go` (helper gains a `grandchild` mode that announces ITSELF, with its pid, over the inherited pipe; the test then asserts processAlive on that pid before killing), parser split to `cmd/mxcli/docker/procgroup_marker_test.go` + TestGrandchildPID", "insight": "A readiness marker is only worth what it proves about the process the test is ABOUT. Emitting it from the parent after Start() proves the parent reached a line of code, which is the one thing never in doubt. Emit it from the process under test, over the channel under test — the unix half already did exactly this (`sh -c 'sleep 60 & echo $!; wait'` plus kill(gpid,0)) and the Windows half had silently diverged, so the fix was porting the sibling's handshake rather than inventing one. Two second-order traps: a deadline bump cannot fix this (the grandchild is never killed, so no amount of waiting helps) and would have buried it; and the marker parser must reject a line not yet terminated by \\n, since a truncated pid parses as a plausible different pid. The same-commit control that settled blame: sha 0710968d ran the identical workflow twice, `push` (35715042749) green and `pull_request` (35715076433) red — when a job is suspected flaky, look for two runs of one commit before reading the diff.", "refs": ["ako/mxcli#594", "ako/mxcli#597", "ako/mxcli#601"]} -{"area": "cmd/mxcli/diag", "date": "2026-09-22", "symptom": "`mxcli diag loop-report --json` reports `\"failed\": 0` across a real 442-invocation log while runs were genuinely exiting non-zero. Read next to `\"unclosed\": 10` in the same object it says 'nothing failed', which is the opposite of the truth. The text report never printed the field at all, so the misreading was reachable only through --json, where no surrounding prose corrects it.", "cause": "The field counted a different population than its name claimed. `rep.Failed++` fires only for an invocation that CLOSED (wrote session_end) whose summary carried errors_count > 0, and errors_count is diaglog's STATEMENT-level counter \u2014 so the only path reaching it is a run that kept going after a failed statement, i.e. `exec --continue-on-error`. A run that actually fails exits through os.Exit, which skips the deferred Close() and PersistentPostRun, writes no session_end, and lands in `unclosed`. The two populations are disjoint by construction and `failed` is near-always zero. Nothing was broken; the name was.", "file": "`cmd/mxcli/diag_loop_report.go` (`loopReport.Failed` -> `StatementErrors`, json tag `failed` -> `runs_with_statement_errors`; renderLoopReport prints it only when non-zero and takes io.Writer so the text output is testable), tests `cmd/mxcli/diag_loop_report_test.go` (TestStatementErrorsIsDisjointFromUnclosed, TestJSONKeyNamesWhatItMeasures, TestStatementErrorLineIsPrintedOnlyWhenNonZero)", "insight": "A metric that is always zero fails silently in the one direction nobody checks: it is indistinguishable from good news, so it is never investigated. This one survived review and a whole feature PR because every local test run genuinely had no --continue-on-error invocations, so 0 was CORRECT in the test set and wrong in the field \u2014 only a 442-invocation log from a real project surfaced it. Two rules fell out. (1) Assert the JSON key literally, not the Go field: the key is the interface the wrong conclusion was drawn through, and a Go-side rename leaves the tag behind. (2) Never print a counter's zero beside a related non-zero counter; suppress it, or the pair reads as a comparison. The larger fix \u2014 making `failed` mean failed \u2014 needs an exit-code path through ~250 os.Exit sites in cmd/mxcli, since Go has no atexit; `unclosed` already carries that signal and the report explains it. Prove-by-revert done both ways: restoring the tag fails the key test with `\"failed\":2` in the payload, relaxing the guard to >= 0 prints `Finished with failed statements: 0` directly under `Did not close: 2`, which is the reported symptom exactly.", "refs": ["ako/mxcli#617", "ako/mxcli#620"]} -{"area": "cmd/mxcli/check", "date": "2026-09-22", "symptom": "`make check-mdl` Error 1 in CI right after the #618 empty-script guard landed. The failing fixture, mdl-examples/doctype-tests/15-fragment-examples.test.mdl, got: 'produced no statements, but it is not empty. The parser could not begin reading it. First line that did not parse: create module FragTest;' \u2014 for a 416-line file that parses perfectly well (18 statements) when copied to a plain .mdl name. The filename was the whole difference.", "cause": "Two defects stacked. (1) MINE: cmd_check.go renders a .test.mdl through testrunner.CheckSource \u2014 a test block is a microflow BODY, so check parses the RENDERING, not the file \u2014 but the #618 guard was given `string(content)`, the file as read. A file with no @test block renders to nothing, so the guard saw zero statements against non-empty ORIGINAL text and quoted a source line the parser had never been handed. (2) PRE-EXISTING: that fixture declares no @test at all. It is a syntax demo misnamed .test.mdl (its sibling 15b-fragment-slots-examples.mdl is plain), so CheckSource rendered it to nothing, check printed 'Check passed!' on zero statements, and `make check-mdl` had been reporting PASS over 416 lines nothing ever read \u2014 concealing a real MDL-PAGE20 violation (page param $Customer, url with no {Customer} segment).", "file": "`cmd/mxcli/cmd_check.go` (guard takes `source`, the parsed text, not `content`; empty rendering from non-empty content gets its own branch), `cmd/mxcli/empty_script.go` (`noTestsDeclaredError`), fixture renamed to `mdl-examples/doctype-tests/15-fragment-examples.mdl` with the url fixed, tests `cmd/mxcli/empty_script_test.go`", "insight": "A guard that reports on text OTHER than what the parser consumed will eventually quote a line the parser never saw, and it reads as authoritative precisely because it names a line. Wherever a command transforms its input before parsing \u2014 a renderer, a preprocessor, a macro pass \u2014 every diagnostic downstream must be fed the transformed text, or it describes a file that was never compiled. The wider lesson is about what the guard FOUND: #618 is 'a silent no-op is the worst outcome', and the repo's own check-mdl suite contained an instance \u2014 a file passing because nothing read it. A suite that reports PASS per file cannot distinguish 'checked and clean' from 'not checked'; the tell was available all along in the statement count, which was 0. When a new guard fails CI, check whether it found a second instance of its own bug before assuming it is a false positive: here it was BOTH, and only fixing the diagnosis would have left the misnamed fixture green and unread. Prove-by-revert done end-to-end: restoring `string(content)` reproduces the CI message verbatim on the same bytes under the .test.mdl name.", "refs": ["ako/mxcli#618", "ako/mxcli#619", "ako/mxcli#1103"]} -{"area":"cmd/mxcli","date":"2026-09-23","symptom":"`mxcli test`, `mxcli check`, `mxcli exec` with no arguments wrote 0 session_start records (`mxcli check /nonexistent.mdl` wrote 1), so a run that failed cobra's Args validation was invisible to `diag loop-report`.","cause":"#617 put diaglog.Init in the root's PersistentPreRun and described that as 'before argument validation'. Cobra's execute() runs ValidateArgs, and answers --help/--version, BEFORE any hook, so arity failures returned before the session opened. The same move also meant the root's -c and REPL sessions were recorded as mode \"mxcli\" instead of \"batch\"/\"repl\", because PreRun reached the singleton first.","file":"cmd/mxcli/session_start.go","insight":"A cobra hook is not 'before anything': execute() order is ParseFlags → help/version → ValidateArgs → PersistentPreRun, so anything that must see every invocation belongs in main() before Execute, keyed on rootCmd.Find(os.Args[1:]), which needs no parsed command. Moving the open that early moves the close problem with it: --help/--version return nil without running PersistentPostRun, so a close left there turns every help lookup into an 'unclosed' (failed) run. That was the false failure #617 had already paid for once. Close in main() after a nil Execute instead. When a singleton is opened earlier, whichever caller reaches it first picks the mode, so resolve the mode the later callers would have passed (batch/repl) at the early call site. The regression test runs the real main() in a re-executed test binary with MXCLI_LOG_DIR. It is the only layer that sees os.Exit paths, and both controls fail at the right place: stubbing startSession gives 0/0/0 records with the /nonexistent.mdl control still passing, and moving the close back to PostRun gives 0 session_end records for --help/--version.","refs":["ako/mxcli#633","ako/mxcli#617"]} -{"area": "cmd/mxcli/diag", "date": "2026-09-23", "symptom": "`mxcli diag loop-report` showed all 5 `test` runs as 'did not close' although every test passed, and inflated the `-c` (111) and `exec` (180) counts in the same log. Reported as 'the command apparently skips the summary record on success too' \u2014 which is not what happens: `test` returns normally, PersistentPostRun fires, and the session_end IS written.", "cause": "mxcli runs mxcli. Measured from a real `mxcli test` with MXCLI_LOG_DIR pointed at a scratch dir: one parent session_start (pid 1071) followed by THREE child session_starts \u2014 `-c DESCRIBE SETTINGS`, `-c SHOW MODULES`, and an `exec` of the generated runner \u2014 before a single test executes. `new`, `eval`, `tui` and the LSP self-spawn the same way (six os.Executable() sites). buildInvocations segmented on 'next session_end OR next session_start, whichever comes first', so a child's start closed the parent's invocation and the parent's own end landed on whatever was open by then. session_end carried no pid, so pairing by process was impossible.", "file": "`mdl/diaglog/diaglog.go` (pid on session_end; parentPIDEnv marker set once in Init and inherited by every child), `cmd/mxcli/diag_loop_report.go` (buildInvocations pairs by pid with the positional rule as fallback; spawned runs excluded from the table and wall time, counted on their own line), tests `cmd/mxcli/diag_loop_report_test.go`", "insight": "The segmentation rule documented itself as exact 'for sequential invocations, which is what an agent loop produces' \u2014 and the thing that breaks that assumption is the tool itself, not concurrency by the user. When a tool can invoke itself, EVERY per-process measurement over it needs a parent link, not just a pid: a pid alone fixes the pairing but still counts three phantom agent calls per test run. The marker belongs on the ENVIRONMENT, not at each spawn site: exec.Command inherits the parent's environment (explicitly via os.Environ(), implicitly when Cmd.Env is nil), so one os.Setenv in Init covers all six self-spawn sites and any added later \u2014 six edits that would each have to be remembered become zero. Second-order trap: spawned runs must be excluded from WALL TIME too, not just the count, because a child's seconds are already inside its parent's; the test asserts 10s for a parent with three 1s children, and the reverted code says 3. Prove-by-revert done on the measured record shape: the positional rule gives Invocations=4 (want 1), Unclosed=1 for a parent whose tests all passed, Wall=3 (want 10).", "refs": ["ako/mxcli#617", "ako/mxcli#629"]} -{"area": "cmd/mxcli/syntax", "date": "2026-09-23", "symptom": "`mxcli syntax page datasource` documented `DataSource: MICROFLOW Module.MF($P)`. That form is a parse error: `dataview dv (datasource: microflow M.DS_X($State))` gives 'line 2:15 no viable alternative at input datasource'. Only the NAMED form `M.DS_X(State: $State)` parses. Hit in a real build, diagnosed from the error rather than the doc.", "cause": "The syntax entry was written from the intended shape rather than from something that had been run through the parser. Nothing checks it: the Syntax and Example fields are free text.", "file": "`cmd/mxcli/syntax/features_page.go` (page.datasource entry now shows `MICROFLOW Module.MF(Param: $P)` and states that the positional form is a parse error)", "insight": "CLAUDE.md deliberately points at `mxcli syntax` instead of restating syntax, so that it cannot go stale \u2014 which makes a wrong entry there worse than a wrong entry in prose, because it is the thing consulted INSTEAD of checking. The cost lands in the agent loop: read it, write it, fail to parse, diagnose, retry. Measured before reaching for the systemic guard: 42 of 164 syntax examples fail `mxcli check` today, but the large majority are fragments by design (a microflow body like `IF \u2026`, a widget snippet like `DATAGRID \u2026`, an OQL fragment) and are legitimately not standalone top-level MDL \u2014 so a blanket 'every example must parse' test would be mostly noise, and making it useful needs a way to mark which examples are standalone. Measuring that first is what stopped a plausible-sounding guard from being built wrong.", "refs": ["ako/mxcli#630"]} -{"area": "cmd/mxcli/test", "date": "2026-09-23", "symptom": "Windows: `mxcli test tests/ -p MyApp.mpr --local` fails with `local runtime: starting mxbuild serve: mxbuild --serve did not become ready` after caching a Linux ELF in %USERPROFILE%\\.mxcli\\mxbuild, and there is \"no flag, environment variable, or mechanism to redirect mxcli to the Windows mxbuild.exe already present in the Studio Pro installation\"", "cause": "Two layers. The platform part (downloading/exec'ing the Linux binary, serving from the cache instead of the resolved binary) was already fixed by #916 and #1122, both after the reporter's v0.21.0. What remained on main: `test` never registered `--mxbuild-path` — `run` gained it in #1125, but `test --local` boots through the same `ResolveMxBuildForLocal` and prints the same 'pass --mxbuild-path' guidance while answering `unknown flag`. `RunOptions` had no field and `localAppOptions` never set `LocalAppOptions.MxBuildPath`, though StartLocalApp honoured it. No env override existed anywhere", "file": "`cmd/mxcli/main.go` + `cmd_test_run.go` (flag), `testrunner/runner.go` + `localapp_options.go` (plumbing), `docker/mxbuild_platform.go` (`MxBuildPathEnv`, read in `resolveMxBuildForLocalOn` after the flag)", "insight": "**The guard for #1125 asserted its invariant against one command** — `TestErrorGuidanceNamesAFlagThatExists` checked only `runCmd`, while the guidance it polices is emitted by a resolver two commands share. When a test pins 'the advertised flag exists', enumerate the callers of the code that ADVERTISES it, not the command the report named; it now iterates `run` and `test`. **Before fixing a platform report, date it against the fixes**: the reporter's first two suggestions ('download platform-correct binary', 'auto-discover Studio Pro') were already on main, and re-implementing them would have been churn — only the override was missing. Put the env var in the resolver, not the CLI, so `run --local` and `test --local` both get it from one line; a flag-level env read would have been one more per-command copy to drift. Control: the env test runs as goos=windows with an unmatched version, so without the override it fails fast with the 'Linux binary cannot run natively on windows' refusal instead of hitting the CDN; removing only the `MxBuildPath:` line in localAppOptions fails the plumbing test for both runners. **Unverified**: no Windows host; code-level with OS-injected tests", "refs": ["mendixlabs/mxcli#1086", "mendixlabs/mxcli#1125", "mendixlabs/mxcli#916", "mendixlabs/mxcli#1122"]} -{"area": "cmd/mxcli", "date": "2026-09-25", "symptom": "Shipped Starlark rules CONV009, CONV010, QUAL001, QUAL003, QUAL004 and CUSTOM002 report nanoflows and rules as microflows: \"Microflow 'NF_Foo' has 30 activities\", and documentType \"Microflow\" in the JSON and report output.", "cause": "microflows() yields all three flow flavours (one catalog table, MicroflowType MICROFLOW/NANOFLOW/RULE), and each rule hardcoded document_type=\"Microflow\" and a \"Microflow '...'\" message. The Go rules had already been fixed with Microflow.DocumentNoun() after MPR002 called a rule a microflow, but that method was never exposed to Starlark, so every Starlark rule had to re-derive it and none did.", "file": "mdl/linter/starlark.go, .claude/lint-rules/{conv009_max_microflow_objects,conv010_act_microflow_content,example_microflow,long_microflows,mccabe_complexity,orphaned_elements}.star, mdl/linter/starlark_flow_noun_test.go", "insight": "When a Go-side fix lives in a method, check whether the Starlark projection can reach it; a fix Starlark cannot call is a fix for half the rules. Exposed as document_noun / document_noun_title on the microflow struct, and the test runs every shipped rule over a fixture with one flow per flavour, requiring a finding on each (else vacuous) plus a guard that any new `for x in microflows()` rule joins the list. Skip the wrong turn of excluding nanoflows/rules from QUAL004: rule calls from decisions ARE emitted as 'call' refs (builder_references.go collectRuleCalls), so only the label was wrong.", "refs": "mendixlabs/mxcli#1178"} -{"area": "cmd/mxcli", "date": "2026-09-25", "symptom": "Starlark rule API docs name microflow_type values the linter never returns: the write-lint-rules skill said \"microflow\"/\"nanoflow\", example_microflow.star (copied into every project by mxcli init) said \"Microflow\"/\"Nanoflow\", only mccabe_complexity.star had \"MICROFLOW\"/\"NANOFLOW\", and none listed \"RULE\". The skill's entity table also omitted has_created_date/has_changed_date/has_owner/has_changed_by.", "cause": "LintContext.Microflows() passes the catalog's MicroflowType through raw (MICROFLOW/NANOFLOW/RULE) while Entities() CASE-normalizes EntityType to TitleCase, so the two adjacent iterators have opposite conventions and each doc author guessed. Nothing tied the documented literals to what the API emits, so two passes over the same skill file (59db6e7b, #1165) fixed instances and left this one.", "file": ".claude/skills/mendix/write-lint-rules/SKILL.md, .claude/lint-rules/example_microflow.star, .claude/lint-rules/mccabe_complexity.star, mdl/linter/starlark_documented_values_test.go", "insight": "Fix the class, not the row: observe the emitted field names and enum values by running a Starlark rule (dir(e), e.entity_type) over a fixture holding every stored kind, then assert the skill tables, the shipped rules' `# .field - ...` headers, and every `.entity_type/.microflow_type ==` comparison in shipped rules against that observed set. Equality, not subset, for the docs -- an undocumented value (RULE) is a flavour a rule silently mistreats. Normalizing MicroflowType instead would have broken every user rule already comparing \"MICROFLOW\" correctly.", "refs": "mendixlabs/mxcli#1178, mendixlabs/mxcli#1164"} -{"area": "cmd/mxcli", "date": "2026-09-26", "symptom": "The write-lint-rules skill, which Starlark rules are written from, omitted seven query functions (java_actions, documents, documentable_elements, navigation_targets, queues, module_cycles, database_connections) plus get_option() and struct(), and fields on scheduled_event (repeat, on_overlap, time_zone) and project_security (anonymous_user_role). Four of the missing functions are used by shipped rules.", "cause": "Each builtin and field was added in code with its own PR and a docs-site paragraph at most; nothing compared the skill to buildPredeclared() or to the struct keys, so every API addition drifted from the skill by default. #1178's test pinned only the entity and microflow tables.", "file": ".claude/skills/mendix/write-lint-rules/SKILL.md, mdl/linter/starlark_skill_coverage_test.go", "insight": "Cover the whole surface at once rather than the table that was reported: an internal test ranges over (&StarlarkRule{}).buildPredeclared() for builtins and go/ast-parses every starlarkstruct.FromStringDict(starlark.String(name), dict) call in the package for struct fields (literal dicts and locally-built ones like ppDict), then requires each struct's skill table to match exactly. Structs documented elsewhere are named in explicit maps (helper params, inline struct{...} in a function row, shared permission table) so an exemption is a visible decision. The quick one-off Python diff over-reported (nested password_policy, permissions_for's entity_name) -- the test encodes those layouts instead of guessing.", "refs": "mendixlabs/mxcli#1178"} -{"area": "cmd/mxcli", "date": "2026-09-26", "symptom": "`--json` output did not parse. `mxcli refs|callers|callees|impact|structure|show|search … --json` and `mxcli -c \"select … from CATALOG.x\" --json` printed \"Connected to:\", catalog load/build progress (`✓ Table: N`, 40+ lines on a cold cache), a \"References to X\" header and a \"Found N\" count ahead of the array; an empty answer was the sentence \"(no references found)\" / \"(no results)\" instead of []; `context` ignored --json and printed markdown; `check --format json` wrote its document to STDERR (one per phase), leaving only executor chatter on stdout; `show widgets|data transformers|import/export mappings|catalog status` had no JSON path for empty or at all; `diff --json` silently printed a text diff. Found by an agent-orientation audit on a 140MB app (Evora Factory Management)", "cause": "The executor has ONE writer, ctx.Output, and it carries both the payload and every status line. The #904 fix (progressSink) only works when the payload is written by the COMMAND (lint/report format their own result to os.Stdout, so the executor's writer can be pointed at stderr); for every query subcommand the payload is written by the executor itself, through the same ctx.Output as the chatter, so no choice of writer at the cmd layer can separate them. The split has to be inside the executor, per line, by what the line is", "file": "`mdl/executor/exec_context.go` (`Diagnostics` field + `diagnostics()`, identical to ako/mxcli#716, plus `progress()`: Output in text mode, diagnostics() in JSON mode), `mdl/executor/format.go` (`writeEmptyResult`), `mdl/executor/cmd_catalog.go` (connect/load/build/warn/count lines, `writeCatalogStatusJSON`), `mdl/executor/cmd_search.go`, `cmd_context.go`, `cmd_widgets.go`, `executor_connect.go`, `reference_target.go`; `cmd/mxcli/cmd_check.go` (one structured document on stdout via `finish`), `cmd/mxcli/cmd_query.go` (search: `--json` canonical, `--format json` deprecated alias sets the executor format), `cmd/mxcli/output_stream.go` (`refuseJSONFlag`, used by diff/diff-local); tests `cmd/mxcli/json_output_purity_test.go`, `mdl/executor/progress_stream_test.go`", "insight": "**Classify each line by what it IS, not where it is printed**: an answer goes to Output, commentary about the run goes to `ctx.progress()`. Text mode is byte-for-byte unchanged because progress() IS Output there — assert that too (`TestTextModeKeepsProgressOnStdout`), or the next fix 'cleans' interactive output by sending it all to stderr. **The empty case is where JSON contracts break**: every handler that early-returns a sentence on zero rows must go through writeEmptyResult (or the existing `&& ctx.Format != FormatJSON` idiom) — the sweep found 8 such sites and most list handlers already had the idiom, so grep for the ones that do not. **--json is a ROOT persistent flag**, so every subcommand accepts it; a command with no JSON output must refuse it (stderr, exit 2), since ignoring it hands text to a parser. **Test in a child process** (reuse TestRunMainHelper): stdout and stderr are then genuinely separate, error paths ending in os.Exit are observable, and no cobra flag state leaks between table rows. Proof: forcing progress() to return Output fails 18/18 --json rows, the search alias and the error test; reverting cmd_check.go fails all five check rows. Left alone: the 10,000-line output guard counts pretty-printed JSON lines, so `show widgets --json` on Evora emits the complete document and then exits 1 'output line limit exceeded'", "refs": ["#904", "ako/mxcli#716"]} -{"area": "cmd/mxcli", "date": "2026-09-28", "symptom": "`mxcli check x.test.mdl` reported an MDL-DEPR001 warning (`create or replace` is deprecated) on the doc comment of every @test block, for a spelling the author never wrote; the conformance gate (#756) counted 70-odd of them across mdl-examples' test files", "cause": "testrunner.CheckSource renders each test block as a microflow so the top-level grammar can check it, and its wrapper text was `CREATE OR REPLACE MICROFLOW …`, placed on the doc-comment line. The deprecation registry landed after the wrapper, and nothing checked mxcli's own generated MDL against it", "file": "`cmd/mxcli/testrunner/check_source.go` (wrapper spelling), test `TestCheckSourceWrapperIsCanonical` in `check_source_test.go`", "insight": "**Generated MDL is subject to the deprecation registry too**, and a warning on generated text lands on the author's line numbers, where it looks like their mistake. The other generators in testrunner (generator.go, endpoint.go) still write `create or replace` into scripts that are executed, not checked, so they warn nowhere a user sees — left alone. Proof: the new test fails with MDL-DEPR001 on line 1 of the rendering before the one-word fix.", "refs": ["ako/mxcli#756"]} -{"date": "2026-09-30", "area": "cmd/mxcli", "symptom": "`mxcli fmt --upgrade tests/csv-import.test.mdl` failed with `no viable alternative at input '/**\\n * @test …'` on a file `mxcli check` passes, so the migration tool could not reach a project's test suite; separately `mxcli check x.test.md` reported a syntax error on every valid markdown test block whose doc comment spans lines.", "cause": "fmt parsed the file as top-level MDL, but a test file is `/** @test */` doc comments and `/` separators around microflow bodies; only check rendered it (testrunner.CheckSource). The markdown parser counted a fence block's lines from the ```mdl-test line instead of the line after it, so BodyLine was one early and the rendering overwrote the doc comment's last line with the body.", "fix": "testrunner.UpgradeSource upgrades CheckSource's line-preserving rendering and maps it back line for line: a line the rendering kept verbatim takes the upgraded text, every other line stays the author's, and a rewrite reaching a wrapper line or changing the line count is refused. fmt routes test files there (plain fmt on one is refused with a pointer to --upgrade). parseMarkdownTests passes blockStart+1 as the chunk line.", "insight": "A test file takes no language header: neither check's rendering nor either runner generator reads a `mdl 1;` line, and one in front of the first @test makes that test's doc comment non-leading, so the runner silently drops it. --header therefore adds no header to a test file and says so, rather than applying mdl 1 rewrites the runner would execute as mdl 0. The markdown BodyLine bug was found only because the upgrade's corpus test re-parsed the rendering of mdl-examples/doctype-tests/microflow-spec.test.md; it also shrank the conformance allowlist.", "issue": "ako/mxcli#837", "file": "cmd/mxcli/testrunner/upgrade_source.go, cmd/mxcli/testrunner/parser.go (parseMarkdownTests), cmd/mxcli/cmd_fmt.go", "test": "cmd/mxcli/testrunner/upgrade_source_test.go, TestCheckSourceMarkdownBodyLine, cmd/mxcli/cmd_fmt_upgrade_test.go TestFmtUpgrade_TestFile"} -{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "The LSP's CREATE MICROFLOW / CREATE NANOFLOW / CREATE ENUMERATION snippet completions inserted MDL that does not parse under any language version (`missing '(' at 'BEGIN'`, a quoted enumeration value name), the CONSTANT snippet the deprecated clause form (MDL-DEPR136), and PAGE/SNIPPET a statement mdl 1 refuses for its missing `;`; completion also offered alias-only keywords (SHOW_PAGE, DELETE_BEHAVIOR, DEFINE) and the `show entities` listings after SHOW.", "cause": "The snippets and keyword lists were hand-written once and never parsed; the grammar moved on (R2 property lists, R8 words, `list` for `show`) and nothing tied the completion text to it. The keyword list is generated from the lexer alone, which cannot tell an alias token from a canonical one: that is said by the `/* @alias MDL-DEPRnnn */` markers in the parser grammar.", "fix": "Snippets rewritten to canonical mdl 1. cmd/gen-completions reads the parser grammar too and drops a token every parser-rule use of which carries an @alias marker (the `keyword` rule, which lists tokens usable as names, does not count). `list` gets the listings; `show` offers page/message/home page.", "insight": "A completion text is MDL that ships in the binary, so it is held to what docs are held to: TestCompletionSnippetsAreMdl1 parses every snippet, expanded with its defaults, under `mdl 1;` and requires no deprecation. Deciding alias-only from the markers is the registry's own data; a token-swap rewrite word (`snippet`, `column`, `comment`) is NOT alias-only \u2014 those words stay canonical elsewhere \u2014 and neither is SHOW (show page, show message).", "issue": "ako/mxcli#714 (decision 5)", "file": "cmd/mxcli/lsp_completion.go, cmd/gen-completions/aliases.go", "test": "cmd/mxcli/lsp_mdl1_test.go TestCompletionSnippetsAreMdl1, TestCompletionOffersNoDeprecatedSpelling, TestCompletionListAndShowContinueIntoMdl1; cmd/gen-completions/aliases_test.go"} -{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "A stub-then-real script set (two files each with `create or modify microflow X`) upgraded with `fmt --upgrade -p` one file at a time ended up split: the stub's file declined the header (MDL-V1-REBUILD) and the real flow's file took it. Run in order, the mdl 0 stub rebuilt the stored real flow and the mdl 1 real statement was refused, every run; mx check 0 errors, the app running the placeholder.", "cause": "Each command judged one file against the model as stored; check/fmt took exactly one file, so nothing could see a flow declared twice across the set, and a per-file header decision is wrong for a pair whose files must agree.", "fix": "check and fmt accept several files as one script set (cmd/mxcli/script_set.go): findFlowRedeclarations finds a flow declared by two create-or-modify statements; check warns MDL-STUB01 naming both; fmt --upgrade groups files sharing such a flow and declines the header for all of them when any one cannot take it (canTakeHeader = the per-file verdict). One file alone is unchanged.", "insight": "A per-file verdict is only right when the files are independent; the run is the unit when two files write the same document. The symptom hides because each half is individually correct (decline is right for the stub, header is right for the real file) - only the combination is wrong, so test the set, with the single-file decision as the control.", "issue": "ako/mxcli#905", "file": "cmd/mxcli/script_set.go", "test": "cmd/mxcli/script_set_test.go TestFmtUpgrade_ScriptSetDecidesStubThenRealHeaderTogether, TestCheck_ScriptSetWarnsOnStubThenReal"} -{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "fmt --upgrade over a stub-then-real set still split the pair when the stub's file carried a written `mdl 0;`: the real file took `mdl 1;`. With the real file already `mdl 1;`, fmt printed 'no language header added' about it while the pair stayed split.", "cause": "canTakeHeader returned true for ANY written header (langver.ScanWrittenHeader's bool), reading a pinned `mdl 0;` as 'already has the header'; and the group decline was applied to a file that already carries the header, which fmt never removes.", "fix": "canTakeHeader: a written header can take it only when it is langver.Latest. decideSetHeaders: a file already under the header is not declined; fmt says the pair stays under different headers and to drop the stub or take the header off.", "insight": "ScanWrittenHeader's bool means 'a header is written', not 'the header is mdl 1'; a written mdl 0 pin is the strongest 'cannot take it' there is. Test the group decision with every header state of each file, not only headerless ones.", "issue": "ako/mxcli#905", "file": "cmd/mxcli/cmd_fmt.go", "test": "cmd/mxcli/script_set_test.go TestFmtUpgrade_ScriptSetPinnedMdl0StubHoldsTheRealFileBack, TestFmtUpgrade_ScriptSetAlreadySplitPairIsReportedAsSplit"} -{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "`mxcli version` fails with `unknown command \"version\" for \"mxcli\"` while `mxcli --version` works", "cause": "cobra's `Version:` field registers only the --version flag; no subcommand existed, though shouldSuppressWarning already listed a `version` argument", "fix": "Add a `version` subcommand (cmd/mxcli/cmd_version.go) printing exactly cobra's --version text (name, version, build time)", "insight": "Code that special-cases an argument (warning suppression) is evidence of an intended command; a test pinning the subcommand output to the flag output keeps the two from drifting", "issue": "ako/mxcli#534", "file": "cmd/mxcli/cmd_version.go", "test": "cmd/mxcli/cmd_version_test.go"} -{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "`mxcli check`/`exec`/`fmt`/`diff` fail on a script saved by Windows PowerShell 5.1: a UTF-8 BOM gives `line 1:0 token recognition error at: '\\ufeff'` (an invisible character), UTF-16LE gives a token error on almost every character; the same through stdin and in .test.mdl files", "cause": "Every script reader passed the raw file bytes to the lexer, which reads UTF-8 without a BOM; there was no shared reader (fmt, diff, the multi-file check pass, the test runner and EXECUTE SCRIPT each called os.ReadFile on their own)", "fix": "New mdl/srctext.Decode (strip a leading UTF-8 BOM, decode UTF-16LE/BE by BOM); readMDLSource calls it and fmt, diff and parseScriptSet now read through readMDLSource; testrunner.ParseTestFile and EXECUTE SCRIPT call it directly", "insight": "A BOM also hides a `mdl 1;` header from langver.ScanWrittenHeader, so stripping it in the parser alone would have left the language version wrong: decode where the bytes are read, before anything inspects the text. Enumerate the readers (grep os.ReadFile / io.ReadAll(os.Stdin)), not just the one the report names", "issue": "mendixlabs/mxcli#1253", "file": "mdl/srctext/srctext.go; cmd/mxcli/mdlsource.go", "test": "mdl/srctext/srctext_test.go; cmd/mxcli/mdlsource_encoding_test.go"} -{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "`mxcli -p App.mpr -c \"\"` opens the interactive REPL (a generator spawning mxcli with an open stdin hangs at `mdl>`); `-c \"describe entity System.User; describe entity String; describe entity System.FileDocument\"` stops at statement 2 with `module name is required: objects must be created within a module` and the third statement is silently never run", "cause": "Root Run tested `commands != \"\"` to choose -c over the REPL, so an empty flag value was indistinguishable from no flag; the -c path used ExecuteProgram, which returns the first error without its position, and describe entity/association reached findModule(\"\"), whose message is written for the create path", "fix": "`cmd.Flags().Changed(\"command\")` selects the one-liner path; runCommandLine (cmd/mxcli/oneliner.go) refuses empty input, reports `statement N of M` and how many later statements were not run (via new Executor.ExecuteProgramReportingStop), and takes --continue-on-error like exec; execDescribe names an unqualified entity/association name", "insight": "A flag's zero value is not its absence: use Changed() whenever an empty value must mean something other than not given. Decided semantics: -c is fail-fast like exec (a later statement may depend on an earlier one), but a stop is never silent", "issue": "mendixlabs/mxcli#1218", "file": "cmd/mxcli/oneliner.go; cmd/mxcli/main.go; mdl/executor/executor.go; mdl/executor/executor_query.go", "test": "cmd/mxcli/oneliner_test.go; mdl/executor/describe_unqualified_name_test.go"} -{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "`mxcli lsp --stdio` exits rc=2 with `panic: only file URIs are supported, got mendix-mdl` on textDocument/didOpen of a `mendix-mdl:` virtual document (the VS Code extension's describe previews); VS Code restarts it, it crashes again, and after 5 crashes it stops restarting the server", "cause": "checkableDocument (diagnostics) and CodeAction called go.lsp.dev/uri URI.Filename(), which panics on any scheme but file, to decide whether the document is a .test.mdl", "fix": "documentPath(uri) returns Filename() only for file: URIs and the URI's path component otherwise; runSemanticCheck (which shells out `mxcli check `) skips non-file documents; virtual documents are still diagnosed in memory", "insight": "Third-party helpers that panic on unexpected input are a crash path in a long-running server; every URI an LSP client sends is untrusted shape. A test with a non-file URI plus a file-URI control (same diagnostics) proves the virtual case is handled, not skipped", "issue": "mendixlabs/mxcli#1245", "file": "cmd/mxcli/lsp_helpers.go (documentPath, isFileURI); cmd/mxcli/lsp_diagnostics.go; cmd/mxcli/lsp_language.go", "test": "cmd/mxcli/lsp_virtual_uri_test.go"} -{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`mxcli test --local` fails to chain the project's after-startup microflow, then cleanup restores `AfterStartupMicroflow: 'AfterStartupMicroflow: ''Mod.Flow'` and the project is left pointing at `MxTest.RegisterEndpoint`", "cause": "`parseSettingValue` scraped DESCRIBE SETTINGS text and split only on `=`; the describe rewrite changed the output to `Key: 'value'`, so the whole line was kept as the value. The #803 tests used the old `=` lines only and stayed green against output describe no longer emits", "file": "`cmd/mxcli/testrunner/runner.go` (`parseSettingValue`, `getAfterStartup`)", "insight": "A parser of another command's human-readable output breaks silently when that output changes; the table test must carry the line the producer emits *today*. Split on the first `:` or `=`, unescape `''`, and match the key as a line prefix. Longer term, read the setting through the backend instead of scraping describe", "refs": []} -{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`mxcli check tests/X.test.mdl` (and the VS Code LSP) warns MDL-V1-SLASH once per test, on the doc-comment line, although the skill says a test file takes no `mdl 1;` header", "cause": "`CheckSource` renders each block as a microflow and closed the wrapper with `END; /`; the rendering is parsed headerless, so the visitor recorded a V1 slash note on a `/` mxcli itself wrote. The author's `/` separators are blanked and never reach the parser", "file": "`cmd/mxcli/testrunner/check_source.go`", "insight": "Generated MDL must be canonical under every language version, or the diagnostics blame the author's file. `TestCheckSourceWrapperIsCanonical` asserted no Deprecations but not LanguageNotes; it now asserts both", "refs": []} -{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`theme create --from --base console` writes `_mxcli-.scss` with a stray `}}`/`}` where the @font-face section was (27 `{` vs 29 `}`); the theme does not compile. Same for --base signal and ledger", "cause": "`dropFontFaces` matched each `@each $weight { @font-face { … } }` block with `[^}]*\\}[^}]*\\}`; the `src: url(\"…-#{$weight}-…\")` interpolation's `}` ended the first span early, so the match stopped at the @font-face close and left the @each's `}`", "file": "`cmd/mxcli/theme/create_seeded.go` (`dropFontFaces`, now `dropFontFaceBlock`)", "insight": "Never match nested SCSS blocks with `[^}]*`; count braces (interpolations are balanced). The existing test used the real shape but asserted only absence of @font-face — any rewrite of a stylesheet should assert brace balance on every shipped asset. Not copying the fonts folder is intended when the design's font stacks name none of the vendored families", "refs": []} -{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`mxcli check tests/X.test.mdl -p app.mpr --references` fails with `module not found: MxTest`; a body calling a microflow that does not exist is never reported", "cause": "`CheckSource` wraps each test block as `CREATE OR MODIFY MICROFLOW MxTest.…` but nothing in the rendering created MxTest (only a run does), and the CreateMicroflowStmt reference check returns on the missing module before `validateFlowBodyReferences`", "file": "`cmd/mxcli/testrunner/check_source.go` (`CheckSource`)", "insight": "A rendering checked against a project must declare everything the runner would create; an early return on the container hides every finding inside it, so a false positive here was also a false negative. `CREATE MODULE` on the first wrapper's line keeps line numbers", "refs": []} -{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`mxcli exec` on a script with report pages prints ~35 MDL-WIDGET15 info notes (one per container with adjacent inline dynamictexts) on every run, burying the warnings and errors that matter", "cause": "execPreflight formatted every ValidateProgram violation with linter.TextFormatter, no severity filter — the same report `check` prints, on a command that is re-run", "file": "`cmd/mxcli/exec_preflight.go` (printPreflightViolations), `mdl/linter/output.go` (TextFormatter.OmittedInfos)", "insight": "Filter at the exec printer, not at the rule: `check` is where a script is reviewed and keeps every note, and lowering a rule's frequency would hide it there too. The formatter's summary line had to learn about the omitted notes, or it read `0 info` above the line saying N were hidden. exec has no structured diagnostics output, so JSON/SARIF (check --format) are untouched. `--verbose` on exec and diff restores the full report", "refs": []} -{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`exec` of a script with a flow change it refuses (\"cannot be spliced\" under mdl 1) writes every statement before the refused one and none after it; `check -p` had predicted the refusal", "cause": "`execPreflight` ran the semantic pass, references and name clashes but not `CheckFlowVerdicts`, which `check -p` and `fmt` already run", "file": "`cmd/mxcli/exec_preflight.go` (`execPreflight`)", "insight": "Every verdict check can compute must also gate exec's pre-flight, or the two disagree and exec half-applies. --continue-on-error keeps running every other statement, so the verdict is printed there but does not refuse", "refs": []} -{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`check -p` prints `1 issues: 0 errors, 1 warnings` then `✓ All references valid` then `1 issues: 1 errors, 0 warnings` — reads as an error next to a passing run, and neither line is the total", "cause": "each tier of runCheckFile (semantic, project verdicts, legacy widgets) formatted its batch with a TextFormatter that printed its own summary line", "file": "`cmd/mxcli/cmd_check.go` (`runCheckFile`), `mdl/linter/output.go` (`TextFormatter.NoSummary`, `WriteSummary`)", "insight": "A multi-stage report needs one summary at exit over everything printed; the per-batch formatter's summary is only right for single-batch callers (lint, exec preflight)", "refs": []} -{"date": "2026-10-02", "area": "cmd/mxcli", "symptom": "`run --local --watch` on Mendix 11.13: a page added while the loop runs is never bundled — the apply reports success (often 'applied via reload'), but opening the page 404s on dist/pages/..js and the page stays blank; restarting the loop fixes it", "cause": "mxbuild's tools/node/rollup-plugin-mendix-pages.mjs (byte-identical in 11.13.0 and 11.14.0) globs web/pages only at bundler start: watchChange compares path.relative(cwd, id) against PAGES_FOLDER=\"./pages\" + \"/\", a './' prefix relative() never yields, so shouldRefreshPageFiles never flips. mxcli could not see it: ensureClientServed probes index.js and its static imports, and pages are dynamic imports", "fix": "missingPageChunks compares web/pages/**/*.js against web/dist/pages/**/*.js (shape-gated: needs web/pages and web/dist/index.js, so 11.14 prebuilt and classic are no-ops); under --watch watchAndApply restarts the WebClientWatcher (a fresh rollup run re-globs) and reports one line; ensureClientServed does a one-shot BuildWebClient for the same condition. RunLocal stops whichever watcher is current at exit", "insight": "A stand-alone rollup watch repro (pages/A.js, then add pages/B.js) isolates it in seconds: the 'change pages/B.js create' event fires but the next bundle still emits only A; a fresh watcher, or the plugin with PAGES_FOLDER=\"pages\", emits both. Restart rather than one-shot: the stale watcher would also never rebuild the new page when it is edited later. A structural change (navigation) clears web/dist and the old index.js fallback re-bundles everything, masking the bug; a page-only add applied via reload is the reproducing case. E2E 11.13: pre-change binary 404 on the new chunk; fixed binary 200 + page text, and a follow-up edit of the same page re-bundled incrementally with no restart (control)", "file": "cmd/mxcli/docker/webclient_pages.go (missingPageChunks, recoverMissingPages); cmd/mxcli/docker/runlocal.go (watchAndApply, ensureClientServed, RunLocal watcher defer)", "test": "cmd/mxcli/docker/webclient_pages_test.go"} -{"date": "2026-10-02", "area": "cmd/mxcli", "symptom": "A `.test.mdl` starting with `mdl 1;` loses its first test: `mxcli test --list` finds 41 of 42 with no message; `mxcli check` on it reads the bodies as mdl 0 (MDL-V1-SLASH / MDL-V1-LIMIT1 warnings fire under a file that says mdl 1); the generated runner scripts end every flow with `/`, which mdl 1 refuses; and `fmt --upgrade --header` adds no header to a test file", "cause": "Nothing in the test format read the header. It was body text in the first `/`-chunk, so that chunk's `/** @test */` was no longer a LEADING doc comment (scanDocComments) and parseMDLTests skipped it silently; CheckSource rendered only the bodies (dropping the header line) and put `END; /` on separators; GenerateTestRunner/GenerateTestFlows always wrote `/` and `create or replace`", "fix": "takeLanguageHeader reads the header with langver.HeaderSpan (the grammar's own rule: first token after trivia) and blanks it in place so lines/columns hold; TestCase carries Version + HeaderLine (markdown: per mdl-test block); CheckSource renders the header on its line and closes wrappers with `END;` (no `/`, any version); the generators write the header, `create or modify` and no `/` under mdl 1; parseTestFiles refuses a suite mixing versions (one suite = one script = one header); UpgradeSource honours AddHeader and writes the header at file top / inside each mdl-test block; UpgradeSource maps the upgraded rendering back with a line diff (difflib opcodes, every changed region must be body lines) instead of an index walk, because the MDL-V1-ESCAPE rewrite of `\\n` writes a real line break and adds a line; writeBodyLines no longer indents a body line that starts inside a string literal", "insight": "A header is not just a flag to pass along — in a format that is NOT parsed as one script, every consumer that splits the text has to take it out first, or it becomes content in whichever chunk it lands in. The silent drop came from the same 'leading doc comment' rule as #927; a per-test Version also forced the question 'what is a suite's version', which only refusal answers safely. The runtime run was what found the last defect: unit tests and `mx check` were green while an upgraded test asserting length 3 saw 5 — the generators indented the continuation line of a multi-line string literal, so the indent became part of the value. Compare an mdl 0 file, its fmt --upgrade output, and a headerless control in the running app (both runners)", "issue": "ako/mxcli#847", "file": "cmd/mxcli/testrunner/parser.go (takeLanguageHeader, suiteLanguageVersion); check_source.go; generator.go; generator_endpoint.go; upgrade_source.go; mdl/langver/langver.go (HeaderSpan)", "test": "cmd/mxcli/testrunner/language_header_test.go; e2e: mxcli test --local on a fresh 11.13.0 app, endpoint and --legacy-runner"} -{"date": "2026-10-02", "area": "cmd/mxcli/check", "symptom": "`mxcli check x.test.mdl -p app.mpr` reports `module not found: MxTest` once per test and exits 1 on a file `mxcli test` runs green — a test file can never pass check, and a real missing reference in a body is hidden behind it", "cause": "CheckSource wraps each test body in a `MxTest.Check_*` microflow (the runner's module), and the reference pass resolved that module against the project; the runner creates MxTest as the first statement of every generated script and drops it afterwards, so the project never has it", "fix": "testrunner.WithRunnerModule appends `create module if not exists MxTest` to the program used for the reference and project-conflict pass of a test file (appended so `statement N` still numbers the tests; definitions are collected program-wide; `if not exists` keeps a user's own MxTest from reading as a conflict)", "insight": "Seed the module the way the runner does, rather than exempting the file: the control (a body with a real missing entity) must still fail, and before the fix it showed only the MxTest error — the false positive was also masking true ones", "issue": "ako/mxcli#677", "file": "cmd/mxcli/testrunner/check_source.go (WithRunnerModule); cmd/mxcli/cmd_check.go", "test": "cmd/mxcli/check_test_file_refs_test.go"} -{"date": "2026-10-03", "area": "cmd/mxcli/check", "symptom": "`mxcli check -p` prints MDL067 (bare commit now WITH events) for a create-or-modify flow already stored with events, which `exec -p` no longer prints", "cause": "cmd_check ran ValidateProgram without the DropSettledCommitNotes filter exec_preflight applies; the project was only connected later, for the reference tier", "fix": "check connects to the project before the semantic report when -p is given, applies DropSettledCommitNotes and StoredTaskClaimViolations, and reuses that connection for the reference tier", "insight": "Two gates over one rule set drift whenever a post-filter lives in only one of them; grep for every caller of ValidateProgram when adding a filter. Control: a flow stored without events still notes", "issue": "ako/mxcli#943", "file": "cmd/mxcli/cmd_check.go", "test": "cmd/mxcli/check_stored_semantics_test.go"} -{"date": "2026-10-03", "area": "cmd/mxcli/test", "symptom": "every `mxcli test` run prints 2x MDL-DEPR001 and 2x MDL-V1-SLASH about a script the user never wrote", "cause": "GenerateEndpointMDL emitted a headerless mdl 0 script with `create or replace` and `/` terminators; the test-flow generators had already moved to the version-aware writeScriptHeader/createFlow/writeFlowEnd", "fix": "GenerateEndpointMDL writes mdl 1 through the same helpers (header, create or modify, `;` only); endpoint script is independent of the suite's version", "insight": "A generated script is checked like a user's one; pin it with a test that parses it and asserts ValidateProgram returns nothing. Verified end to end with `mxcli test --local` on a fresh 11.13 app", "issue": "ako/mxcli#943", "file": "cmd/mxcli/testrunner/endpoint.go", "test": "cmd/mxcli/testrunner/endpoint_clean_test.go"} -{"date": "2026-10-03", "area": "cmd/mxcli/theme", "symptom": "`theme create acme --from design.css` with `--mxt-font: \"Inter\", system-ui, sans-serif` prints nothing about Inter; the theme ships no woff2 and no @font-face for it and renders in the fallback font wherever Inter is not installed", "cause": "planFonts only decided which VENDORED families to drop; a seeded family outside the vendored set was never looked at, so the silent outcome was the default", "fix": "unvendoredSeededFamilies takes the primary (first) family of each seeded font stack, skips generic families and var() and the families the base partial loads, and CreateResult.UnvendoredFonts carries them to cmd_theme.go, which prints a note per family naming mxcli-fonts/ and the partial", "insight": "Only the first family of a stack is the design's choice; flagging the fallbacks (Helvetica, Arial) would make the note noise. The controls are a vendored family (IBM Plex Mono) and a generic stack, which must stay silent", "issue": "ako/mxcli#944", "file": "cmd/mxcli/theme/create_seeded.go (unvendoredSeededFamilies, planFonts); cmd/mxcli/cmd_theme.go", "test": "cmd/mxcli/theme/create_seeded_test.go (TestCreate_NamesSeededFontsItDoesNotVendor)"} -{"area": "cmd/mxcli/docker", "date": "2026-10-03", "symptom": "`mxcli docker check` (a check) rewrote an MPRv1 project's .mpr permanently, and on v1 and v2 alike rewrote theme-cache/web/theme.compiled.css(.map) and created deployment/sass/main.scss \u2014 with or without --no-update-widgets", "cause": "update-widgets ran on the user's project, protected only by a snapshot/restore of the v2 storage (.mpr + mprcontents/), so v1 had no protection; and `mx check` itself compiles the theme into theme-cache/ and writes deployment/sass/, which nothing guarded", "file": "`cmd/mxcli/docker/check.go` (`Check`), `cmd/mxcli/docker/check_copy.go` (`copyProjectForCheck`)", "insight": "A snapshot of the files you expect a tool to touch protects only those files; measure with a whole-tree hash+mtime diff before/after, which is what showed that plain `mx check` writes too. The fix is to run both mx steps on a temporary copy (skipping deployment/, releases/, theme-cache/, .git, node_modules) and rewrite the copy's path back in mx output. Control: a CE0117 microflow is still reported on both formats with the tree unchanged. `docker build` keeps runUpdateWidgets because it is expected to write deployment/ \u2014 but it still rewrites a v1 .mpr", "refs": ["ako/mxcli#951", "ako/mxcli#568", "ako/mxcli#646"]} -{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "A project whose CLAUDE.md, skills and lint rules were written by a newer mxcli is served by an older binary (v0.24.0 on PATH) with no warning: 'mdl 1;' is a parse error and shipped lint rules crash, all reading as project defects", "cause": "Nothing recorded which mxcli wrote the tooling; .claude/bootstrap-mxcli.sh linked whatever mxcli was on PATH; init --sync-skills refreshed only .ai-context/skills, never .claude/lint-rules or CLAUDE.md/AGENTS.md", "fix": "init and every sync write .ai-context/mxcli-tooling.json; root PersistentPreRun warns once on stderr when the binary is provably older (release by number, nightly by tag date, mixed by build date, dev never); sync refuses from an older binary; the bootstrap script carries a POSIX-sh copy of the ordering and neither links an older PATH binary nor keeps an older ./mxcli, downloading via a temp file + mv; sync also refreshes bundled lint rules by name and the CLAUDE.md/AGENTS.md section between mxcli:begin/end markers", "insight": "A binary cannot warn about a stamp it predates, so the guard for already-shipped binaries must live in the generated script, which the newer mxcli regenerates. The sh and Go comparisons share one test table so they cannot drift. curl -o ./mxcli on a symlinked ./mxcli would overwrite the PATH binary — always download to a temp name and rename", "issue": "ako/mxcli#952", "file": "cmd/mxcli/tooling_stamp.go; cmd/mxcli/init_tooling_sync.go; cmd/mxcli/init_hook.go (bootstrapScriptTemplate); cmd/mxcli/main.go; cmd/mxcli/init.go", "test": "cmd/mxcli/tooling_stamp_test.go; cmd/mxcli/init_hook_version_test.go; cmd/mxcli/init_tooling_sync_test.go"} -{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "CONV006 emits one finding per entity x role x CREATE/DELETE (111 on a mid-sized app), the same advice repeated per role, and the per-finding Security score is driven by role count rather than by entities", "cause": "The Starlark rule appended a violation inside the permissions_for() loop", "file": "`.claude/lint-rules/conv006_no_create_delete_rights.star` (synced to `cmd/mxcli/lint-rules/`)", "insight": "Group per entity and per right with de-duplicated sorted roles (a role can hold several access rules on one entity). Test both rule copies (.claude and the embedded one) like SEC008's test does", "refs": ["ako/mxcli#953"]} -{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "`mxcli report` could not score a project's own modules: `lint` has --modules, `report` had only --exclude", "cause": "Feature gap; and the LintContext module filter alone would not make the score exact, because project-level findings (CONV008 role mappings, project security) carry no module and are reported regardless", "file": "`cmd/mxcli/cmd_report.go`, `mdl/linter/report.go` (`ScopeToModules`, Report.Modules)", "insight": "Filter the scored violations to those located in a selected module, and print the selection in every format so a module score is not mistaken for the project's", "refs": ["ako/mxcli#953"]} -{"area": "cmd/mxcli/docker", "date": "2026-10-03", "symptom": "`mxcli docker build` (and docker run / reload, which call it) rewrote an MPRv1 project's .mpr, rewrote every MPRv2 .mxunit (restored with new mtimes), and wrote theme-cache/, deployment/, javasource/ proxies, the .launch file, .classpath and .project into the project; the TUI checker and `mxcli eval`'s mx_check wrote theme-cache/web/ and deployment/sass/ on every run", "cause": "update-widgets ran on the project under a snapshot that restored only v2 storage, and mx check / MxBuild ran on the project itself; the TUI and eval runner ran a bare `mx check `", "file": "`cmd/mxcli/docker/build.go` (`buildOnCopy`), `cmd/mxcli/docker/check.go` (`MxCheckOnCopy`), `cmd/mxcli/tui/checker.go` (`runMxCheck`), `cmd/mxcli/evalrunner/checks.go` (`checkMxCheck`)", "insight": "Run update-widgets, mx check and MxBuild on one temporary copy (copyProjectToTemp) and write only the output directory. Measured on the 11.14 testapp: the PAD from the copy differs from the in-place build in the same 9 files as two in-place builds of identical copies differ (cache-bust stamps, operation ids, the native metro paths), so the output is equivalent; rebuild time unchanged (58s vs 59s), because a PAD build gains nothing from the project's deployment/. 11.14's blank app needs JDK 25, so the real-mx build test skips without it; run it with 11.13's mx.", "refs": ["ako/mxcli#961", "ako/mxcli#951", "mendixlabs/mxcli#808"]} -{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "ako/mxcli#962 item 3: in VS Code, two calls to a stored void JavaScript action with the same output name (Studio Pro's `$ReturnValueName`/`$RefreshEntity` shape) are squiggled MDL063, while `mxcli check -p` passes them", "cause": "runSemanticValidation called executor.ValidateMicroflow/ValidateNanoflow, which pass a nil void-action resolver: without the project every call output counts as a declaration", "file": "`cmd/mxcli/lsp_diagnostics.go` (runSemanticValidation); `mdl/executor/validate_void_code_calls.go` (FlowRules, CodeActionCache)", "fix": "executor.NewFlowRules(prog, s.findMprPath(), s.codeActions): resolves actions through the script and the workspace project (opened lazily), treats an unresolvable action as possibly void (unknownIsVoid) for MDL063 but never for MDL093, and shares project answers across keystrokes for 30s since one action read costs ~300ms on PedApp", "insight": "An entry point that wraps a richer internal API with nil arguments (ValidateMicroflow = validateMicroflowWith(stmt, nil)) silently gives every caller the weakest behaviour; a fix landed in the richer API (#958) does not reach them. Grep the exported wrapper's callers when the internal one gains a parameter. Measure the cost before putting project I/O on a keystroke path", "test": "`cmd/mxcli/lsp_void_calls_test.go` (PedApp: void pair clean with and without project, Boolean pair control, MDL093 only with project); `mdl/executor/validate_void_code_calls_test.go` (TestCodeActionCache_SharesProjectAnswersAcrossRuns)"} -{"area": "cmd/mxcli", "date": "2026-10-04", "symptom": "`run --local --watch` (Mendix <= 11.13, rollup bundler): after a while every page change fails with `web client rebuild failed: web client watcher exited` (or `client bundle not served after apply: web client re-bundle: web client build timed out after 5m0s`) and nothing reaches the browser until `run --local` is restarted; after adding an entity the next changes fail with `ENOTDIR: not a directory, stat '.../web/pages/.js/package.json'`", "cause": "watchAndApply held a bare *WebClientWatcher: (1) once it exited nothing restarted it (only recoverMissingPages did), so WaitForRebuild failed every later change; (2) a failed incremental build (rollup's commonjs resolver hitting web/pages mid-rewrite by the serve build) left the watcher erroring and the change was dropped; (3) ensureClientServed's recovery ran a one-shot NODE_ENV=production BuildWebClient in the same web/ dir while the watcher was still running — two rollups on web/dist; (4) the 5m limit was hard-coded and a timeout printed nothing about why", "file": "cmd/mxcli/docker/webclient_supervisor.go, cmd/mxcli/docker/runlocal.go (watchAndApply, ensureClientServed), cmd/mxcli/docker/webclient.go (webClientTimeout, webClientBuildLogTail)", "fix": "bundlerSupervisor owns the bundler: EnsureAlive restarts an exited one (backoff 2s..60s after failed starts), AwaitRebuild retries a failed/aborted incremental rebuild once with a fresh bundler, Rebundle = stop+reap then start (never two). ensureClientServed takes the rebundle func; clientRebundler hands it the supervisor under --watch and the one-shot otherwise. --web-client-timeout / MXCLI_WEB_CLIENT_TIMEOUT; timeout errors append the last 30 lines of deployment/log/web-client-build.log. sessionNotice prints that a restart dropped sessions", "insight": "Killing the runner (`kill `) reproduces the dead-watcher state in seconds — no need to wait for it to die on its own. A fresh bundler is the universal recovery under --watch: its first build is a full bundle of the current source, so it covers missing pages, dangling chunks, a dist/ deleted by Gradle, and a transient incremental failure alike, without a second process on web/dist. Note: exec.Cmd.Wait called a second time concurrently with the reaper did block until exit on this Go version, so the old Stop was not the overlap — the one-shot in ensureClientServed was", "test": "cmd/mxcli/docker/webclient_supervisor_test.go (TestBundlerSupervisor_RestartsExitedBundler, _AwaitRebuildRecovers, _RebundleNeverOverlaps, TestEnsureClientServed_WatchModeRebundleIsExclusive, TestBuildWebClient_TimeoutShowsLogTail); live: 11.13 scratch app, 7 consecutive changes incl. a killed runner"} -{"area": "cmd/mxcli", "date": "2026-10-04", "symptom": "`run --local --watch`: an `mxcli exec` (or save) made while a change is still building/applying is never built — no `Change detected` follows, the app keeps the previous model, and re-running the script writes nothing (byte-idempotent) so nothing re-triggers", "cause": "watchAndApply set `last = sourceMTime(...)` after every successful apply, under a comment claiming it kept mid-build edits; it did the opposite — the edit's mtime was folded into the baseline, so the next tick saw nothing newer", "file": "cmd/mxcli/docker/runlocal.go (watchAndApply)", "fix": "keep `last` at the settled mtime the build was taken from; the build writes nothing under the watched model/theme source (checked with find -newer during a live run), so this cannot self-trigger", "insight": "Found while reproducing #971 with a script that waited for the first output line of a change instead of its `applied` line — the next exec landed during a 2-minute restart-apply and vanished. Any test of a watch loop should include an edit made DURING a build, not only between builds", "test": "live only (11.13 scratch app, hsqldb): exec an entity add, exec a page change 15s into its build; fixed binary builds it as the next build, the binary with the refresh restored shows no further build after 45s"} -{"area": "cmd/mxcli", "date": "2026-10-04", "symptom": "ako/mxcli#970 item 2: `theme create --from design.css` with only a :root block (no dark block) wrote the design's light --mxt-ground/--mxt-ink/--mxt-brand into the base theme's dark mixin, whose other surfaces stayed dark; the first injected token also sat on the `@mixin … {` line, unindented", "cause": "Tokens.forVariant returned the base declarations for EITHER variant, and seedTokens applied it to the alt-palette mixin unconditionally; applyTokens matched `(?m)^(\\s*)name`, and \\s* at ^ swallows the preceding newline (and blank line), so the replacement lost its line break and indent", "file": "`cmd/mxcli/theme/create.go` (seedTokens, Create/CreateResult.UnseededVariant); `cmd/mxcli/theme/tokens.go` (applyTokens, Tokens.declares); `cmd/mxcli/cmd_theme.go` (note)", "fix": "seed the alt mixin only when the design declared a block for that variant; otherwise leave it byte-identical to the base and report UnseededVariant, which the CLI prints as a note; match the indent with [ \\t]* instead of \\s*", "insight": "A base palette is the default variant's palette, not 'both': a token set that does not say which variant it describes must not seed the other one. And under (?m), ^\\s* is not 'leading indentation' — it crosses lines; use [ \\t]*. The control for 'mixin untouched' is the same scaffold with no design at all, compared byte for byte", "test": "`cmd/mxcli/theme/create_variant_test.go` (all three bases, base-only vs variant block, indentation); `cmd/mxcli/cmd_theme_test.go` (TestThemeCreate_NotesTheVariantABaseOnlyDesignDidNotSeed)"} -{"area": "cmd/mxcli", "date": "2026-10-04", "symptom": "ako/mxcli#982 item 2: `run --local --admin-port 8091` printed `Query data: mxcli oql -p app.mpr` and that command failed `cannot connect to Mendix admin API at localhost:8090`; `mxcli log list` failed the same way", "cause": "oql resolved the admin port as flag > ADMIN_PORT env > .docker/.env > 8090 and log used flag defaults 8090/mxcli-local-dev; neither read the .mxcli/run-local.json handshake the loop publishes with its port and password (only `constant set --apply` did)", "file": "`cmd/mxcli/devloop_admin.go` (devLoopAdminOptions); `cmd/mxcli/docker_oql.go`; `cmd/mxcli/cmd_log.go` (logAdminOptions, logConnectionHint); `cmd/mxcli/docker/runlocal.go` (hint)", "fix": "one helper takes port/password from a LIVE handshake for any flag not given (explicit host means 'not this loop'), forces direct loopback HTTP; oql and log both use it; the log hint prints the resolved port", "insight": "A hint printed by the process that knows the port must be runnable without that knowledge: either print the flags or make the consumer read what the producer published. Reading the handshake fixes every consumer at once; the test is an httptest admin API on a random port with a fake run-local.json, controlled by the no-handshake and stale-pid cases", "test": "`cmd/mxcli/devloop_admin_test.go`"} -{"area":"cmd/mxcli","date":"2026-10-05","symptom":"run --local --db-type postgresql|hsqldb works but is in neither the run-local skill's nor docs-site/src/tools/run-local.md's flag table; users without PostgreSQL hit 'no local PostgreSQL superuser available to create the role/database' instead of finding --db-type hsqldb","cause":"Flag shipped with help text and proposals only. Separately, the skill's --app-port and --db-host/--db-name/--db-user/--db-password rows had drifted below the OpenTelemetry section, detached from the table header, so they rendered as stray text — a grep for '--db-name' in SKILL.md matched and hid that the table did not contain it","file":"cmd/mxcli/cmd_run_dbflags_doc_test.go","insight":"Check flag-table coverage by parsing the table (rows under a '| Flag |' header), not by grepping the file: a stranded row or a prose mention passes a grep. The test reads the --db-* set from runCmd.Flags(), so the next database flag fails it without editing the test. Full-flag coverage was not asserted because several other run flags (--configuration, --constant, --hub-prefix…, --page-check) are also absent — a separate gap.","refs":["mendixlabs/mxcli#1292"]} -{"area": "cmd/mxcli", "date": "2026-10-05", "symptom": "`mxcli run --local --page-check --screenshot-user U --screenshot-password P` reported every secured page as `title=\"Login\"` — the verdict described the sign-in form, not the page asked for; adding `--screenshot` made it sign in", "cause": "the boot step that signs in and saves the Playwright storage state was gated on `opts.Screenshot && opts.ScreenshotUser != \"\"`; `--page-check` (added later, ako/mxcli#614) reuses that storage but was never added to the gate, so it probed anonymously", "file": "`cmd/mxcli/docker/runlocal.go` (step 6b login gate, reportPageChecks)", "fix": "gate the sign-in on `(Screenshot || PageCheck) && ScreenshotUser != \"\"`; the page probe is now shared with `mxcli playwright check` (docker.ProbePages), which also treats a 401 or the sign-in form as a failure instead of a page", "insight": "A flag that consumes state produced by another flag's setup step silently degrades when used alone. The verdict looked healthy (title, text, console-errors=0) because the login page IS a healthy page; the tell is the title. `playwright check` now fails on `#usernameInput` or a 401 so the same mistake cannot pass", "test": "manual on a scratch 11.13.0 app (run --local --page-check --screenshot-user pc_user: `Logging in as \"pc_user\"` then the Items verdict with rows=2); unit coverage of the login-form failure in `cmd/mxcli/docker/pagecheck_check_test.go`"} -{"area": "cmd/mxcli", "date": "2026-10-05", "symptom": "`mxcli playwright check` (and the login of `run --page-check`/`--screenshot-user`) failed with \"node not found; the page check needs the Node that Playwright uses\" on a machine with no system node whose newest cached mxbuild is 11.15, although mxbuild 11.15 ships tools/node//node", "cause": "resolveNodeForScript found mxbuild's bundled node through resolveNodeTooling, which also requires tools/node/rollup-runner.mjs; mxbuild 11.15 dropped the rollup runner (rspack only), so the lookup failed before it looked for the node binary", "file": "`cmd/mxcli/docker/screenshot_login.go` (resolveNodeForScript)", "fix": "look up the node binary directly with findNodeBinary(/tools/node) instead of going through the rollup-runner check", "insight": "a helper that validates a whole toolchain was reused to find one binary in it; when the platform dropped an unrelated piece of that toolchain the reuse broke a caller that never needed it. Found by a smoke test on a machine whose newest mxbuild was 11.15, not by unit tests, which put node on PATH", "test": "cmd/mxcli/docker/screenshot_login_node_test.go TestResolveNodeForScript_RspackOnlyMxBuild (PATH emptied, 11.15 tools/node layout; fails with the old lookup)"} -{"area": "cmd/mxcli", "date": "2026-10-05", "symptom": "`mxcli run --local --watch`: a model change made while the app is still booting is never built — the app keeps serving the model from before it, `--watch` logs no `Change detected`, and a waiter on the change times out. Re-running the exec does nothing (byte-idempotent: it writes nothing, so nothing re-triggers the watcher)", "cause": "watchAndApply took its baseline `last := sourceMTime(...)` after the boot finished, so an edit landing between the boot build's model read and the watch loop's start was already folded into the baseline", "file": "`cmd/mxcli/docker/runlocal.go` (`RunLocal` bootSource, `watchAndApply`)", "insight": "A watcher's baseline must be the source time the last build was MADE FROM, taken before that build — not 'now' when the watcher starts. Same class as the earlier fix that stopped moving the baseline to time.Now() after an apply. Measured with the run-lifecycle integration test: exec a page while the detached run's state says the boot build is in progress; with the old baseline `run wait` times out after 2m (no build #2), with the fix it reports `applied: build #2 via reload`. Exposed by making `run wait` race-free on source time: a generation-counting wait could not have told the change was lost.", "refs": ["cmd_run_lifecycle_integration_test.go"]} -{"area": "cmd/mxcli", "date": "2026-10-06", "symptom": "`mxcli run --local --watch` on Mendix 10.24 / 11.6 dies after 5 minutes with `starting web client bundler: web client watcher timed out after 5m0s`, while the bundler's own log says `Bundling finished in 8999 milliseconds` — the bundle was done in 9s", "cause": "`parseBundlerStatus` (`cmd/mxcli/docker/webclient_watch.go`) only understood the modern-web-bundler protocol (`{\"protocol\":\"mx-modern-web-bundler\",\"type\":\"status\",\"payload\":{\"kind\":…}}`). The rollup-runner.mjs shipped with 10.24 and 11.6 predates it and writes `{\"code\":\"START\"|\"SUCCESS\"|\"ERROR\",\"payload\":…}` (ERROR payload an object with `message`, or a bare string when the config fails to load). Every status line was dropped as plain logging, so the first-build wait never saw a success", "file": "cmd/mxcli/docker/webclient_watch.go", "insight": "--watch had never worked on these versions: the parser has accepted only the new protocol since it was written (#349). Nothing exercised the watcher below 11.12 until the run-lifecycle integration test reached the nightly matrix — and it reached it only because ResolveMxForVersion substitutes ANY cached mxbuild for the test's default 11.13.0, so each matrix leg ran it against its own version. The runner's protocol is part of the mxbuild version contract: read tools/node/rollup-runner.mjs of the oldest supported version before assuming a stdout shape. Control: the lifecycle test on 11.6.8 with the parser reverted fails with the nightly's exact timeout (MXCLI_WEB_CLIENT_TIMEOUT=60s makes it fail in a minute); with the fix it passes", "refs": ["nightly 2026-10-06 (ako/mxcli run 37437089065, mendixlabs/mxcli run 37437605368)"]} -{"area": "cmd/mxcli", "date": "2026-10-06", "symptom": "`mxcli syntax rename` printed `Unknown topic: rename` and `mxcli syntax --json` had rename only for attributes, values and definitions, while `RENAME MICROFLOW TO ;` passed `mxcli check` and `mxcli help rename` documented it; an agent concluded MDL could not rename a microflow and copied, re-pointed and deleted it by hand", "cause": "The renameStatement rule (MDLParser.g4) and its executor (mdl/executor/cmd_rename.go) never got a SyntaxFeature in cmd/mxcli/syntax/; the only discovery path was the cobra `rename` subcommand's Long text, which `syntax` does not read. BySegmentMatch could not rescue it because no registered path has a `rename` segment", "file": "cmd/mxcli/syntax/features_misc.go", "insight": "Same class as the TABCONTAINER gap (widget_keywords_drift_test.go): an agent treats absence from `mxcli syntax` as absence from the language, so a statement the grammar accepts but the registry omits is effectively missing. Fixed with a `rename` topic plus a guard (rename_topic_test.go) that reads the renameTarget rule from the .g4 and requires every alternative in the topic, and see_also links from microflow/page/entity/move. The CLI subcommand and the MDL statement diverge (MDL also takes JAVA ACTION and WORKFLOW); the topic says so rather than implying parity. Control: with the topic reverted the built binary prints `Unknown topic: rename` verbatim and TestSyntaxRenameTopic fails; dropping the RENAME WORKFLOW line fails the grammar guard. A sweep for other top-level statements with no topic would be the cheap next step", "refs": ["mendixlabs/mxcli#1318"]} -{"area": "cmd/mxcli", "date": "2026-10-08", "symptom": "Main's build-and-test fails intermittently in the run-lifecycle tests, several at once in one run and green on the next: TestRunStop_LeavesNoOrphanBehind (\"fake run has 0 session members, want the leader and its child\"), TestRunStop_ReapsTheLeftoversOfAKilledRun, TestSessionMembers_FindsOrphanedGrandchild (\"members … = [], want the leader and its sleep\"). Not reproducible locally (20/20 green). In real use the same path makes `run stop` / `run status` miss a live run's processes.", "cause": "docker.SessionMembers' pid-reuse guard rebuilt a process's start time as btime + starttime/USER_HZ and dropped it if that fell more than 1s before notBefore. btime in /proc/stat is whole seconds (up to 1s early) and starttime whole 10ms ticks (up to 10ms early), so the rebuild can be ~1.01s early: a process started a few ms after notBefore was dropped. frac(btime) is fixed for a machine's uptime, so a CI VM booted late in a second failed every such test in the run, and others never did; a process started a few ms later (the leader's sleep) survived.", "file": "`cmd/mxcli/docker/session_linux.go` (`startedBefore`, `startSlack` = 2s)", "insight": "A flake that fails a whole family of tests in one run and none in the next points at per-machine state, not timing within the test: here the fractional second of the VM's boot. Make the comparison a pure function and pin the worst case (boot at X.9999, start 9.9ms into a tick, 1ms after notBefore); the test then fails deterministically against the old slack. Rebuilding a wall-clock time from btime + ticks loses up to 1s + 1 tick; any slack must exceed both.", "refs": ["https://github.com/ako/mxcli/actions/runs/37733157296", "https://github.com/ako/mxcli/actions/runs/37688181946"]} -{"area": "cmd/mxcli", "date": "2026-10-08", "symptom": "`mxcli test --local` had no --db-type, so on a machine without PostgreSQL it stopped with \"Error: ensuring database: no local PostgreSQL superuser available to create the role/database\" while `run --local --db-type hsqldb` served the same project. Passing the type through naively then failed with \"database not reachable at 127.0.0.1:5432\".", "cause": "The headless boot (docker.LocalAppOptions) never learned the file database: applyDefaults filled PostgreSQL's host/user/password into every config, StartLocalApp pinged that host, and testrunner hardcoded EnsureDB=true. run --local had the file-database branches in its own applyDatabaseDefaults/StartLocalRun, a second boot path that the test runner does not use. DBConfig.IsFileBased keys on the runtime spelling \"HSQLDB\", so the canonical \"hsqldb\" passed straight through also silently skipped every file-based branch.", "file": "`cmd/mxcli/docker/localapp.go` (`applyDefaults`, `prepareLocalAppDatabase`), `cmd/mxcli/testrunner/localapp_options.go` (`dbConfig`, `ResolveTestDBType`), `cmd/mxcli/cmd_test_run.go`", "insight": "Two boot paths (StartLocalRun for run --local, StartLocalApp for test --local) each own their database defaults, so a capability added to one is absent from the other until someone asks; grep both when adding a database option. Keep the _test scratch suffix for HSQLDB as well: the name selects the database files under deployment/data/database/hsqldb/, and the unsuffixed ones are the dev loop's (shared data + HSQLDB file lock). A test that asserts EnsureDB is refused must match the refusal's message: EnsureDatabase itself fails on a host without PostgreSQL, which passes an err != nil check for the wrong reason.", "refs": ["https://github.com/mendixlabs/mxcli/pull/1274", "https://github.com/mendixlabs/mxcli/issues/1292"]} -{"area": "cmd/mxcli", "date": "2026-10-08", "symptom": "Studio Pro (11.12/11.13) shows conflicts the change pane does not show, history entries with BranchName/ModelerVersion \"(unknown)\", and crashes on open (\"Unable to find 'system' property in 'system'\") after an agent committed and branched with plain git on a Team Server project", "cause": "Studio Pro keeps per-commit metadata in git notes under refs/notes/mx_metadata (written on every Studio Pro commit, pushed with it); git commits had none, so Studio Pro 11.13 back-fills a placeholder (HasModelerVersion:false) for each after every background fetch (~3 min), and that fetch force-replaces the local notes ref, discarding notes an agent wrote but did not push. Branches made with git checkout -b have no upstream (ako/mxcli#972).", "file": "cmd/mxcli/gitnote.go", "fix": "`mxcli git note` computes the note from the commit (the .mpr Unit index + mprcontents at commit and parent, canon.Equal to skip $ID-only churn, a deleted module listed once, Moved from a container change, the project root listed on a version change); the teamserver-git skill teaches server branches and pushing refs/notes/mx_metadata with the branch.", "insight": "The ground truth was in the user's own repository, not in any doc: `git log refs/notes/mx_metadata` separates Studio Pro's notes (libgit2 'git_note_create', or the git CLI in 11.13 within a second of a host-timezone commit) from agent ones, and the notes reflog shows the 3-minute fetch-and-backfill cycle. Comparing a generator against those notes commit by commit settled every rule; an ordinary commit must match exactly, and the only residual difference is upgrade commits, where Studio Pro also records names from its editing session (a rename before commit) that no diff can recover.", "refs": ["mendixlabs/mxcli#1337", "ako/mxcli#972"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-06-06-mxcli-cmd-p-c-path-app-mpr-windows-backslash-path-fails.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-06-06-mxcli-cmd-p-c-path-app-mpr-windows-backslash-path-fails.json new file mode 100644 index 0000000000..26c58b6281 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-06-06-mxcli-cmd-p-c-path-app-mpr-windows-backslash-path-fails.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-06-06", "symptom": "`mxcli -p \"C:\\\\path\\\\App.mpr\"` (Windows backslash path) fails with `unable to open database file (14)` and the echoed path is mangled (`C:\\temp` → `C:emp`, backslashes dropped); forward slashes (`-p C:/path/App.mpr`) work", "cause": "The CLI fabricates `CONNECT LOCAL ''` MDL from the flag value and re-parses it, so the lexer's `unquoteString` interprets `\\t`/`\\n`/`\\r` and escape backslashes in the path. The positional `.mdl` arg is a raw path, so it's unaffected", "file": "every `cmd/mxcli/*.go` `fmt.Sprintf(\"CONNECT LOCAL '%s'...\", projectPath)` site + `mdl/visitor/visitor_helpers.go`", "insight": "Never interpolate a raw filesystem path into MDL source. Wrap it in `visitor.QuoteString(projectPath)` (escapes `\\`→`\\\\`, `'`→`''` — the exact inverse of `unquoteString`) at every CONNECT-string site. Round-trip test in `mdl/visitor/visitor_connection_test.go`. Issue #644", "refs": ["#644"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-01-mxcli-oql-against-docker-deployed-app-returns-0-rows-tables.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-01-mxcli-oql-against-docker-deployed-app-returns-0-rows-tables.json new file mode 100644 index 0000000000..71c34b38d8 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-01-mxcli-oql-against-docker-deployed-app-returns-0-rows-tables.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-07-01", "symptom": "`mxcli oql` against a docker-deployed app returns **0 rows** for tables that clearly have data (false negative — tester concluded \"saving is broken\")", "cause": "NOT a database-config problem (that's a red herring — proven: aligning the runtime to Postgres with data still returned 0). The OQL preview servlet (`/dev/preview_execute_oql`) is registered by `com.mendix.basis.livepreview.LivePreviewRegistry` → `AppContainer.addDevelopmentServlet`, which **only mounts the servlet when the JVM system property `mendix.running.locally.by.studiopro=true` is set** (Studio Pro sets it; a deployed `bin/start` PAD doesn't). Without it the endpoint returns HTTP 200 `{\"result\":-5,\"message\":\"Action not found\"}`, which mxcli silently parsed as empty data. (Found by decompiling `com.mendix.mxruntime.jar` / `com.mendix.appcontainer.jar`.)", "file": "`cmd/mxcli/docker/templates/docker-compose.yml` (the `command`) + `cmd/mxcli/docker/oql.go` (`oqlDevError`)", "insight": "Add `-J -Dmendix.running.locally.by.studiopro=true` to the start command (each JVM opt needs its own `-J`), next to the existing `-Dmendix.live-preview=enabled` — this mounts the dev servlets and `mxcli oql` returns live data (verified end-to-end; DTAP mode is NOT required, DB config is irrelevant — the preview runs in-process against the runtime DataStore). Separately, make `oqlDevError` surface `{\"result\":<0,\"message\":…}` (not just `{\"error\":…}`) so the failure isn't swallowed as 0 rows. **Related startup bug found:** the generated `etc/Default` inherits the project's ports, which are `0` when unset → the runtime rejects port 0 and won't start; `patchRuntimePorts` (patch.go) forces `runtime.http { port = 8080 }` / `admin { port = 8090 }`. Verified with a full docker build/run/seed cycle"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-08-ce0463-mdl-created-combobox-other-platform-widget-mxcli.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-08-ce0463-mdl-created-combobox-other-platform-widget-mxcli.json new file mode 100644 index 0000000000..bec742beed --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-08-ce0463-mdl-created-combobox-other-platform-widget-mxcli.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-07-08", "symptom": "CE0463 on an MDL-created **Combobox** (or other platform widget) at `mxcli docker build/check` time — but the SAME widget passes `mx check` on a project whose installed `.mpk` matches mxcli's embedded template (Mendix 11.6 / combobox 2.5.0). NOT the old incomplete-template bug (#112, fixed — a matching-version combobox is clean)", "cause": "Widget-VERSION mismatch: mxcli emits the embedded 2.5.0-shaped PropertyTypes, but the project has a NEWER combobox (e.g. 2.8.1) that reorders/regroups properties. `augmentFromMPK` patches presence + enum values but can't restructure the baseline; `GenerateFromMPK` is less faithful still (fails even on a matching version). The designed remediation is `mx update-widgets` (docker build/check runs it before `mx check`)", "file": "`cmd/mxcli/docker/check.go` + `build.go` (`updateWidgetsPathArg`) — the update-widgets *invocation*, not the widget emission", "insight": "The real trap was that `mx update-widgets ` **crashed** (`AddProjectDirAsAllowedPath` → `Path.GetDirectoryName(\"app.mpr\")` = \"\" → `System.ArgumentNullException`) and some mx builds exit 0 after printing it, so the migration silently no-op'd and CE0463 survived. Pass an **absolute** path to update-widgets (always has a directory component). `mx check` is unaffected. Diagnosis: run the bundled `mx update-widgets ` yourself — if it throws ArgumentNullException on AddProjectDirAsAllowedPath, the path lacks a dir. Faithful multi-version widget emission is the larger fix (#529). Issue #112; repro `mdl-examples/bug-tests/112-combobox-enum-ce0463-widget-version.mdl`", "refs": ["#112", "#529"], "ce": ["CE0463"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-22-editing-themesource-main-scss-theme-web-main-scss-while.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-22-editing-themesource-main-scss-theme-web-main-scss-while.json new file mode 100644 index 0000000000..50f7b212df --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-22-editing-themesource-main-scss-theme-web-main-scss-while.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-07-22", "symptom": "Editing `themesource/**/main.scss` (or `theme/web/main.scss`) while `mxcli run --local` serves on `:8080` keeps showing **old styles** — reads exactly like a stale compiled-CSS cache. `rm -rf theme-cache/ .mendix-cache/ deployment/` \"fixes\" it only because a restart came with it", "cause": "THREE distinct causes, none a CSS cache: (1) **no `--watch` = no watcher at all** — `mxbuild --serve` only rebuilds on a `/build` request (startup, or a watch tick), so a save changes nothing; (2) **stale process silently adopted** — a leftover serve/runtime on the ports answers the startup readiness probes (`waitReady`/`waitAdminReady` only check \"port answers\"), so a new run attaches to the OLD process and its own child is torn down by `defer`; a backgrounded `run --local` whose wrapping shell exited non-zero dies while its serve+runtime keep serving; (3) the theme source was **watched by nothing** — the `--watch` signal was model-only (`.mpr`+`mprcontents/`). The incremental theme step itself is FINE: one `/build` after an scss **content** edit does rewrite `theme-cache/web/theme.compiled.css` (verified), so there is no cache to clear", "file": "`cmd/mxcli/docker/runlocal.go` — `checkTargetPortsFree` (guard), `themeSourceMTime`/`sourceMTime` (watch signal), `watchAndApply` (generation log)", "insight": "(2) Refuse to boot when `:8080/:8090/:6543` already answer, with an actionable message (never auto-kill — user's call). (3) Add `theme/`+`themesource/` scss/css/js to the `--watch` mtime-poll signal (`sourceMTime` = max(model, theme)); poll-based so it's container-safe (unlike the rollup chokidar/inotify web-client watcher). Log a build-generation counter (`build #2`) so \"did it take?\" is answerable. Docs: `docs-site/src/tools/run-local.md` + skill `run-local.md` — \"SCSS needs a rebuild (`--watch` or clean restart), never a cache-clear; kill the old serve/runtime first\". Tests `TestThemeSourceMTime_WatchesThemeAndThemesource`, `TestCheckTargetPortsFree`", "refs": ["#2"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-25-mxcli-run-warm-loop-ergonomics-from-sudoku-findings-17.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-25-mxcli-run-warm-loop-ergonomics-from-sudoku-findings-17.json new file mode 100644 index 0000000000..33384673d0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-25-mxcli-run-warm-loop-ergonomics-from-sudoku-findings-17.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-07-25", "symptom": "`mxcli run` warm-loop ergonomics from Sudoku findings: (a #17) a **relative** `-p` fails with MxBuild's raw \"should be an absolute path\" + a Windows JSON sample; (b #15/#23) a **build failure in the watch loop** (incl. a SCSS compile error like `Expected expression. _x.scss 180:35`) printed only the generic `build failed: `, swallowing the real detail", "cause": "(a) `cmd_run.go` passed `-p` straight through without `filepath.Abs`; (b) the watch path at `runlocal.go` printed only `build.Message` while `build.Raw` (the full serve `/build` body, which the cold-build path already prints) held the compiler/model detail", "file": "`cmd/mxcli/cmd_run.go` + `cmd/mxcli/docker/runlocal.go` (watch build-failed branch)", "insight": "(a) `projectPath, _ = filepath.Abs(projectPath)` after reading the flag; (b) also print `strings.TrimSpace(string(build.Raw))` (indented) when it differs from `build.Message`, matching the cold path. **Diagnosis pattern**: when a dev-loop error is unhelpfully generic, check whether a `Raw`/full-body field is already captured and just not printed on that code path", "refs": ["#15", "#17", "#23"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-26-follow-up-above-25-re-test-run-local-writes-runtime-log-but.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-26-follow-up-above-25-re-test-run-local-writes-runtime-log-but.json new file mode 100644 index 0000000000..c55c5b3d64 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-26-follow-up-above-25-re-test-run-local-writes-runtime-log-but.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-07-26", "symptom": "Follow-up to the above (#25 re-test): `run --local` writes `runtime.log` but it stays **nearly empty** — the JVM tee captures startup/JVM output only; **application** logs (microflow `LOG`, server-side exception stack traces) never reach stdout, so a page-action error still can't be diagnosed", "cause": "A standalone runtime (launched via `runtimelauncher.jar`) attaches **no log subscriber** by default — unlike a Studio Pro / m2ee run, which calls `create_log_subscriber` **after** start. Mendix application logs flow to log *subscribers*, not stdout, so with none attached they go nowhere", "file": "`cmd/mxcli/docker/runtime_controller.go` (`RuntimeController.LogSubscriberFile`/`Stdout`, `attachFileLogSubscriber`, called at the end of `Start`) + `cmd/mxcli/docker/localboot.go` (`StartLocalRuntime` sets `ctrl.LogSubscriberFile` to the abs runtime-log path)", "insight": "After a successful `start` (and on every restart's `Start`, since each fresh JVM has no subscriber), call the `create_log_subscriber` admin action with `{type:\"file\", name:\"mxcli-run-local\", autosubscribe:\"INFO\", filename:, max_size:1GiB, max_rotate:0}`. **`max_rotate:0` is load-bearing**: the JVM stdout tee holds an fd on the same file, and a rotate-rename would detach it. Best-effort (a logging failure must not fail an up runtime — warn to Stdout instead). Pass an **absolute** path (the runtime's cwd is `/runtime`, not mxcli's). Tests `TestStart_AttachesLogSubscriber`, `TestStart_NoLogSubscriberWhenUnset`, `TestStart_LogSubscriberFailureNonFatal`. Findings #25 (round 2)", "refs": ["#25"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-26-follow-up-round-2-25-re-test-subscriber-registered-but.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-26-follow-up-round-2-25-re-test-subscriber-registered-but.json new file mode 100644 index 0000000000..b94fea51c6 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-26-follow-up-round-2-25-re-test-subscriber-registered-but.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-07-26", "symptom": "Follow-up to round 2 (#25 re-test): the subscriber is registered but `runtime.log` **still** holds only the 4 JVM-banner lines — a probe microflow's `log info/warning/error` and a forced runtime exception produce **zero** lines. Isolation test: boot+subscriber → 0 probe lines; then call `start_logging` → 6 lines", "cause": "A standalone runtime boots with logging **not started**, so a registered subscriber sits **inert** — nothing is delivered until the logging subsystem is activated. `create_log_subscriber` alone is necessary but not sufficient", "file": "`cmd/mxcli/docker/runtime_controller.go` (`configureRuntimeLogging` — renamed from `attachFileLogSubscriber`; now also calls `start_logging`)", "insight": "After `create_log_subscriber`, call `CallM2EE(c.opts, \"start_logging\", nil)` in the same step (order: create subscriber → start_logging). Treat an \"already started\" response as success (`start` re-runs on the DB-update retry / restart paths on a still-running JVM). Still best-effort (warn to Stdout, never fail an up runtime). Test `TestStart_StartLoggingAlreadyStartedIsSuccess`; `TestStart_AttachesLogSubscriber` now asserts the `[start, create_log_subscriber, start_logging]` sequence. **Diagnosis pattern**: when a registered sink receives nothing, check whether the subsystem that feeds it is even *running* — registration ≠ activation. Findings #25 (round 3)", "refs": ["#25"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-26-mxcli-run-local-page-action-throws-browser-shows-generic.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-26-mxcli-run-local-page-action-throws-browser-shows-generic.json new file mode 100644 index 0000000000..cc1ce6a43c --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-26-mxcli-run-local-page-action-throws-browser-shows-generic.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-07-26", "symptom": "`mxcli run --local`: when a page action throws, the browser shows the generic Mendix error dialog and there is nothing to correlate it against — the runtime's own stdout/stderr (server stack trace, microflow `LOG` output) is swallowed, so a server-side bug can't be told apart from a client one", "cause": "The runtime JVM was spawned with `cmd.Stdout=log; cmd.Stderr=log` where `log` is an in-memory `syncBuffer` surfaced only on a *startup* failure; during normal operation it goes nowhere on disk", "file": "`cmd/mxcli/docker/localboot.go` (`spawnAndConfigure`, `openRuntimeLog`, `LocalRuntime.logFile`, `LocalRuntimeOptions.RuntimeLogPath`) + `cmd/mxcli/docker/runlocal.go` (default `/.mxcli/runtime.log`) + `cmd/mxcli/cmd_run.go` (`--runtime-log`)", "insight": "Tee the JVM's stdout+stderr to `/.mxcli/runtime.log` via `io.MultiWriter(log, file)` (the in-memory buffer still backs startup-error reporting). Append across restarts with a `=== runtime start … ===` marker; close the handle on Stop/reopen. Default on; `--runtime-log ` relocates, `-` disables. Print the path at boot. Test `TestOpenRuntimeLog`. Findings #25", "refs": ["#25"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-30-mxcli-test-leaves-project-mutated-security-level-changed.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-30-mxcli-test-leaves-project-mutated-security-level-changed.json new file mode 100644 index 0000000000..d69883ee38 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-30-mxcli-test-leaves-project-mutated-security-level-changed.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-07-30", "symptom": "`mxcli test` leaves the project mutated: Security Level changed, after-startup pointing at the deleted `MxTest.TestRunner`, and only a `Warning:` line about it. Or: an empty `MxTest` module accumulates after every run", "cause": "Three defects in one teardown. (a) `getAfterStartup` trimmed quotes *before* trailing punctuation, so a DESCRIBE SETTINGS line ending in `,` yielded `Module.Flow',` and the restore statement was unparseable; (b) cleanup dropped only the microflow, not the module it created; (c) the Security Level was forced OFF and restored to a hardcoded PRODUCTION", "file": "`cmd/mxcli/testrunner/runner.go` (`parseSettingValue`, `quoteMDLString`, `projectState`, `setupCommands`, `cleanupCommands`)", "insight": "Strip trailing `,;` before unquoting; re-emit via `quoteMDLString` (doubling embedded quotes, never backslashes); capture a `projectState` before the first mutation and restore from it; drop the module only when the run created it (a pre-existing `MxTest` is the user's); leave Security Level alone entirely; return cleanup errors instead of printing warnings, and fail the run when they occur. Command lists are pure functions so the restore is testable without a project or Docker. Issues #802/#803/#804", "refs": ["#802", "#803", "#804"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-31-mxcli-docker-check-build-bare-mx-update-widgets-silently.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-31-mxcli-docker-check-build-bare-mx-update-widgets-silently.json new file mode 100644 index 0000000000..9237a0b75e --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-31-mxcli-docker-check-build-bare-mx-update-widgets-silently.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-07-31", "symptom": "`mxcli docker check`/`build` (or a bare `mx update-widgets`) **silently deletes `mprcontents/`** and rewrites an MPRv2 project into single-file v1 — `check` reports **0 errors** and looks successful, but the git working tree diverges from tracked files, a running `mxcli run --local` loop breaks (it watches `mprcontents/`), and Studio Pro may crash on open (`LibGit2RepositoryProvider.WriteBaseFile`). Triggered by following the CE0463 remediation on a `mxcli new` (always v2) project", "cause": "The pre-check `mx update-widgets` step (run to suppress false CE0463) **performs the conversion**: it inlines every unit into the `.mpr` (`Unit.Contents` column) and deletes `mprcontents/`. The `check` itself is read-only; `update-widgets` is the mutator. docker check/build invoked it with no storage-format protection", "file": "`cmd/mxcli/docker/update_widgets.go` (`runUpdateWidgets` / `snapshotStorageFormat`) — call sites in `check.go` and `build.go`", "insight": "Snapshot `.mpr` + `mprcontents/` to a temp dir before `update-widgets`, `defer restore()` after the check (restore removes the post-conversion single-file `.mpr` residue and puts the v2 tree back); MPRv1 projects need no protection. The check still runs against the widget-normalized model, so CE0463 stays suppressed — only the on-disk format is preserved. **Never tell an agent to run bare `mx update-widgets` on a v2 project** — the synced skills (`create-page.md`, `custom-widgets.md`, `migrate-design-prototype.md`, `download-marketplace-content.md`) + dev `debug-bson.md` route to `mxcli docker check`/`build` (v2-safe) instead. **Fix the operation, not the call site**: PR #764 wrapped the invocation inside `Check` only, and `Build` had a second bare copy — so `docker build`/`run`/`reload` kept converting projects for another 40 issues, until #808. The snapshot now lives in `runUpdateWidgets`, which is the only place that may exec `update-widgets`; grep for `\"update-widgets\"` should return exactly one hit. When a mutating external step is guarded, put the guard in a function that also *performs* the step, so a new caller cannot get it wrong. Issues #763 / PR #764, #808", "refs": ["#763", "#764", "#808"], "ce": ["CE0463"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-31-mxcli-oql-p-app-mpr-fails-against-mxcli-run-local-app-first.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-31-mxcli-oql-p-app-mpr-fails-against-mxcli-run-local-app-first.json new file mode 100644 index 0000000000..6ff3375269 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-07-31-mxcli-oql-p-app-mpr-fails-against-mxcli-run-local-app-first.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-07-31", "symptom": "`mxcli oql -p app.mpr \"…\"` fails against a `mxcli run --local` app — first `admin password required`, then (with the password) `OQL error: Action not found.` and 0 rows — even though the same query works under `mxcli docker up`", "cause": "Two-part gap. (a) The local runtime booted the JVM with no system properties, so `/dev/preview_execute_oql` was never mounted (docker mode passes `-Dmendix.live-preview=enabled -Dmendix.running.locally.by.studiopro=true` via docker-compose; the local boot did not). (b) `run --local` never printed or wrote the admin password, and `mxcli oql` errored instead of defaulting to it", "file": "`cmd/mxcli/docker/localboot.go` (`LocalRuntimeOptions.jvmArgs` — new) + `cmd/mxcli/docker/m2ee.go` (`resolveM2EEDefaults` token fallback) + `cmd/mxcli/docker/oql.go` (Action-not-found hint) + `cmd/mxcli/docker/runlocal.go` (banner)", "insight": "Always pass the two live-preview `-D` flags in `jvmArgs` (run --local is always DTAPMode=D, matching docker); default the oql admin token to `defaultLocalAdminPass` when nothing else supplies it (admin API is loopback-only); branch the \"Action not found\" hint to cover the `--local` case; print a `Query data: mxcli oql …` line in the run banner. Tests `TestJVMArgs`, `TestResolveM2EEDefaults_Defaults`. traceops #36", "refs": ["#36"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-01-mxcli-new-version-x-prints-resolving-mxbuild-x-then-produces.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-01-mxcli-new-version-x-prints-resolving-mxbuild-x-then-produces.json new file mode 100644 index 0000000000..bc9963fa0c --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-01-mxcli-new-version-x-prints-resolving-mxbuild-x-then-produces.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-01", "symptom": "`mxcli new --version X` prints \"Resolving MxBuild X...\" and then produces a project at a **different** Mendix version — silently. Every later step (init, mxbuild, runtime, `run --local`) follows the wrong version", "cause": "`ResolveMxForNewProject` delegated to `ResolveMxForVersion`, whose last resort is `AnyCachedMxPath()` — *any* cached mx, of any version. That fallback is fine when the project already exists and its version is a preference; for `new` the requested version **is** the output, because `mx create-project` stamps the project with the version of the binary that ran it", "file": "`cmd/mxcli/docker/check.go` (`localMxForVersion`, `ResolveMxForNewProject`) + `cmd/mxcli/cmd_new.go` (postcondition)", "insight": "Resolve **exactly** the requested version for `new` (exact Studio Pro install → exact versioned install path → exact download cache; **not** PATH, which carries no version guarantee), and download otherwise. Then check the postcondition: reopen the created `.mpr`, compare `ProductVersion` to `--version`, and fail loudly on a mismatch — resolution bugs are invisible without it. **Generalisable**: when a flag names the version/identity of the artifact being produced, a \"close enough\" local substitute is never valid, and the produced artifact should be verified against the request rather than the resolution trusted. Found while reproducing #812 in a browser — cost a full project rebuild before it was noticed", "refs": ["#812"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-mxcli-oql-silently-omits-whole-column-select-b-renders-only.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-mxcli-oql-silently-omits-whole-column-select-b-renders-only.json new file mode 100644 index 0000000000..8e4a74868e --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-mxcli-oql-silently-omits-whole-column-select-b-renders-only.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-06", "symptom": "`mxcli oql` silently omits a whole column: a `select A, B …` renders only `A`, and the JSON output has no `B` key at all — no error, no empty column. Reproduces whenever the value of `B` is **null in the first row**", "cause": "The runtime omits a null-valued column from a row's JSON object entirely, and `parseOQLFeedback` took the column list from `extractColumnOrder(rows[0])` — one row, chosen for its key *order*. Every later row was then projected onto that short list, so the column vanished from the result rather than showing NULLs", "file": "`cmd/mxcli/docker/oql.go` (`parseOQLFeedback`, `mergeColumnOrder`, `hasOnlyKnownKeys`)", "insight": "Union the keys of **all** rows, inserting each new key directly after the last key already known rather than appending — merging `[A, C]` with `[A, B, C]` must give `[A, B, C]`, not `[A, C, B]`, or a column that is null early in the result set jumps to the end of the table. The per-row re-scan is skipped when a row carries no unseen key, so the uniform case still costs one length check. **Generalisable**: any \"take the shape from the first element\" over a sparse encoding is a silent-wrong-answer bug, not a formatting bug — the output looks complete. Tests `TestParseOQLFeedback_ColumnUnionAcrossRows`, `TestMergeColumnOrder`; proven by stubbing the union back to first-row-only and watching the reported symptom return. sudoku #39, first half", "refs": ["#39"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-mxcli-test-cannot-run-all-container-without-docker-daemon.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-mxcli-test-cannot-run-all-container-without-docker-daemon.json new file mode 100644 index 0000000000..647aa1aa97 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-mxcli-test-cannot-run-all-container-without-docker-daemon.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-06", "symptom": "`mxcli test` cannot run at all in a container without a Docker daemon — parsing, runner generation, model injection and the **entire mxbuild build** succeed natively, then `docker up` fails with \"failed to connect to the docker API at unix:///var/run/docker.sock\". So microflow tests are unavailable in exactly the environment mxcli targets (Claude Code web containers ship `/usr/bin/docker` with no daemon)", "cause": "Only one step of the run was containerised — start the runtime and read its log — but it was wired directly to `docker compose`, with no seam for another way to run the app. `run --local` had all three pieces already (boot a standalone runtime, tee its log, restore)", "file": "`cmd/mxcli/docker/localapp.go` (new `StartLocalApp`), `cmd/mxcli/testrunner/runner_local.go` (new), `cmd/mxcli/testrunner/runner.go` (docker path extracted to `runDockerAndCapture`), `cmd/mxcli/cmd_test_run.go` + `main.go` (`--local`)", "insight": "Give the run a seam — `runLocalAndCapture` / `runDockerAndCapture` — so both modes share parse/inject/parse-results/cleanup and differ only in how the app is started. **Two traps, both found by running it rather than reasoning about it.** (a) The runner reports via an **after-startup** microflow, so its LOG output happens DURING the start action, before the runtime's log subscriber attaches; registering the subscriber early is not possible (the runtime answers `LoggingException` pre-start). What actually carries the output is the JVM console tee, live from spawn — verified by A/B running with the early attach removed. (b) A **failing** test makes the runner return false, which makes the after-startup action fail, which makes `start` return an error — the first version reported that as a broken run and dumped a stack trace instead of the test report. A failed boot whose log shows a verdict is a normal outcome. Local runs use their own ports (8081/8091) and a `_test` database so a `run --local` dev loop can keep serving. Verified end-to-end in a daemon-less container: 2 passed; then 1 passed / 1 failed with exit 1, project restored. sudoku #41", "refs": ["#41"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-scss-written-themesource-name-web-main-scss-never-reaches.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-scss-written-themesource-name-web-main-scss-never-reaches.json new file mode 100644 index 0000000000..5840f8ae0c --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-scss-written-themesource-name-web-main-scss-never-reaches.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-06", "symptom": "SCSS written to **`themesource//web/main.scss` never reaches the app** — no error, no warning, the build succeeds and the rules are simply absent from `theme-cache/web/theme.compiled.css`. Looks exactly like an SCSS cache problem, so the usual reflex (`rm -rf theme-cache/`) wastes the session", "cause": "A theme source folder is only compiled when `` matches a **real module in the model**. mxbuild walks the model's modules and pulls each one's `themesource//web/main.scss`; it never globs the `themesource/` directory, so an invented folder (`themesource/my_theme/`) is silently skipped. Verified on 11.13: a probe rule in `themesource/myfirstmodule/` compiled, the identical rule in `themesource/mxcli_theme/` did not", "file": "`cmd/mxcli/theme/theme.go` (package doc records the compile order); the target paths live in `cmd/mxcli/theme/assets//files/`", "insight": "Put app-level styling in **`theme/web/`**, not in an invented theme source folder: `theme/web/main.scss` is compiled **last** — after Atlas Core *and* after every module theme source — so a partial imported from it overrides any Atlas rule without `!important`. Use a module's theme source only when the styling genuinely belongs to that module (it exports with the `.mpk`). **Generalisable — the shape to look for**: when CSS \"doesn't apply\", first prove the file is *compiled at all* (grep a unique probe selector in `theme-cache/web/theme.compiled.css`) before debugging specificity or caches — absent and overridden look identical in the browser. Note also that `theme/web/custom-variables.scss` is imported once **per module**, so it must hold declarations only; a rule there is emitted N times"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-text-painted-atlas-topbar-widget-invisible-dark-theme.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-text-painted-atlas-topbar-widget-invisible-dark-theme.json new file mode 100644 index 0000000000..08f349970f --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-06-text-painted-atlas-topbar-widget-invisible-dark-theme.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-06", "symptom": "Text painted by an **Atlas topbar widget is invisible in a dark theme** — the language selector measures ~1.13:1 contrast, glyph pixels spanning 4 luminance values out of 255. A theme override exists and *names the right element*, so it looks handled", "cause": "Two separate mistakes stacked. (1) **Specificity**: Atlas's own rule is `.navbar-brand .widget-language-selector .current-language-text` at (0,3,0); a bare `.current-language-text` at (0,1,0) never wins, and only appears to on layouts that do not nest the selector under `.navbar-brand`. (2) **Wrong value**: `color: inherit` inherits *body ink*, which is dark, while the rail is dark in both palettes — so even at the winning specificity it measures 1.00:1. Atlas paints from `--bg-color-secondary` with a `#fff` fallback because it assumes a dark rail", "file": "`cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-atlas-map.scss` (the \"Atlas fixups\" block)", "insight": "Re-declare **Atlas's own selector shape** so the weights match and source order decides, and resolve the colour through the rail token (`var(--mxt-rail-ink-active, var(--mxt-rail-ink))`) rather than `inherit`. List the bare and the `.navbar-brand`-nested selectors together — each is matched at its own specificity, so one rule covers both layouts. **Generalisable — the shape to look for**: a guard that names the right element is not evidence it applies. Read the *winning* declaration (`CSS.getMatchedStylesForNode` in DevTools, or the computed value) instead of the one you wrote. **Measure contrast, not colour**: reading `getComputedStyle(el).color` once and seeing a plausible value proves nothing — compute the WCAG ratio against the first non-transparent ancestor background, which is what turns \"looks fine\" into 1.13 vs 19.47. Reported from the RssReader test build; tests in `cmd/mxcli/theme/theme_test.go`, verified in a browser at 17.79:1 light / 19.47:1 dark"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-alter-module-x-add-jar-dependency-succeeds-list-jar.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-alter-module-x-add-jar-dependency-succeeds-list-jar.json new file mode 100644 index 0000000000..c6894b00d8 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-alter-module-x-add-jar-dependency-succeeds-list-jar.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "raw": "| `ALTER MODULE X ADD JAR DEPENDENCY (…)` succeeds, `list jar dependencies` reports it, the build is **green** — and the runtime throws `SQLException: No JDBC driver found in app for URL`. `deployment/build.gradle` has no dependencies block and `find deployment -iname '**'` returns nothing | Not a bad write. Declaring and resolving are **separate steps**: the model records the coordinate, and `mx sync-java-dependencies ` is what downloads it into `vendorlib/`. Studio Pro runs that when you edit Module Settings; nothing headless was running it. Confirmed on 11.12.1 — a full `mxbuild --target=deploy` resolves nothing, and the sync command then fetches the jar | `cmd/mxcli/docker/javadeps.go` (`SyncJavaDependencies`, `UnvendoredJarDependencies`), `cmd/mxcli/cmd_sync_java_deps.go` (`mxcli sync-java-deps [--check]`), `cmd/mxcli/docker/runlocal.go` (vendors before boot), `mdl/executor/cmd_modules.go` (`warnUnvendoredJarDependencies`) | **How to find the missing step**: the reporter's open question was \"does mxbuild skip Maven resolution, or does mxcli write it somewhere MxBuild cannot read?\" — neither. `strings mx.dll | grep -i dependenc` surfaced `ISyncJavaDependenciesRunner`/`SkipManagedDependencySync`, and `mx --help` listed `sync-java-dependencies`. When a model-level write \"works\" but the artefact never appears, check whether the **toolset** has a separate command for it before suspecting the write. Wired at three levels so the gap cannot stay silent: the executor says so the moment it writes an unvendored coordinate, `run --local` resolves it before boot, and `--check` exits non-zero as a build gate. Resolution needs network, so every call site is best-effort with an actionable message. Tests `cmd/mxcli/docker/javadeps_test.go`. mxcli-formula1 #12 |", "refs": ["#12"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-contrast-low-not-everything-uses-dark-theme-switching-theme.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-contrast-low-not-everything-uses-dark-theme-switching-theme.json new file mode 100644 index 0000000000..b4a24a6581 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-contrast-low-not-everything-uses-dark-theme-switching-theme.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "\"Contrast is low and not everything uses the dark theme\" — and switching theme does not help, because `signal`, `ledger` and `console` all render the same defects. The worst of it is the **login page**: Atlas's stock photograph fills half the viewport on a dark app, and the Sign in button is green while the app's primary button is the brand colour", "cause": "The login page is served from `theme/web/login.html`, which loads the SAME compiled theme CSS (`{{themecss}}`) — so it IS themeable, but nothing themed it. Two Atlas rules do the damage: `.loginpage-image` layers a brand-tinted gradient over `url(\"./resources/work-do-more.jpeg\")`, and the submit button is `.btn-success`, so it follows the **success** colour rather than the brand. Separately, `--link-color` was mapped straight to the brand, and console's light-variant teal is 3.74:1 on white — under AA for body text", "file": "`cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-atlas-map.scss` (login block + `--mxt-link` indirection, all three copies stay byte-identical), `cmd/mxcli/theme/assets/console/.../_mxcli-console.scss` (light `--mxt-link`)", "insight": "Replace the photo with a token-built gradient and point the login button at the brand; give link TEXT its own `--mxt-link` token defaulting to the brand, because a link needs 4.5:1 as text while a brand used as a button FILL only needs 3:1 plus contrast against its own ink — darkening the brand everywhere would have been the wrong lever. Console light gets `#0f766e` (5.47 / 5.10 / 4.92 against surface / ground / surface-alt). **The project's own `theme/web/logo.png` is deliberately left alone** — it is the app's asset to replace, and hiding it would strip a real logo from apps that have one. **Verification boundary, stated plainly**: verified at the compiled-CSS layer (the overriding `.loginpage-image` is last and carries no photo; the button and link declarations resolve), NOT in a browser — raising the scratch project's security level to serve a login page surfaced pre-existing model errors there that block deploy. The `#999` empty-state label could not be reproduced: every `#999` in the compiled CSS is a Bootstrap default (popover arrows, modal border, print styles), so that one needs the reporter's app. mxcli-todo #19", "refs": ["#0", "#19", "#999"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-fresh-clone-project-created-mxcli-new-goes-dirty-first-time.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-fresh-clone-project-created-mxcli-new-goes-dirty-first-time.json new file mode 100644 index 0000000000..a7f91f26fa --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-fresh-clone-project-created-mxcli-new-goes-dirty-first-time.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "A fresh clone of a project created by `mxcli new` goes dirty the first time anyone builds it: ~50 **tracked** files modified that nobody edited — every `javascriptsource/*/actions/*.js` gains a banner, `import { Big } from \"big.js\"` and `export async function`, plus the matching `javasource` stubs. In a cloud session with a stop-hook git check it reads as \"uncommitted changes\" at the end of clean work", "cause": "The template ships the generated action stubs in a slightly older shape and MxBuild rewrites them all on the first build. `mx check` does **not** — only a build does — so nothing before the first `run --local` could reveal it, which is after the user has already committed", "file": "`cmd/mxcli/docker/settle.go` (`SettleGeneratedSources`), `cmd/mxcli/cmd_new.go` (step 5/6, `--skip-build`), `cmd/mxcli/init.go` (`/theme-cache/` in the generated ignore list)", "insight": "Fix the *timing*, not the content: run the build while the project is still being created, so the settled form lands in the first commit. Do **not** reimplement the rewrite — it is mxbuild's generator and version-specific; run the real thing. Best-effort by contract (no JDK, no mxbuild, failed build → warning, never a failed creation), because a settled tree is a nicety and a usable project is the deliverable. The other half is gitignore: `theme-cache/` is a cache and says so. A/B on 11.12.1, both git-init'd then built: `--skip-build` → 50 dirty files, default → **0**. Tests `cmd/mxcli/docker/settle_test.go`. mxcli-todo #7", "refs": ["#7"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-login-known-correct-starts-failing-run-local-page-says-only.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-login-known-correct-starts-failing-run-local-page-says-only.json new file mode 100644 index 0000000000..a7a4652df2 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-login-known-correct-starts-failing-run-local-page-says-only.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "A login that is known to be correct starts failing on `run --local` — the page says only **\"Sign in failed\"**, as if the password were wrong. `--screenshot-user` silently produces screenshots of the login page instead of the requested page", "cause": "The local runtime is unlicensed, and an unlicensed runtime caps concurrent sessions. Past the cap it refuses the sign-in and logs `Maximum number of sessions exceeded! (You are currently using a trial license)` to `.mxcli/runtime.log` only; the page reports the refusal identically to a bad password. mxcli's own login helper never noticed either — it filled the form, waited, and saved whatever session it had", "file": "`cmd/mxcli/docker/screenshot_login.go` (`loginScript` failure detection, `loginFailureHint`, `readLogTail`), `cmd/mxcli/docker/runlocal.go` (passes `RuntimeLogPath`), `.claude/skills/mendix/run-local.md`", "insight": "Not an mxcli defect in the model layer — but mxcli held both halves and joined neither. Mendix answers a rejected sign-in by re-rendering the same form, so **the username field still being present after the click is the signal** that login did not complete; on that signal read the tail of the runtime log and name the cap. **Generalisable**: when a browser-driven helper \"succeeds\" with a degraded result, look for a server-side log that already says why — the page is often the least informative witness. Sessions are released by restarting `run --local`; a browser-driving script should sign out at the end, or the fifth or sixth run is the one that fails and it looks like a regression. Tests `TestLoginFailureHint`, `TestReadLogTail`. mxcli-todo #16", "refs": ["#16"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-exec-p-app-mpr-eof-eof-fails-error-reading-file-open.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-exec-p-app-mpr-eof-eof-fails-error-reading-file-open.json new file mode 100644 index 0000000000..aa3a85425e --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-exec-p-app-mpr-eof-eof-fails-error-reading-file-open.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli exec -p app.mpr - <<'EOF' … EOF` fails with `Error reading file: open -: no such file or directory` — `-` is taken literally as a filename, so MDL cannot be piped or written as a heredoc and every ad-hoc script needs a temp file first", "cause": "`exec` (and `check`) called `os.ReadFile(path)` directly, with no case for the conventional stdin spelling", "file": "`cmd/mxcli/mdlsource.go` (new `readMDLSource`, `mdlSourceLabel`), `cmd/mxcli/cmd_exec.go`, `cmd/mxcli/cmd_check.go`", "insight": "One helper both commands share, so `check` gained the same spelling rather than only the reported one; `check` reports the source as `` instead of a bare `-`. Verified live: a heredoc through `exec` and a pipe through `check` both run. Tests `cmd/mxcli/mdlsource_test.go`. mxcli-todo #5", "refs": ["#5"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-init-run-from-solution-root-several-app-folders-no-mpr.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-init-run-from-solution-root-several-app-folders-no-mpr.json new file mode 100644 index 0000000000..d0d1913503 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-init-run-from-solution-root-several-app-folders-no-mpr.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli init` run from a solution root (several app folders, no `.mpr` at the root) reports success and writes tooling that points at `project.mpr` — a file that does not exist. Nobody is told which project it picked, because it did not pick one", "cause": "`findMprFile` looks only in the target directory; an empty result fell through to a hardcoded `\"project.mpr\"` default and initialisation continued as if that were a real project", "file": "`cmd/mxcli/init.go` (`findMprFilesInSubdirs` + the three-way branch on the candidate count)", "insight": "Look one level down, then branch on the **count**: 0 → warn that generated paths will be placeholders; 1 → announce the project and initialise **that** directory; 2+ → refuse, list them, and print the exact command naming one. One level only — a Mendix app keeps its `.mpr` at its own root, and walking deeper starts finding deployment copies and backups. Candidates are sorted so the refusal and its suggested command are stable rather than directory-order dependent. **Generalisable**: a \"sensible default\" that names a file which does not exist is not a default, it is a silent wrong answer — count the candidates and let the count choose the behaviour. Tests `cmd/mxcli/init_discover_test.go`. mxcli-formula1 #3", "refs": ["#3"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-syntax-documents-spellings-parser-rejects-so-agent.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-syntax-documents-spellings-parser-rejects-so-agent.json new file mode 100644 index 0000000000..20ca948188 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-syntax-documents-spellings-parser-rejects-so-agent.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli syntax` documents spellings the parser rejects, so an agent following the reference writes MDL that fails — `TEXTBOX … (Binds: Attr)` (\"'Binds:' is no longer supported, use 'Attribute:' instead\") and `DataSource: MICROFLOW Module.MF()` (a zero-arg microflow datasource takes NO parens, unlike RETRIEVE/CALL)", "cause": "Nothing checks the `syntax` corpus against the parser. `make check-skill-mdl` validates MDL blocks in the skills and the docs site, but the `Syntax`/`Example` strings in `cmd/mxcli/syntax/*.go` are not covered, so a retired spelling can sit there indefinitely", "file": "`cmd/mxcli/syntax/features_page.go` (10 × `Binds:` → `Attribute:`, the datasource parens), `cmd/mxcli/syntax/retired_spellings_test.go` (new guard)", "insight": "Fix the text **and** pin it: a table-driven test fails if a retired spelling reappears in any topic's Syntax or Example. It is a spelling guard rather than a parse — the snippets are fragments (a DATAVIEW body, a property line) that do not stand alone as statements, so they cannot just be fed to the parser. Proven by reintroducing `Binds:` and watching the test name the topic and field. **A third claim in the same report did not reproduce**: `CONTAINER (OnClick: SHOW_PAGE M.P(Param: $currentObject))` parses fine on current main, so only the two verified ones were changed. mxcli-todo #8", "refs": ["#8"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-test-attach-fails-reload-model-failed-authentication.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-test-attach-fails-reload-model-failed-authentication.json new file mode 100644 index 0000000000..de12c2bdae --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-test-attach-fails-reload-model-failed-authentication.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli test --attach` fails with `reload_model failed: Authentication failed.` — after the test microflows have already been injected into the project", "cause": "The M2EE admin API and the test endpoint are **different secrets**. `attach` built its `RuntimeController` with `M2EEOptions{Token: hs.Token}` — the endpoint token — instead of the runtime's admin password", "file": "`cmd/mxcli/testrunner/runner_attach.go` (`attach`), `cmd/mxcli/testrunner/handshake.go` (`Handshake`)", "insight": "Carry `AdminPass` in the handshake alongside `Token` and pass that to `M2EEOptions`. The hosting `run --local` publishes it via `docker.LocalAppInfo` (the resolved value, not the package default, so a `--admin-pass` override still works). Whenever one process drives another's M2EE API, check which credential is being passed — `defaultLocalAdminPass` and any app-level token are unrelated"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-test-local-any-other-startlocalapp-caller-fails.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-test-local-any-other-startlocalapp-caller-fails.json new file mode 100644 index 0000000000..540e4bc597 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-test-local-any-other-startlocalapp-caller-fails.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli test … --local` (or any other `StartLocalApp` caller) fails with MxBuild's `the project file path should be an absolute path`, followed by a page of Windows sample requests, whenever `-p` is given a **relative** path", "cause": "`ServeServer.Build` forwarded `ProjectFilePath` verbatim. `mxcli run` had learned to absolutize at the CLI layer (findings #17), but that fix lived in `cmd_run.go`, not in the code that talks to MxBuild — so the next caller re-hit it", "file": "`cmd/mxcli/docker/mxserve.go` (`ServeServer.Build`)", "insight": "Absolutize `req.ProjectFilePath` in `Build` itself, the single place that talks to MxBuild, so no future caller can miss it; also resolve `LocalAppOptions.ProjectPath` in `applyDefaults` so `DeployDir` and the runtime log path are not derived from a relative value. Test by pointing a `ServeServer` at an `httptest` fake and asserting on the request body — the CLI-layer fix cannot be tested that way, which is part of why it did not generalise", "refs": ["#17"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-test-tests-p-app-app-mpr-fails-no-such-file-directory.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-test-tests-p-app-app-mpr-fails-no-such-file-directory.json new file mode 100644 index 0000000000..7189932661 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-mxcli-test-tests-p-app-app-mpr-fails-no-such-file-directory.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "`mxcli test tests/ -p app/App.mpr` fails with \"no such file or directory\" for a `tests/` that sits right next to the `.mpr`", "cause": "Test paths resolved against the process CWD only. Defensible in isolation, but mxcli otherwise encourages naming the project (`-p`) rather than standing in its directory, and project auto-discovery searches outward — so the two conventions collide and the failure looks like a missing directory", "file": "`cmd/mxcli/cmd_test_run.go` (`resolveTestPaths`)", "insight": "Fall back to project-relative **only when the CWD-relative path does not exist**: a `tests/` in both places must resolve to the one the user is standing in, since silently preferring the project's copy would run the wrong suite. A path that exists in neither is passed through unchanged so the error names what was typed, not a rewritten path the user never mentioned. Tests `cmd/mxcli/cmd_test_run_paths_test.go`. mxcli-formula1 #13", "refs": ["#13"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-sessionstart-hook-mxcli-init-writes-cannot-survive-idle-reap.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-sessionstart-hook-mxcli-init-writes-cannot-survive-idle-reap.json new file mode 100644 index 0000000000..edaaab7e5e --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-sessionstart-hook-mxcli-init-writes-cannot-survive-idle-reap.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "raw": "| The SessionStart hook `mxcli init` writes cannot survive an idle reap: it is guarded on `test -x ./mxcli`, and `.gitignore` excludes that binary (~85 MB) on purpose. The container is reclaimed, the repo re-cloned without it, the guard fails and the hook no-ops through `\\|\\| true` — the next session has no mxcli, no MxBuild cache, no database, and no message saying so | The hook inlined the whole bring-up in one shell line, so \"binary missing\" could only be expressed as \"skip\". A hook line cannot reasonably do OS/arch detection and a download; something committed has to | `cmd/mxcli/init_hook.go` (`bootstrapScriptTemplate`, `writeBootstrapScript`, `sessionStartHookCommand`, `sessionStartHookMarkers`) | Emit a committed `.claude/bootstrap-mxcli.sh` that resolves OS/arch, fetches the binary when absent (`MXCLI_TAG` to pin), then runs the setup; the hook becomes `sh .claude/bootstrap-mxcli.sh \\|\\| true`. **Changing the hook command breaks dedupe**, which matched on the old command string — so `addSessionStartHook` now recognises *any* known marker and **rewrites the entry in place**, migrating an old project instead of leaving it with two hooks that both run. **Generalisable**: a guard whose condition is something you deliberately do not commit is a silent no-op waiting for a fresh clone — make the guard able to satisfy itself. Verified by reproducing the reap: moved `./mxcli` out of the project, ran the hook command verbatim, watched it re-download (88 MB, new mtime) and finish with \"Setup complete … database ready\". Tests `TestAddSessionStartHook_MigratesLegacyCommand`, `TestEnsureSessionStartHook_WritesFile`. mxcli-todo #2 |", "refs": ["#2"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-solution-s-apps-served-one-host-different-ports-so-they.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-solution-s-apps-served-one-host-different-ports-so-they.json new file mode 100644 index 0000000000..388adc4e62 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-solution-s-apps-served-one-host-different-ports-so-they.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "A solution's apps are served on one host with different ports, so they share a cookie jar (cookies key on host name and **ignore the port**) — logging into one silently replaces the other's `XASSESSIONID`. Giving each app a host name in **App Settings -> Configurations -> Application root URL** appears to do nothing under `run --local`", "cause": "`runtimeConfigParams` builds the entire boot `update_configuration` payload from `LocalRuntimeOptions`, and `ApplicationRootUrl` was only ever populated from a `--hub` registration. The model's own value had no path into the payload — and since the admin action REPLACES rather than merges, nothing else could supply it either", "file": "`cmd/mxcli/docker/runlocal.go` (`configuredApplicationRootURL`, `applicationRootURLFrom`, `customHostRootURL`)", "insight": "Read the setting off the project at boot and use it when no hub URL was assigned (hub wins — that URL is the one actually serving the app). **The trap is that a blank Mendix app already ships `ApplicationRootUrl = http://localhost:8080/`**, so \"is set\" does not mean \"was chosen\": honouring every value would change behaviour for every existing project and, under `--app-port`, advertise a port the app is not serving on. Only a **non-loopback host** is passed through, and a port that disagrees with `--app-port` warns. Serving under the host name needs no flag at all — the runtime accepts any `Host` and the client uses relative URLs (verified: 200 via `/etc/hosts`, nip.io and localtest.me); the setting matters only for the **absolute** URLs Mendix generates. **Generalisable**: before defaulting from a model setting, check what a blank project already has in it — a non-empty default makes \"fall back to the model\" a behaviour change, not a no-op. Verified end-to-end on 11.12.1: with `backend.local` configured, boot prints `Application root URL from configuration \"Default\"` and the app answers 200 on both the host name and the listen address. Tests `TestApplicationRootURLFrom`, `TestCustomHostRootURL`"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-test-annotated-cleanup-rollback-no-cleanup-all-rollback.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-test-annotated-cleanup-rollback-no-cleanup-all-rollback.json new file mode 100644 index 0000000000..5e87fa87dd --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-07-test-annotated-cleanup-rollback-no-cleanup-all-rollback.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-07", "symptom": "A test annotated `@cleanup rollback` (or with no `@cleanup` at all — rollback is the documented default) still leaves its rows in the database; a misspelled strategy like `@cleanup rollbak` does the same, silently, while the run reports PASS", "cause": "`TestCase.Cleanup` was parsed and then used nowhere. The after-startup runner had no seam to implement it — tests run inside the startup action, so there is no context the runner owns. The test endpoint creates that seam: it builds the `IContext` each test runs on", "file": "`cmd/mxcli/testrunner/endpoint.go` (the handler's execute block), `cmd/mxcli/testrunner/cleanup_strategy.go`", "insight": "Wrap the call in `ctx.startTransaction()` … `ctx.rollbackTransaction()` in a **finally** (a throwing test is the one most likely to leave half-written data), gated on a `rollback=1` query parameter the client sends per test. Report `rolledBack`/`rollbackError` in the response and warn per test — a rollback that fails silently is worse than none. Reject an unknown `@cleanup` value at **parse** time so `--list` catches it too. Verify against the database, not the endpoint's own claim: run one test with rollback and one with `@cleanup none` in the same suite and query Postgres — the `none` row must be the only survivor"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-ai-context-skills-still-carries-previous-release-s-guidance.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-ai-context-skills-still-carries-previous-release-s-guidance.json new file mode 100644 index 0000000000..d7f8c7135f --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-ai-context-skills-still-carries-previous-release-s-guidance.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-08", "raw": "| `.ai-context/skills/` still carries the previous release's guidance after the mxcli binary is upgraded — binary rebuilt at 12:05, skills stamped the day before — with no warning that they disagree | The skills are embedded in the binary and written exactly once, by `mxcli init`. Nothing re-ran init on upgrade, and nothing compared what was on disk against what the binary carried | New `cmd/mxcli/init_skills_sync.go` (`syncAIContextSkills`, `reportSkillSync`), `--sync-skills` flag in `cmd/mxcli/init.go`, and a sync step in the SessionStart bootstrap in `cmd/mxcli/init_hook.go` | **Stale guidance is worse than missing guidance**, because an agent reads it with identical confidence either way — so the failure mode is silent and confident. Fix it where it is consumed, not where it is authored: the SessionStart bootstrap already runs on every session and can fetch the binary, so it is the one place guaranteed to execute immediately before an agent reads the files. Two properties keep an every-session job acceptable: **write only what differs** (an mtime that moves every session makes \"when did this last change\" unanswerable — a test asserts the mtime holds) and **stay silent when current**. Never fatal: a skills refresh must not block a session, hence `|| true`. A test asserts the bootstrap script actually calls it *before* the exec'd setup, since a step ordered after an `exec` never runs. Tests `init_skills_sync_test.go`; the control (write-once) leaves the stale file in place. mxcli-formula1 §16 |"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-app-themed-dark-still-shows-light-mode-drop-shadows-under.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-app-themed-dark-still-shows-light-mode-drop-shadows-under.json new file mode 100644 index 0000000000..6d0ab07b2f --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-app-themed-dark-still-shows-light-mode-drop-shadows-under.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "An app themed dark still shows light-mode drop shadows under the datagrid's filter-operator popover and dropdown filter lists", "cause": "The generated widget layer re-pointed `.column-selectors` but not the four rules in `_datagrid-filters.scss` that bake the same two-layer shadow. Each already takes its *background* from `--bg-color-secondary`, so Atlas re-colours the panel and leaves the shadow — which is why it reads as a partial fix rather than an untouched widget", "file": "`cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-widgets.scss`", "insight": "**Read the selectors out of the shipped `themesource/`, not the bug report** — the fourth here is `.dropdown-container .dropdown-list`, nested under a parent the report did not name. **Verify in the compiled CSS, never the source** (the §33 rule): apply the theme to a real project, run `mxbuild --target=deploy --java-home=… --java-exe-path=…`, then grep `theme-cache/web/theme.compiled.css` and check the *line number* — the fix must land after the widget module's own rule (30794 vs 27765 here) or the cascade eats it. A theme-cache file older than the SCSS you just wrote is a stale compile, and mtime is the cheapest way to catch it. mxcli-formula1 #33 / suggested issue 4", "refs": ["#33"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-every-page-locally-run-app-black-screen-mxcli-check-passes.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-every-page-locally-run-app-black-screen-mxcli-check-passes.json new file mode 100644 index 0000000000..2abb9083b0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-every-page-locally-run-app-black-screen-mxcli-check-passes.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "Every page in a locally-run app is a black screen. `mxcli check` passes, the build reports success, the runtime log is quiet, `curl /` returns 200 with a valid HTML shell, and every OData service answers — only a browser sees it. It also comes and goes between runs of the same command at the same version", "cause": "`run --local` bundles the browser client at step 5b, then the runtime boot runs Gradle `clean-custom-classes compile package`, whose package pass repopulates `deployment/web` and takes `dist/` with it — deleting the bundle 51 seconds after the same command wrote it. It only bites when Gradle has work to do (a new Java action, a full recompile), which is why the app boots fine for weeks and then stops", "file": "`cmd/mxcli/docker/webclient.go` (`WebClientBundled`, `EnsureWebClientBundle`, `ReportLostWebClientBundle`), `cmd/mxcli/docker/runlocal.go` (step 6b, after the boot), `cmd/mxcli/docker/localapp.go` (headless boots warn instead of paying ~30s)", "insight": "**A pre-condition established before a step that rewrites the same directory is not a post-condition** — verify after, not before. The guard is a `stat` when the bundle survived, which is what makes it affordable at every boot; re-ordering the bundle to after the boot instead would leave the app reachable-but-blank for ~30s on every cold start. **`curl /` returning 200 is not evidence the app renders**: the shell is served by the runtime, the client by a file the shell references. The one-line check is `curl -o /dev/null -w '%{http_code}' /dist/index.js`. **Do not silently pay for a repair on a path that does not need it** — `test --local` destroys the bundle too, but tests are headless, so it prints the loss and the remedy rather than spending 30s on a two-second loop. Tests `webclient_bundle_test.go`; both controls run (guard never fires → the wipe test fails; guard always fires → the survivor test fails). mxcli-formula1 §35"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-mxcli-run-local-ensure-db-fails-freshly-built-mxcli-init-dev.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-mxcli-run-local-ensure-db-fails-freshly-built-mxcli-init-dev.json new file mode 100644 index 0000000000..96adbc4d3b --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-mxcli-run-local-ensure-db-fails-freshly-built-mxcli-init-dev.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "`mxcli run --local --ensure-db` fails on a freshly built `mxcli init` dev container — there is no PostgreSQL service to start and no `postgres` superuser — even though `psql` is on PATH", "cause": "The generated Dockerfile installed `postgresql-client` **only**. `EnsureDatabase` starts a local service (`service postgresql start`) and provisions the role + database through `sudo -u postgres psql`, both of which need the **server** package. `psql` being present makes the container look correctly provisioned", "file": "`cmd/mxcli/tool_templates.go` (`generateDockerfile`)", "insight": "Install `postgresql` alongside `postgresql-client`. **Generalisable**: when a feature shells out to a service, assert the *server* package in the image template, not the client that happens to satisfy a `LookPath` check — the CI/web image having the server is what hid this (`/usr/lib/postgresql/16` is present there but not in the generated dev container). Guarded by `TestGenerateDockerfile_PostgresServer`, which asserts the server package for both the docker and podman variants"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-mxcli-test-tests-p-app-app-mpr-list-fails-stat-tests-no-such.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-mxcli-test-tests-p-app-app-mpr-list-fails-stat-tests-no-such.json new file mode 100644 index 0000000000..fa6f7b7dec --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-mxcli-test-tests-p-app-app-mpr-list-fails-stat-tests-no-such.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "`mxcli test tests/ -p app/App.mpr --list` fails with `stat tests/: no such file or directory` while the same command without `--list` runs fine", "cause": "The `--list` branch passed raw `args` to `ListTests`, bypassing `resolveTestPaths` — so a path relative to the project (rather than the working directory) resolved for execution but not for listing", "file": "`cmd/mxcli/cmd_test_run.go` (the `if list` branch)", "insight": "Pass `resolveTestPaths(args, projectPath)` there too. When a command has two entry points into the same input, check both go through the same path resolution. mxcli-formula1 findings #15", "refs": ["#15"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-suite-passes-under-mxcli-test-attach-fails-under-local.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-suite-passes-under-mxcli-test-attach-fails-under-local.json new file mode 100644 index 0000000000..2d0cff79cb --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-suite-passes-under-mxcli-test-attach-fails-under-local.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "A suite passes under `mxcli test --attach` and fails under `--local`, with assertions that depend on startup state (a loaded cache, seeded reference data) seeing zero rows", "cause": "The `--local` runner pointed after-startup at its own registration microflow and did **not** chain the project's own, so the app's startup logic never ran. It was a deliberate choice (a known baseline) but was invisible: the run printed only `After-startup set to MxTest.RegisterEndpoint`, never that the user's microflow had been displaced", "file": "`cmd/mxcli/testrunner/runner.go` (`runEndpoint`), `cmd/mxcli/testrunner/cleanup_strategy.go` (`describeStartup`)", "insight": "Capture project state **before** generating the endpoint MDL, and pass `state.afterStartup` to `GenerateEndpointMDL` so the generated flow chains it — the hosted `--test-endpoint` path already did this, and the mismatch between the two was the bug. Add `--skip-app-startup` for a deterministic empty baseline, and always print which of the two happened. Note the startup microflow's writes run at boot, outside any test transaction, so `@cleanup rollback` does not undo them. mxcli-formula1 findings #19", "refs": ["#19"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-themed-app-palette-everywhere-except-few-widget-details-data.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-themed-app-palette-everywhere-except-few-widget-details-data.json new file mode 100644 index 0000000000..00799c941c --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-08-themed-app-palette-everywhere-except-few-widget-details-data.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-08", "symptom": "A themed app is on-palette everywhere except a few widget details — the **Data Grid 2 pager caption is invisible** (1.02:1 on a dark ground), row-select checkboxes stay stock Mendix blue, popovers cast light-mode shadows. Re-pointing tokens changes nothing, and the same widget's other parts (the pager *buttons*) are fine", "cause": "The theme source shipped by the **widget modules** (`themesource/datawidgets`, `atlas_web_content`) styles some things with Sass variables and literals — `datawidgets/web/variables.scss:18` is `$pagination-caption-color: #0a1325`. Sass resolves those at compile time, before any custom property exists, so the value is baked into `theme.compiled.css` and no `--mxt-*` can reach it. The parts that *do* work resolve `var(--gray-darker, …)` through Atlas: same bar, two mechanisms", "file": "`cmd/mxcli/theme/assets/*/files/theme/web/_mxcli-widgets.scss` (the shared widget layer, imported after the theme partial)", "insight": "Add a CSS rule per baked declaration, resolving through a token so both palettes follow. **The obvious fix does not work**: each module's `main.scss` imports `theme/web/custom-variables` before its own `!default` vars, so `$pagination-caption-color: var(--mxt-ink-muted)` there *would* win — but (1) the names collide with Atlas Core's, which feeds them to Sass colour functions (`atlas_core/web/_variables.scss:20` computes `mix($brand-primary, #e7e7e9, 10%)`; handing `mix()` a `var()` is a compile error) and (2) the worst offenders are not behind a variable at all — `_three-state-checkbox.scss` writes `#264ae5` and `rgba(#264ae5, 0.4)` directly. **Generalisable — the shape to look for**: read the **compiled CSS, not the SCSS**, when deciding what to override. The sources are full of `var(--token, #fallback)` declarations that already resolve correctly; in one measured app `#264ae5` appeared in 46 declarations and **24 were harmless fallbacks**, so grepping the source would have produced twice the rules for no benefit. Reported from the Formula1 test build (§33); verified in a browser: pager caption 1.02:1 → 6.99:1 console dark, 6.39:1 light, 6.78/5.93 on signal", "refs": ["#0", "#264"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-09-headless-browser-cannot-log-app-started-hub-so-every.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-09-headless-browser-cannot-log-app-started-hub-so-every.json new file mode 100644 index 0000000000..b3eae4f3bb --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-09-headless-browser-cannot-log-app-started-hub-so-every.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-09", "symptom": "A headless browser cannot log in to an app started with `--hub`, so every screenshot silently shows the login page and rendering defects survive every other check", "cause": "Under `--hub` the runtime boots with the public **https** root URL, so it marks session cookies `Secure` and prefixes them `__Host-`; a browser on a non-trustworthy http origin cannot store them", "file": "`cmd/mxcli/docker/screenshot_login.go` — the login browser context declares `X-Forwarded-Proto: http` when the target is http (Mendix 10.24+ lets that header override `ApplicationRootUrl`)", "insight": "**The reported cause did not reproduce as stated, and saying so is part of the fix.** On 11.12.1 an https root URL does *not* block a headless browser on `127.0.0.1`: loopback is a **trustworthy origin**, so Chromium accepts `Secure`/`__Host-` cookies there — the app rendered clean, no console errors, no failed requests. The mechanism is real only for a **non-loopback** http origin (a container hostname, a LAN address). Fix shipped anyway because the header is *accurate* rather than a workaround (the request genuinely is http), it costs nothing on an already-http root URL, and real users over https are unaffected. Verified at the layer the bug lives in: the captured Playwright storage state goes from `__Host-XASSESSIONID(secure=true)` to `XASSESSIONID(secure=false)`. **`curl` cannot see this class of bug and neither can a loopback browser** — when a report blames cookie flags, check whether the origin is trustworthy before believing the flags are the blocker. mxcli-formula1 §38 / suggested issue 7"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-09-mxcli-run-local-refuses-boot-port-6643-already-use-previous.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-09-mxcli-run-local-refuses-boot-port-6643-already-use-previous.json new file mode 100644 index 0000000000..fa57bfadb2 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-09-mxcli-run-local-refuses-boot-port-6643-already-use-previous.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-09", "symptom": "`mxcli run --local` refuses to boot — *\"port 6643 is already in use — a previous 'mxcli run --local' … is likely still serving on it\"* — and names a process the user already killed. The guard is right to refuse; the diagnosis is a guess, and the recovery it suggests is a three-command `pgrep` hunt whose own pattern (`pgrep -f 'mxcli run'`) matches the shell it is typed into", "cause": "Two different situations printed one message. mxcli **does** reap its children on Ctrl-C/SIGTERM (`procgroup_unix.go` puts mxbuild's JVM, the runtime and the bundler each in their own process group and signals the group), so a held port after a graceful stop is a *foreign* listener. But `kill -9`, a crash, or a reaped container runs no handler at all, and then it really is the previous run's orphan. The guard could not tell them apart because it never asked who held the port", "file": "`cmd/mxcli/docker/portowner_linux.go` (`listenerOnPort`, `parseListeningInodes`, `pidForSocketInode`), `portowner_other.go` (non-Linux stub), `portguard.go` (`portCulpritAdvice`), wired into `checkTargetPortsFree` (`runlocal.go`) and `checkLocalAppPortsFree` (`localapp.go`)", "insight": "**Resolve the port owner from `/proc`, never by shelling out to `lsof`/`ss`** — both are routinely absent from slim containers, and needing a subprocess to explain why a boot failed is its own failure mode. `/proc/net/tcp{,6}` gives the socket inode; scanning `/proc//fd` for `socket:[]` gives the pid. **Read `tcp6` as well as `tcp`**: a JVM binding \"localhost\" lands on `[::1]` or dual-stack `[::]` as often as `127.0.0.1`, and its local_address is a 128-bit hex blob, not the 32-bit one. **Filter on state `0A` (TCP_LISTEN)** — an ESTABLISHED socket on that port is a *client*, and naming it would tell the user to kill their own browser. **Report the two cases differently**: a leftover of a previous run gets `kill `, a foreign listener gets \"pick another port\" and no kill suggestion, because offering to kill a stranger's process is worse advice than the vague message it replaced. Keep the guard **detection-only** — reaping someone else's process stays the user's call. **Three test traps hit in a row here.** (1) The live-IPv6 test only ever *skipped* (no IPv6 in the container), so the tcp6 parser was effectively untested — assert against captured kernel output instead. (2) A parser test calling `parseListeningInodes` directly does not prove `listeningInodes` *opens* both files: deleting `/proc/net/tcp6` from the list left every test green. (3) Making the list a `var` so a test can point it at fixtures then means the test no longer covers the **shipped** default either — assert the default's contents before overriding it. Only the third version of that control failed. The best test in the file is the cheapest: bind a port in the test process and demand `listenerOnPort` return `os.Getpid()` — it cannot pass by accident. Tests `cmd/mxcli/docker/portowner_linux_test.go`. mxcli-formula1 suggested issue 8"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-09-no-way-see-what-running-app-s-subsystem-doing-everything.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-09-no-way-see-what-running-app-s-subsystem-doing-everything.json new file mode 100644 index 0000000000..b0bfebba90 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-09-no-way-see-what-running-app-s-subsystem-doing-everything.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-09", "symptom": "No way to see what a running app's subsystem is doing: everything logs at `INFO`, the detail is not in the log at all, and raising the whole runtime to `TRACE` is unusable on a busy app", "cause": "Nothing in mxcli drove the runtime's per-node log levels, though the M2EE admin API has exposed them all along", "file": "New `cmd/mxcli/docker/loglevels.go` (`GetLogSettings`, `SetLogLevels`, `NormalizeLogLevel`, `MatchLogNodes`), `cmd/mxcli/cmd_log.go` (`mxcli log list` / `log set`), documented in `.claude/skills/mendix/analyze-runtime.md`", "insight": "**Probe an undocumented API before designing the command around it.** Every fact here came from a live 11.12.1 runtime: `get_log_settings` requires one of `node`/`subscriber`/`sort`; `sort` accepts only `node` and `subscriber`; `set_log_level` takes `{\"nodes\":[{name,level}],\"force\":bool}`. **The HTTP response for an AdminException says only \"See logging output for details\" — the actual message (\"Please specify node, subscriber or sort option in params\", \"Unknown sort option: name\", \"Unknown LogNode X. Use the 'force' parameter…\") is in the runtime log**, so probe with the runtime log open or you learn nothing. `force` means \"allow a node that does not exist yet\" and **permanently registers the name**, so a typo becomes a real empty node — hence it is opt-in, and an unknown node is an error by default. **Resist the subsystem-specific command**: this was proposed as `mxcli odata trace`, but `set_log_level` takes a list of nodes, so the primitive is generic and the OData knowledge belongs in docs. **The node list is a property of the APP, not of Mendix** — a node appears only once something registers it. A first pass concluded \"there is no log node for a published OData service\" from a project that had none; adding one service turns 57 nodes into 58 and `OData Publish` (with a space) appears, logging the full incoming URI at TRACE — exactly the question that motivated the command. Enumerating capabilities against one sample app and generalising is the trap; `log list` against *this* app is the answer. Distinguish \"cannot reach the admin API\" from \"the runtime refused the request\" (`ErrAdminUnreachable`) or the wrong hint buries the real one. Tests `cmd_log_test.go`, `loglevels_test.go`; verified end-to-end against a booted runtime (both arg forms, multi-node, typo refused, `--force` accepted, unreachable port). mxcli-formula1 suggested issue 4"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-after-mxcli-marketplace-install-id-p-app-mpr-project-s.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-after-mxcli-marketplace-install-id-p-app-mpr-project-s.json new file mode 100644 index 0000000000..f36a1a0010 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-after-mxcli-marketplace-install-id-p-app-mpr-project-s.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-11", "symptom": "After `mxcli marketplace install -p app.mpr`, the project's `mprcontents/` folder is gone, the `.mpr` has grown from tens of KB to tens of MB, and `mxcli diff-local` no longer works. Git shows hundreds of deleted `.mxunit` files and one enormous binary", "cause": "`mx module-import` rewrites an MPR **v2** project as **v1** — silently, and one-way. Measured on a blank 11.12.1 app: 69,632-byte `.mpr` + 341 `.mxunit` → 14,295,040-byte `.mpr` + 0, with the `_Transaction` table dropped. Reproduced independently on 11.13.0, so it is not version-specific. mxcli's install command shells straight out to it against the **user's** project", "file": "`cmd/mxcli/cmd_marketplace_install.go` (`checkStorageFormatPreserved`, `isMPRv2`, `reportFormatChange`, `--allow-format-change`)", "insight": "**Refuse, do not warn** — the conversion cannot be undone (`mx convert` targets Mendix versions, not storage formats) and it destroys the per-document layout `diff-local`, `.mxunit` merges and ADR-0008's observable idempotence all rest on. **Detect the format the way the readers do** (presence of `mprcontents/`), not with a second definition. **Check the guard's negative case**: a test that only asserts \"v2 is refused\" also passes a guard that refuses everything — assert a v1 project still imports. **Do not guard every `module-import` call site**: `marketplace diff` runs one against a throwaway reference project, where the format is irrelevant (and the resulting v1 reference still diffs exactly against a v2 project, because DESCRIBE output does not depend on storage). Related, same family: `mx module-import` also refuses any module whose `Projects$ModuleImpl → IsThemeModule` is `true` (exit 112), which is the sole gate — flipping that one boolean makes an otherwise identical package import cleanly. Tests `cmd/mxcli/cmd_marketplace_install_test.go`. Reported in mxcli-formula1 FINDINGS §53"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-mpr-extracted-from-mpk-copied-anywhere-beside-existing.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-mpr-extracted-from-mpk-copied-anywhere-beside-existing.json new file mode 100644 index 0000000000..6d13457e5d --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-mpr-extracted-from-mpk-copied-anywhere-beside-existing.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-11", "symptom": "A .mpr extracted from a `.mpk` (or copied anywhere beside an existing project) reads with the wrong unit contents: edits appear to succeed, `ListRawUnitsByType` returns plausible documents, and a write lands nowhere the file will carry. Downstream the operation fails exactly as if nothing had been attempted", "cause": "The MPR format test is **adjacency, not content**: an `.mpr` is treated as v2 when an `mprcontents/` directory sits next to it (`sdk/mpr/reader.go` `OpenWithOptions`). A package's v1 `project.mpr` unpacked into a work directory that already holds a scratch v2 project is therefore read as v2, and every unit resolves against the *other* project's `.mxunit` files", "file": "`cmd/mxcli/marketplace/theme.go` (`makeImportable` unpacks into its own `os.MkdirTemp` subdirectory)", "insight": "**Unpack any foreign .mpr into a directory of its own** — never beside a project, even a throwaway one. **The failure is silent in both directions**: the read returns real-looking documents (from the wrong model) and the write reports success, so nothing surfaces until a later step refuses. The tell is a fix that provably works in isolation (`mx module-import` accepted the hand-built package) but does nothing in situ — that gap is environment, not logic. Test `TestMakeImportable_UnpacksAwayFromTheScratchProject` asserts the unpack does not land in the work-directory root"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-mxcli-new-into-deep-output-directory-dies-system-io.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-mxcli-new-into-deep-output-directory-dies-system-io.json new file mode 100644 index 0000000000..f978ccca99 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-mxcli-new-into-deep-output-directory-dies-system-io.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-11", "symptom": "`mxcli new` into a deep output directory dies with `System.IO.PathTooLongException` and leaves the directory holding ~259 files and no `.mpr` — output that looks like a project until you try to open it", "cause": "MxToolset refuses any full destination path over **259 characters** (its own Windows-compatibility limit, not the filesystem's) and aborts extraction PART WAY THROUGH. mxcli pointed `mx create-project` straight at the user's output directory, so the abort happened there", "file": "`cmd/mxcli/newproject_paths.go` (new: `stagedProjectDirs`, `longestRelativePath`, `warnIfPathTooLongForStudioPro`, `moveProject`), wired into `cmd/mxcli/cmd_new.go` step 2", "insight": "**Stage the work somewhere safe and move it in, rather than validating a path you could just avoid** — creating in a short temp dir and renaming makes ANY depth work (POSIX allows 4096) instead of merely failing politely, and the destination is never partially populated because nothing is written there until creation succeeded. Check the relocation is safe first: `grep -rl ` returned **0 files**, so a fresh Mendix project embeds no absolute paths. **Bisect for the real threshold instead of trusting arithmetic** — 77 characters creates a project on 11.13.0, 78 fails leaving 259 files, which pins `len(dest) + 1 + longest ≤ 259` exactly. **Measure the template, don't hardcode it**: the longest relative path is 181 on 11.13.0 and 182 on 11.12.0, so walk the staged tree and use the real number, or the reported budget drifts a character per release. **Warn, don't refuse, when the finished path is over budget** — the project works on POSIX, so refusing would block a machine where it is fine; but Studio Pro on Windows would not open it, so silence would be worse. Cross-device staging needs an `os.CopyFS` fallback: `os.Rename` cannot cross filesystems. Tests `cmd/mxcli/newproject_paths_test.go`. upstream #825", "refs": ["#825"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-mxcli-syntax-topic-teaches-mdl-does-not-parse-binds-removed.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-mxcli-syntax-topic-teaches-mdl-does-not-parse-binds-removed.json new file mode 100644 index 0000000000..0be997fd7d --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-11-mxcli-syntax-topic-teaches-mdl-does-not-parse-binds-removed.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-11", "symptom": "`mxcli syntax ` teaches MDL that does not parse — `Binds:` (removed in favour of `Attribute:`, and hard-rejected by the parser), workflow decision outcomes without the `->` arrow, `ALTER WORKFLOW … SET DUE DATE = '…'` (no `=`), `INSERT AFTER ` (operands reversed), `IMAGE 'name'` (identifier, not string), a `BEFORE` that does not exist. Nothing failed, because nothing checked", "cause": "The registry (`cmd/mxcli/syntax/features_*.go`) is hand-maintained while the grammar moves underneath it, and every existing test checked *structure* — fields populated, aliases resolve, see-also targets exist — never whether the documented MDL is real. 26 of 120 entries had a non-parsing example", "file": "`cmd/mxcli/syntax/example_parses_test.go` (new guard), plus corrections across `features_page.go`, `features_workflow.go`, `features_integration.go`, `features_microflow.go`, `features_misc.go`", "insight": "Parse every `Example` and fail the build if it does not. Examples come in several legitimate shapes, so each blank-line-separated **block** is tried as a statement, a microflow activity, a page widget, a workflow activity, and a retrieve clause — a failure means it parses as none of them. Only `Example` is checked; `Syntax` carries metasyntax (`[OR MODIFY]`, ``) by design. **Generalisable — the shape to look for**: documentation that is *data in the binary* can be executed against the real parser, which turns \"the docs drifted\" from a review problem into a test failure. The same trick applies to any embedded example corpus (skills, `--help` text, README snippets extracted at build time). **Why it matters more than it looks**: the registry is the first surface an agent consults, so a gap there does not read as \"undocumented\", it reads as \"unsupported\" — a contact-management app built with mxcli worked around three features that already existed, including replacing a `SAVE_CHANGES CLOSE_PAGE` button with a bespoke microflow because only the two halves were listed separately. **Close the opt-out** — a guard that skips empty input is a guard you can silence by emptying the field; `TestFeatureFieldsPopulated` already rejects a blank `Example`, and the new guard additionally fails an example that is all comments"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-after-headless-module-install-mx-check-reports-project-level.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-after-headless-module-install-mx-check-reports-project-level.json new file mode 100644 index 0000000000..7328c0a44f --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-after-headless-module-install-mx-check-reports-project-level.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "After a headless module install, `mx check` reports project-level `CE6087` \"Design properties have been renamed in your theme and need to be updated\" with an empty location, and no mxcli command clears it — or the same for `CE0463`, which `mxcli docker check` makes pass without actually fixing", "cause": "The module ships its own design properties; Mendix's fix is `mx rename-design-properties`, which mxcli never ran. Measured on 11.12.1: it does real work (149 design properties across 41 documents) **and** collapses MPR v2 — 1,865 `.mxunit` files to 0, a 249,856-byte index to 39,895,040 bytes. Third member of the family, after `mx module-import` and `mx update-widgets`", "file": "`cmd/mxcli/docker/harvest.go` (`RunToolPreservingFormat`), `cmd/mxcli/cmd_fix.go` (`mxcli fix design-properties` / `fix widgets`)", "insight": "**The `update-widgets` snapshot trick does not transfer** — that one restores the pre-run storage because the resync only has to hold for the duration of the check, whereas these renames must **persist**, so restoring undoes the fix. **Harvest instead**: let the tool convert the project, read every unit back out of the converted file, restore the v2 storage, then write the changed units into it through the writer — which is also where `canon.Reconcile` elides the units the tool rewrote without changing (ADR-0008), so a second run writes 0. Safe for the same reason a module transplant is: no binary `$ID` crosses a unit boundary. **Restore on every failure path after the tool has run**, or a failed command leaves a half-converted v1 file. **Print the storage count before and after** — the collapse this prevents shows up there as a zero, and a success message without its own evidence is how it shipped the first time. **`mprcontents/` is not all units**: it also holds an `mprname` file, so a naive file count is one high and reads as a dropped unit. Measured: 203 errors (202 CE0463 + 1 CE6087) to 0 on a vanilla 11.12.1 app, MPR v2 intact at 1,868 units, reproduced from a restored pre-fix snapshot. Tests `cmd/mxcli/docker/harvest_test.go` — each verified to fail with the reported symptom when the fix is stubbed out", "ce": ["CE0463", "CE6087"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-after-marketplace-module-update-mx-check-reports-ce6083.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-after-marketplace-module-update-mx-check-reports-ce6083.json new file mode 100644 index 0000000000..b67ca031a6 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-after-marketplace-module-update-mx-check-reports-ce6083.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "After a marketplace module update, `mx check` reports `CE6083` \"Design property X is not supported by your theme\" on the updated module's own widgets, and it survives BOTH `mx update-widgets` and `mx rename-design-properties` (which renames 0)", "cause": "The update copied only the model and `widgets/`. The design properties are declared in the module's **own** `themesource//web/design-properties.json`, which was still the old version's copy — so the model referenced properties the shipped theme file did not define", "file": "`cmd/mxcli/marketplace/update.go` (`InstallPackageFiles` replaces `InstallPackageWidgets`)", "insight": "**Copy everything the package ships**, excluding only `project.mpr` and `package.xml` — enumerating the directories that seem to matter is what produced two bugs in a row (widgets, then themesource). **CE6083 surviving `rename-design-properties` is the tell**: that command renames properties between Atlas generations, so 0 renamed means the declaration is missing entirely rather than renamed. **Do not diagnose this as a cross-module Atlas dependency** — that was the wrong call here; the declaring module was the one being updated. Guard against `..` in package paths while copying. Measured: DataWidgets 3.5.0 → 3.11.3, 49 files replaced, 29 errors → 0", "ce": ["CE6083"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-constant-set-alter-settings-constant-m-c-value-configuration.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-constant-set-alter-settings-constant-m-c-value-configuration.json new file mode 100644 index 0000000000..6337586566 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-constant-set-alter-settings-constant-m-c-value-configuration.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "A constant set with `alter settings constant 'M.C' value '…' in configuration 'Default'` has no effect on the running app. The statement succeeds, `describe settings` round-trips it, `mx check` is clean, and the app behaves as if the constant were still at its default", "cause": "mxbuild writes `deployment/model/config.json` with each constant's **default** value, and that map is what `run --local` hands the standalone runtime as `MicroflowConstants` — the configuration's overrides are not in it, and nothing read them. An app ran for hours with an empty encryption key while the model said otherwise", "file": "`cmd/mxcli/runconstants.go` (`resolveConstantOverrides`, `reportConstantOverrides`), `cmd_run.go` (`--configuration`), `cmd/mxcli/docker/localboot.go` (`mergeConstantOverrides`)", "insight": "**A silent no-op is the worst shape a bug can take** — every layer reported success. The fix therefore prints in *every* case, including \"no overrides applied\", so silence stops meaning \"your value is in effect\". **Merge, never replace**: `config.json` carries the defaults for constants the configuration is silent about, and replacing the map drops them (the app 530s on the first microflow that reads one) — which is exactly the shape `--runtime-setting MicroflowConstants={…}` has. **A private override has no value in the model at all** (a `Settings$PrivateValue` marker; the value lives on the workstation), so applying it would blank the constant — skip and name it. **Do not guess between configurations**: with several and no `Default`, applying one silently could push production's API key into a local run. Tests `cmd/mxcli/runconstants_test.go`, `cmd/mxcli/docker/localboot_constants_test.go`; verified live — the run now prints `Applying 1 constant value(s) from configuration \"Default\"`. Reported in mxcli-chat FINDINGS §33"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-marketplace-module-update-leaves-app-running-old-widget-code.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-marketplace-module-update-leaves-app-running-old-widget-code.json new file mode 100644 index 0000000000..ed62056e79 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-marketplace-module-update-leaves-app-running-old-widget-code.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "A marketplace module update leaves the app running the OLD widget code: `show modules` reports the new version, pages reference new widget definitions, and the binaries in `widgets/` are unchanged. Symptoms are whatever the version gap causes at render time, with no build error naming the cause", "cause": "The update moved only the **model**. A widget module's `.mpk` ships its widget binaries under `widgets/` inside the package, and copying units out of a reference project never touches them. Measured on DataWidgets 3.5.0 → 3.11.3: model updated cleanly, all 10 widget binaries still 3.5.0 (`Datagrid.mpk` project=216193 vs package=166933)", "file": "`cmd/mxcli/marketplace/update.go` (`InstallPackageWidgets`, called from `PerformUpdate`)", "insight": "**Take the widget files from the `.mpk`, not from the reference project** — the reference is a blank app plus the module, so its `widgets/` also holds the template's widgets and copying those overwrites widgets the update has nothing to do with. **Skip zero-size directory entries** in the zip or you write a stray file (the 3.11.3 package has 10 `widgets/` entries and 9 real files). **A module with no widgets cannot expose this**: Administration updated end-to-end and looked completely correct, which is why the gap survived a full slice. Pick a second subject with a different shape before believing an update path works. Related but distinct: after any headless module install `mx update-widgets` is needed (CE0463); and a newer module can want a newer Atlas than the project has (29 × CE6083 on DataWidgets 3.11.3), which `rename-design-properties` does NOT fix — that is a dependency, not a resync", "ce": ["CE0463", "CE6083"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-marketplace-diff-reports-local-edits-project-nobody.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-marketplace-diff-reports-local-edits-project-nobody.json new file mode 100644 index 0000000000..db73029c69 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-marketplace-diff-reports-local-edits-project-nobody.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "`mxcli marketplace diff` reports local edits on a project nobody has edited — typically an Atlas snippet and a building block. `--save-edits` then writes MDL that would *destroy* the element if replayed (an empty `create or modify snippet X (Folder: 'Web') { }`)", "cause": "The comparison is on DESCRIBE output, and `Describable()` only asked whether describe *errored*. Some types describe successfully into output that says nothing: an empty `{ }` body, or a building block under \"Building blocks are read-only; they cannot be created via MDL\". Two such renderings can still differ — one unresolved name is enough — and the difference was reported as a user edit", "file": "`cmd/mxcli/marketplace/snapshot.go` (`Element.Conclusive`, `declaredReadOnly`, `emptyBody`), `compare.go` (`classify`), `update.go` (`SaveEdits`)", "insight": "**Check equality BEFORE conclusiveness** — identical text is solid evidence of \"unchanged\" whatever the type, and inverting the order marks every Atlas building block unknown and drains `verified` of meaning. **Judge the output, not the type**: a type list goes stale the moment a describe handler improves, while \"this text contains nothing to compare\" stays true by construction — match the handler's own read-only wording so the two stay in step. **A rescue file must never be a deletion**: the same rule gates `--save-edits`, including `OnlyInstalled` findings, which reach it without passing through `classify`. **Test that a real edit is still Modified** — a guard that returns unknown for everything passes every \"not evidence\" test. Tests `cmd/mxcli/marketplace/inconclusive_test.go`. Reported in mxcli-chat FINDINGS §16"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-marketplace-install-id-p-app-mpr-fails-mx-module.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-marketplace-install-id-p-app-mpr-fails-mx-module.json new file mode 100644 index 0000000000..a2c915c73f --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-marketplace-install-id-p-app-mpr-fails-mx-module.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "`mxcli marketplace install -p app.mpr` fails with `mx module-import failed: exit status 117` — \"The package could not be imported, because it was created with a newer version of Mendix Studio Pro\" — after downloading the package and building a reference project, and the message is buried under the command's full flag list", "cause": "The marketplace publishes new releases against the newest Studio Pro patch within days, and `install` with no `--version` resolves to the latest, so on any project not on the very newest patch the **default** version is the one that cannot be installed. Measured 2026-08-12 on an 11.12.1 project: the latest release of all six agent-stack modules required 11.12.2, published five days earlier. Every version the API returns already carries `minSupportedMendixVersion` — nothing consulted it", "file": "`cmd/mxcli/marketplace_compat.go` (`checkMendixCompatibility`, `newestCompatibleVersion`, `mendixVersionOf`), called from `cmd_marketplace_install.go` and `cmd_marketplace_update.go`; `SilenceUsage`/`SilenceErrors` on install/update/diff", "insight": "**Check what the API already told you before spending a download and a reference build on it** — the refusal was always going to happen, three layers lower and in mxcli's voice rather than Mendix's. **Name the version to use, not just the problem**: `--version 4.1.0 (the newest release built for 11.12.1 or older)` is the whole fix. **Skip, do not refuse, when the check cannot evaluate** (no project version, no published minimum) — same rule as `checkFeature`. **`SilenceErrors` belongs with `SilenceUsage`**: `main()` already prints what `Execute` returns, so silencing only usage leaves every refusal printed twice. Tests `cmd/mxcli/marketplace_compat_test.go`, including the negative case — a check that refuses everything passes the refusal test"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-marketplace-update-diff-fail-version-6-0-0-not-found.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-marketplace-update-diff-fail-version-6-0-0-not-found.json new file mode 100644 index 0000000000..5d80ab9778 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-marketplace-update-diff-fail-version-6-0-0-not-found.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "`mxcli marketplace update`/`diff` fail with `version \"6.0.0\" not found` on a module the project actually has, and `--force` does not bypass it", "cause": "Both download the **installed** version to build the local-edit baseline, and that version had been unpublished. A blank 11.13 app ships NanoflowCommons 6.0.0 while the 6.x line now starts at 6.1.1 — so the module most in need of updating is the one whose baseline cannot be built", "file": "`cmd/mxcli/cmd_marketplace_update.go` (`--no-baseline`, and the refusal now names it)", "insight": "**`--force` was never going to work**: it overrides a *finding*, and here the comparison never ran, so there is no finding. Needing a second flag is the signal that these are two different decisions — \"I accept losing edits I have been shown\" vs \"I accept not being shown\". **Say what the flag costs in the flag's own message** — with no baseline, local edits go without being named, so the honest instruction is to commit first. Reported in mxcli-chat FINDINGS §15"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-run-ensure-db-fails-start-postgresql-no-service.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-run-ensure-db-fails-start-postgresql-no-service.json new file mode 100644 index 0000000000..c48672d0f1 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-mxcli-run-ensure-db-fails-start-postgresql-no-service.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "`mxcli run --ensure-db` fails to start PostgreSQL when no service manager becomes ready (e.g. Arch): `exec: \"pg_ctlcluster\": executable file not found in $PATH`, though `initdb`/`pg_ctl`/`psql` are present", "cause": "`startLocalPostgres` only knew the `service`/`pg_ctlcluster` helpers (the latter a placeholder that could never run), and role/database provisioning assumed `sudo -u postgres`, which a user-owned cluster does not need", "file": "`cmd/mxcli/docker/ensuredb.go` (`startLocalPostgres`, `startUserCluster`, `resolveSuperuser`)", "insight": "Fall back to a user-owned `initdb`/`pg_ctl` cluster under `~/.mxcli/postgres`, but never initialize a competitor when the requested TCP port is already owned by a service-started server that is still recovering. Enforce the passwordless-superuser boundary instead of assuming it: explicitly tighten an existing socket directory to `0700`, set the socket itself to `0700`, and persist `listen_addresses`/`port`/`unix_socket_directories` in `postgresql.conf` so a plain later `pg_ctl start` stays safe. Refuse, with a cleanup path, a cluster made by the earlier development revision if its host authentication is still `trust`. Reuse a running cluster only when both the port and socket directory in `postmaster.pid` match. Keep `sudo -u postgres` for system clusters, but make both sudo and psql non-interactive, retain peer authentication over the system Unix socket at the exact requested port, and force SCRAM password storage for pre-14 defaults. Normalize the endpoint once and return it to the runtime; parse the bracketed form before any last-colon IPv6 compatibility fallback. Tests stub every external tool and exercise slow service startup, legacy-cluster refusal, permissions, persisted settings, endpoint propagation, sudo arguments, and repeated/running cases. Issue #823", "refs": ["#823"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-project-mpr-v2-v1-after-marketplace-install-update.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-project-mpr-v2-v1-after-marketplace-install-update.json new file mode 100644 index 0000000000..197376d3a8 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-project-mpr-v2-v1-after-marketplace-install-update.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "A project that was MPR v2 is v1 after a marketplace install or update: `mprcontents/` is empty or gone, the `.mpr` jumped to tens of MB, `mxcli diff-local` stops working. The user followed the command's own \"Next:\" hint, or the docs", "cause": "The hint said to run bare `mx update-widgets ` to clear the CE0463 a headless install leaves behind. That command performs the resync **and** rewrites a v2 project as v1 — measured on 11.12.1: 369 `.mxunit` files → 0, a 69,632-byte index → 14,405,632 bytes. Same family as the `mx module-import` collapse, arriving through advice rather than through a call", "file": "`cmd/mxcli/cmd_marketplace_install.go` + `cmd_marketplace_update.go` (the \"Next:\" lines now name `mxcli docker check`), `docs-site/src/guides/marketplace.md`, `.claude/skills/mendix/download-marketplace-content.md`", "insight": "**`mxcli docker check` is the v2-safe resync** — it runs the same `mx update-widgets` under a storage-format snapshot (`cmd/mxcli/docker/update_widgets.go`, #808), so the check sees the resynced model and the project keeps its format. Verified: 369 `.mxunit` / 69,632 bytes before and after. **The resync is then not persisted** — the stored model still holds the old widget definitions and a later `mx check` reports CE0463 again; Studio Pro's \"Update all widgets\" persists it in v2, and `mxcli widget sync` is the partial headless equivalent (7 of 40 on the reference fixture). Say that rather than implying the resync sticks. **A guard in the code does not cover a string in the help text**: #808 already protected every call site mxcli owns, and the collapse came back as a sentence telling the user to bypass it", "refs": ["#808"], "ce": ["CE0463"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-widgets-silently-go-backwards-project-datawidgets-3-11-3.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-widgets-silently-go-backwards-project-datawidgets-3-11-3.json new file mode 100644 index 0000000000..9ae9a302a1 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-12-widgets-silently-go-backwards-project-datawidgets-3-11-3.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-12", "symptom": "Widgets silently go backwards: a project on DataWidgets 3.11.3 ends up running 3.4.0 widget code after updating an unrelated module (Atlas_Web_Content, FeedbackModule). `mx check` stays clean, `show modules` shows the new module version, and nothing mentions widgets", "cause": "A module `.mpk` bundles a copy of every widget its pages use, pinned at its author's release time — and different modules pin different versions of the **same** widget. `InstallPackageFiles` copied everything the package shipped, so the last module installed decided the widget versions. Measured on the published packages: Atlas_Web_Content 4.3.0 ships 5 Data Widgets at 3.4.0 against DataWidgets 3.11.3's 3.11.3", "file": "`cmd/mxcli/marketplace/widgetversion.go` (`widgetVersionInMpk`, `versionLess`, `unpackedTwinOf`), `update.go` (`InstallPackageFiles` returns `[]SkippedFile`), `cmd_marketplace_update.go` (`reportSkippedFiles`)", "insight": "**Read ``, not ``** — the root attribute is the manifest schema and is `1.0` on every widget ever published, so comparing it makes every version look equal, which is exactly as broken as not comparing. **An unparseable version must never count as \"older\"** — the default is to install, and a wrong \"older\" verdict silently withholds a file the package shipped. **Skipping must be reported**: a silent skip is the same class of bug as the silent overwrite it replaces. **Test the negative half** — a guard that skips every widget also passes \"the newer one survived\"; assert that a newer *and* an equal bundled widget still install. Same file handles FINDINGS §18: a package that ships a widget as both `.mpk` and unpacked tree (FeedbackModule 5.0.0, both 12.0.4) installs only the `.mpk`, but an unpacked widget with **no** packaged twin must still install. Verified against the real packages: 5 kept, 13 twin entries skipped. Reported in mxcli-chat FINDINGS §14/§18"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-13-constant-value-changed-disk-machine-store-project.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-13-constant-value-changed-disk-machine-store-project.json new file mode 100644 index 0000000000..07ea64f893 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-13-constant-value-changed-disk-machine-store-project.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-13", "symptom": "A constant value changed on disk (machine store, or the project configuration) does not reach an app that is **already running** — a `mxcli run --local` keeps serving the old value until it is restarted", "cause": "Constants are a boot-time layer: mxcli sends them once, in the `update_configuration` call at start. Changing them live needs the M2EE admin API, and needs BOTH of its calls", "file": "`cmd/mxcli/docker/localboot.go` (`ApplyConstants`, `LocalRuntime.BootConfig`), `cmd/mxcli/devloop_handshake.go`, `cmd/mxcli/cmd_constant.go` (`--apply`)", "insight": "**`update_configuration` is STAGED, not applied** — measured on 11.12.1: the running app keeps its old values, and the call still answers `result:0`. Only the next `reload_model` applies them, so a version sending just the first call reports success and changes nothing. That is why the two calls live in one function rather than at the call site. **Re-send the WHOLE payload**: the admin action has no read-back (`get_configuration`, `get_current_configuration`, `runtime_config`, `get_current_runtime_status` are all \"Action not found\"), so anything not sent is simply gone from the configuration afterwards — which is why the dev loop publishes what it booted with rather than a second process guessing. **A handshake from a dead process is refused, not used**: its ports may since have been taken by something else, and pushing a configuration change into the wrong process is worse than reporting no dev loop. **Say plainly what cannot be confirmed** — with no read-back, two successful calls are the strongest evidence available, so the command names what would actually confirm it (a microflow that returns the constant) instead of claiming success. **Failure to apply is not failure to set**: the value is already on disk and the next boot uses it, so `--apply` reports and returns rather than exiting non-zero over a half-succeeded command. Tests `cmd/mxcli/docker/localboot_constants_test.go`, `cmd/mxcli/devloop_handshake_test.go`. See `docs/11-proposals/PROPOSAL_constant_values.md` slice 4"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-13-constant-value-must-not-committed-has-nowhere-live-constant.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-13-constant-value-must-not-committed-has-nowhere-live-constant.json new file mode 100644 index 0000000000..c44d3ee9a6 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-13-constant-value-must-not-committed-has-nowhere-live-constant.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-13", "symptom": "A constant value that must not be committed has nowhere to live: a constant's default and a shared configuration override both go to git, and Mendix's own **private** configuration value — the correct slot — is encrypted per user account by Studio Pro from 10.9, so nothing headless can read or write it", "cause": "Not a defect so much as an absent layer. mxcli mirrors the concept with a store it can actually reach: `/.mxcli/constants.json`, mode 0600, gitignored, sitting between `--constant` and the configuration", "file": "`cmd/mxcli/constantstore/` (load/save), `cmd/mxcli/cmd_constant.go` (`constant set/unset/list`), `cmd/mxcli/constant_gitignore.go` (`ensureStoreIgnored`), `cmd/mxcli/constants_resolve.go` (`layerMachine`)", "insight": "**The promise has to be made true, then checked — asserting it is not enough.** `mxcli init` writes a `.gitignore` only when the project has none, and a Mendix project usually already has one, so the entry the whole layer rests on could simply be absent. `constant set` appends it *and then asks git*, refusing to write the value on \"not ignored\": a store that leaks is worse than no store. **Which rule defeats an ignore entry is not guessable** — `!.mxcli/**` does NOT re-include anything (git cannot re-include a file whose parent directory is excluded), while `!.mxcli` does; the test was corrected against measured git behaviour rather than the assumed case. **A corrupt store is an error, a stale entry is not**: an unparseable file means values the author set are about to be silently absent (fatal), while an entry naming a constant the project dropped is the user's own file and is skipped-and-named, or every run fails until it is hand-edited. **An empty store is deleted, not written as `{}`** — a file that configures nothing should not exist. Tests `cmd/mxcli/constantstore/store_test.go`, `cmd/mxcli/constant_gitignore_test.go`; verified live that the value reaches a running app and that `git status` never sees the file. See `docs/11-proposals/PROPOSAL_constant_values.md` slice 3"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-13-test-mdl-asserting-something-constant-feeds-passes-under.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-13-test-mdl-asserting-something-constant-feeds-passes-under.json new file mode 100644 index 0000000000..4ed9284692 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-13-test-mdl-asserting-something-constant-feeds-passes-under.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-13", "symptom": "A `.test.mdl` asserting on something a **constant** feeds passes under `mxcli test --attach` and fails under `mxcli test --local` (or the reverse), with nothing in either output to explain the difference", "cause": "`--local` boots an app of its own through `StartLocalApp`, whose options had no `ConstantOverrides` field — so it ran with each constant's **default** from `deployment/model/config.json`, while `--attach` runs against an app `run --local` booted, which applies the configuration's shared overrides. A constant resolving to the wrong value is not an error, so both runs reported success and only the assertion differed", "file": "`cmd/mxcli/docker/localapp.go` (`LocalAppOptions.ConstantOverrides`, `runtimeOptions`), `cmd/mxcli/testrunner/localapp_options.go` (new, shared by both `--local` runners), `runner.go` (`RunOptions.ConstantOverrides`), `cmd/mxcli/cmd_test_run.go` + `main.go` (`--configuration`)", "insight": "**A boot path that takes options needs one mapping function, not an inline struct literal per caller** — the two `--local` runners each built their own `LocalAppOptions`, so a field added for one would not reach the other, and neither had the constants. Both now go through `localAppOptions`, and the `LocalAppOptions`→`LocalRuntimeOptions` step is `runtimeOptions()` so the forwarding is assertable without booting anything: a dropped field there is otherwise invisible until an app runs with the wrong configuration. **Resolve in one place**: `cmd/` decides precedence and the runner only carries the map, or \"which configuration wins\" gets two answers. **Only report what this run actually uses** — `--attach` inherits the constants of the app it attached to, so resolving and printing them there would be a confident lie. Verified at the layer the symptom lives in (`.claude/skills/verify-in-runtime.md`): the same suite, one project, `--local` and `--attach` must agree, with the reverted-wiring control run showing the constant's default. Unit tests `cmd/mxcli/testrunner/localapp_options_test.go`, `cmd/mxcli/docker/localboot_constants_test.go`. See `docs/11-proposals/PROPOSAL_constant_values.md` slice 1"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-14-windows-defender-flags-mxcli-windows-release-binary-trojan.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-14-windows-defender-flags-mxcli-windows-release-binary-trojan.json new file mode 100644 index 0000000000..3029f5ddca --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-14-windows-defender-flags-mxcli-windows-release-binary-trojan.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-14", "symptom": "Windows Defender flags the mxcli **Windows** release binary as `Trojan:Script/Sabsik.EN.A!ml`; enterprise EDR (Defender for Endpoint, CrowdStrike, SentinelOne) blocks it harder. Not the generic unsigned-Go-binary false positive of #185", "cause": "The binary genuinely embedded **chisel**, a dual-use tunnelling/pivoting tool (SSH over WebSocket), on every platform — although the tunnel only ever runs inside a Linux container. `run --hub` linked `chisel/client`, `tunnel-hub` linked `chisel/server`, so windows/darwin carried 32 packages incl. the whole `x/crypto/ssh` stack for a feature they cannot use", "file": "`cmd/mxcli/docker/tunnel_linux.go` + `tunnel_other.go` (client seam), `cmd/mxcli/tunnelhub/control_linux.go` + `control_other.go` (server seam), `scripts/check-tunnel-deps.sh` (guard)", "insight": "**Never obfuscate, pack or rename to dodge the scanner** — attacker tradecraft, and it makes the binary less trustworthy, not more. **Code signing does not fix this class**: a signed binary containing chisel is still flagged behaviourally; signing only addresses #185's generic false positive. The fix is to stop shipping the capability where it is unused: one interface per seam, `_linux.go` impl + `!linux` stub, commands still registered everywhere but failing with an actionable message. **Prove absence three ways, and know that `go tool nm` is not one of them** — release ldflags `-s -w` strip the symbol table, so nm reports \"no symbols\" whether or not the code is linked and would give a false pass; use `go list -deps`, `go version -m`, and `strings` (nm only on a deliberately unstripped build). **Guard against the transitive path, not the name**: match the module list (`x/crypto/ssh`, `gorilla/websocket`, `armon/go-socks5`, `jpillora/*`) so re-entry without the word \"chisel\" still trips it, and assert a **positive control** (chisel IS in the linux graph) so the check cannot pass vacuously. Verified by re-adding the import and watching the guard fail on all four windows/darwin targets. Result: -13.5 MB (-14.7%) on windows+darwin, linux unchanged. See ADR-0009", "refs": ["#185"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-15-mxcli-test-reports-pass-assertion-must-fail-expect-1-2.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-15-mxcli-test-reports-pass-assertion-must-fail-expect-1-2.json new file mode 100644 index 0000000000..5822a37d0b --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-15-mxcli-test-reports-pass-assertion-must-fail-expect-1-2.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-15", "raw": "| `mxcli test` reports **PASS for an assertion that must fail** — `@expect 1 = 2`, `@expect length($result) = 999`, `@expect find($result, 'Z') >= 0` with the needle absent. Nothing in the output distinguishes a real assertion from a vacuous one, so a suite certifies work as verified while asserting only that the microflow did not throw. Mutation testing is what exposes it: mutants that return an obviously wrong value survive the suite | The `@expect` annotation was matched with a regex for one shape — `@expect $var (=|<>) ` — and `FindStringSubmatch` returning nil produced **no assertion at all** rather than an error. A test with zero assertions passes if its body completes. So the narrow support was not the defect; the silence was | `cmd/mxcli/testrunner/parser.go` (`expectPattern`, `parseAnnotations`), `cmd/mxcli/testrunner/expect.go` (new — `ParseExpect`, the validating parser), `cmd/mxcli/testrunner/generator_endpoint.go` + `generator.go` (emit the condition, not a rebuilt equality), `cmd/mxcli/testrunner/results.go` (`expectErrorResult`) | Capture the **whole** annotation body and hand it to a validating parser; anything it cannot compile becomes an `ExpectErrors` entry, the test is not generated at all, and the runner reports `StatusError` (which `FailCount` counts, so the exit code is non-zero). The parser is a strict recursive-descent pass over `exprcheck.Lex` — **not** `mdl/exprcheck`'s own parser, which recovers and emits hints, exactly the wrong behaviour here. Two measurements pinned the emitted expression against mxbuild 11.6.6: `<>` really is CE0117 (so the rewrite to `!=` is load-bearing, not cosmetic) and a wrong-typed comparison really is caught (`$result = 3` → CE0117), which is what makes the 0-error run on the 11 generated shapes mean something. **Generalisable**: when a pattern-matching parser can match *less* than its input, the non-match branch is a silent-failure path — audit every `if m := re.FindStringSubmatch(...); m != nil` whose else-branch does nothing. Repro `mdl-examples/bug-tests/expect-vacuous-assertions.mdl`. mxcli-sudoku FINDINGS #46 |", "refs": ["#46"], "ce": ["CE0117"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-16-test-suite-s-green-unreadable-test-asserts-nothing-prints.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-16-test-suite-s-green-unreadable-test-asserts-nothing-prints.json new file mode 100644 index 0000000000..9bb5b440f3 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-16-test-suite-s-green-unreadable-test-asserts-nothing-prints.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-16", "symptom": "A test suite's green is unreadable: a test that asserts **nothing** prints the same `PASS` as one with six assertions, and `@verify` — documented as an OQL post-condition — is parsed and evaluated by nothing at all. After @expect started failing closed, the cheapest way back to green is to delete the assertion, and the output cannot tell that apart from a repair", "cause": "Two silent-absence paths rather than the silent-drop path fixed in the row above. `TestResult` carried no assertion count, so nothing downstream could report one; and `TestCase.Verify` was populated by `parseAnnotations` and read by nothing but `--list` — `grep -n '\\.Verify' cmd/mxcli/testrunner/*.go` returns the parser and the lister, no runner", "file": "`cmd/mxcli/testrunner/results.go` (`TestResult.Assertions`/`SourceFile`, `newResult`, `vacuousResult`, `resultNote`, `VacuousCount`), `cmd/mxcli/testrunner/parser.go` (`AssertionCount`, `AssertionErrors`, the @verify rejection), `cmd/mxcli/testrunner/junit.go` (`junitClassName`, assertions property), `cmd/mxcli/main.go` + `cmd_test_run.go` (`--require-assertions`)", "insight": "Count assertions on the test case and carry them onto every result through **one** constructor (`newResult`) — the previous code built `TestResult` literals at five sites, which is exactly how a new field gets populated in one path and silently missing in another. Report the count on the ordinary result line, not behind `--verbose`: the whole lesson of #46 is that the *default* output must distinguish a test that asserted from one that did not. Vacuous tests warn by default and error under `--require-assertions`, because a smoke test is legitimate but an indistinguishable one is not. **Generalisable**: when auditing an annotation/config field for dead ends, grep for *readers*, not writers — a field with a parser and no consumer is a feature the docs promise and the code does not deliver, and it fails silently by construction. mxcli-sudoku FINDINGS #46 (follow-up)", "refs": ["#46"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-mxcli-oql-fails-every-mendix-older-than-11-11-action-not.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-mxcli-oql-fails-every-mendix-older-than-11-11-action-not.json new file mode 100644 index 0000000000..ac8605757e --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-mxcli-oql-fails-every-mendix-older-than-11-11-action-not.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-17", "symptom": "`mxcli oql` fails on every Mendix older than 11.11 with *\"Action not found ... upgrade mxcli\"*, and separately reports a **rejected query as `0 rows`** rather than as an error", "cause": "Two independent misreadings of the runtime's replies. (1) The 11.11+ REST route `/dev/preview_execute_oql` does not exist on older runtimes, and the admin API does not 404 it — it dispatches the POST as an ordinary admin request, finds no `action` field, and answers **HTTP 200** with `{\"result\":1,\"message\":\"Action not found\"}`. The fallback to the legacy M2EE action keyed on the 404 alone, so the legacy action — which works fine there — was never tried. (2) That legacy action reports a bad query as `{\"feedback\":{\"error\":\"...\"},\"result\":0}` — inside the feedback, with a **success** result code — so `M2EEError()` (which keys off the result) says nothing and the error body parses as an empty result", "file": "`cmd/mxcli/docker/oql.go` (`legacyOQL`, `oqlDevErrorKind`, the `error` field in `parseOQLFeedback`'s envelope)", "insight": "Fall back to the legacy action on *either* absence signal, and surface `feedback.error` regardless of the result code. Measured on 11.6.6: before, `mxcli oql` could not run any query; after, `select count(*) as n from Mod.E` returns a row, and a bad query is an error instead of `0 rows`. **Generalisable**: when a fallback is keyed on one specific failure signal, check what the *other* end actually sends — an HTTP-level 404 and an application-level \"not found\" are different wires, and a success code next to an error message is common enough to assume it happens. Found while wiring @verify (FINDINGS #48); related to #39", "refs": ["#39", "#48"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-run-local-watch-deploys-half-applied-model-mxcli-exec.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-run-local-watch-deploys-half-applied-model-mxcli-exec.json new file mode 100644 index 0000000000..7f98468bf3 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-run-local-watch-deploys-half-applied-model-mxcli-exec.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-17", "symptom": "`run --local --watch` deploys a **half-applied model** when an `mxcli exec` is running, and there is no way to recover: re-running the script writes nothing (it is byte-idempotent), so nothing re-triggers the watcher and the stale build stands", "cause": "`watchAndApply` (`cmd/mxcli/docker/runlocal.go`) rebuilt on the **first** mtime bump. An exec rewrites the `.mpr` and many `mprcontents/*.mxunit` over seconds, so the build snapshots the tree mid-write. There was a settle for the web bundler and nothing for the model. Two behaviours each correct alone: idempotency removed a *recovery path* nobody had written down as one — \"just run it again\" was load-bearing", "file": "`cmd/mxcli/docker/runlocal.go` (`settleSource`, `sourceSettleWindow`, the `case <-ticker.C` branch)", "insight": "Wait for the source mtime to stop advancing (two poll intervals of quiet) before building. The wait is unbounded on purpose — a long exec is the case it exists for, and building late beats building mid-write — but it still honours the interrupt channel so Ctrl-C is not swallowed. `touch` on the `.mpr` remains the force-rebuild hatch, now documented. **Generalisable, twice over**: (a) a change *signal* is not a change *event* — anything polling mtime must debounce or it samples a writer mid-flight; (b) when adding an optimisation that skips work, ask what informal recovery procedure depended on that work happening. **Test trap hit while fixing it**: the first version asserted the file count *after* `wg.Wait()`, which holds against a `settleSource` that returns immediately — the assertion has to read the writer's state at the moment it returned. mxcli-sudoku FINDINGS #45", "refs": ["#45"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-running-mxcli-test-dirties-project-git-status-reports-mpr.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-running-mxcli-test-dirties-project-git-status-reports-mpr.json new file mode 100644 index 0000000000..42ddc93f6b --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-running-mxcli-test-dirties-project-git-status-reports-mpr.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-17", "symptom": "Running `mxcli test` dirties the project: `git status` reports the `.mpr` modified after a run that changed nothing, so a \"run the tests, assert the tree is clean\" CI step fails and a pull request carries a meaningless diff. `mprcontents/` is byte-identical; only the `.mpr` differs, and a `.mpr` diff is opaque", "cause": "The runner injects an `MxTest` module and takes it back out. Cleanup restores the model, but every unit write stamps a fresh UUID into the `.mpr`'s `_Transaction.LastTransactionID` (`updateTransactionID`, in both engines' writers) **and** the insert/delete cycle relays SQLite's pages. Restoring the row alone is measurably insufficient: three consecutive runs then hold the id stable and still produce a different file hash each time", "file": "`cmd/mxcli/testrunner/snapshot.go` (new — `projectSnapshot`, `documentTreeDigest`), `cmd/mxcli/testrunner/runner.go` (`captureProjectState`, `restoreProjectFile` at all three cleanup sites), `cmd/mxcli/testrunner/host.go` (`HostedEndpoint.Remove`)", "insight": "Snapshot the whole `.mpr` before injecting and write it back after a successful cleanup — byte-exact by construction rather than by enumerating what might have changed. Two refusals are load-bearing: **cleanup failed** and **the `mprcontents/` tree moved**. In either case the project is not in the state the snapshot describes, and restoring would turn a visible harmless discrepancy into an invisible misleading one — a tree that reads as clean while the model is not. Write through a temp file + rename so an interrupted restore cannot truncate the `.mpr`. **Generalisable**: a tool that mutates a project to do its work owes a restore that is byte-exact, not semantically equivalent — every consumer downstream of it compares bytes. And when the obvious cause is a single row, measure the file hash before concluding it is the only one. Verified with the reporter's own harness: 3 runs, `.mpr` and `mprcontents` hashes unchanged throughout. mxcli-sudoku FINDINGS #47", "refs": ["#47"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-verify-cannot-fail-no-oql-well-formed-not-true-not-against.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-verify-cannot-fail-no-oql-well-formed-not-true-not-against.json new file mode 100644 index 0000000000..81f31af027 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-17-verify-cannot-fail-no-oql-well-formed-not-true-not-against.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-17", "symptom": "`@verify` cannot fail. No OQL — well-formed or not, true or not, against a real entity or an invented one — makes a test fail: `@verify select count(*) as n from Mod.Game = 999999` reports PASS on a table with one row, and so does `this is not a query`", "cause": "The annotation was parsed into `TestCase.Verify` and read by nothing but `--list`. Same silent-absence class as the dropped `@expect`, in the annotation covering the *harder* half of Mendix testing: most microflows are side effects, so asserting on rows written is the only way to test them", "file": "`cmd/mxcli/testrunner/verify.go` (new — `ParseVerify`, `scalarOf`, `compare`, `runVerifies`), `cmd/mxcli/testrunner/parser.go` (`checkVerifyCleanup`), `runner_endpoint.go` + `runner_attach.go` + `watch.go` (`adminOptions` on `testTarget`), `runner.go` (`rejectVerifyOnLegacyRunner`)", "insight": "Run the OQL over the admin API after the microflow returns, compare against a literal, and keep three outcomes apart: holds → unchanged, does not hold → FAIL with the observed value, cannot be evaluated → ERROR. Three things are enforced rather than left to trip you up, each learned by running it: **`@cleanup rollback` is refused** (the default undoes the writes before the query could see them, so it would assert against the pre-test state); **the result must be one row × one column** (picking a cell out of a table is a guess); and the **legacy after-startup runner refuses the whole suite**, since its tests run during boot with no seam to query at. Split the expected value off at the **last** comparison operator outside quotes and parens, then require the RHS to be a literal — without that check the split silently eats a `where x = 1` and sends a truncated query. **Generalisable**: unit tests with a stubbed endpoint proved the logic and would have shipped it broken — the first real run against a booted app found the OQL endpoint unreachable on that Mendix and the alias requirement, neither of which any stub could show. mxcli-sudoku FINDINGS #48", "refs": ["#48"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-18-mxcli-check-script-mdl-p-app-mpr-prints-unqualified-check.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-18-mxcli-check-script-mdl-p-app-mpr-prints-unqualified-check.json new file mode 100644 index 0000000000..b151d896bd --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-18-mxcli-check-script-mdl-p-app-mpr-prints-unqualified-check.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-18", "raw": "| `mxcli check script.mdl -p app.mpr` prints an unqualified `Check passed!` having resolved **nothing** against the project — a misspelled icon, entity or page name sails through the command that was handed the model. Reported as \"check does not validate icon names\", which it does | `cmd_check.go` gated the whole reference pass on `--references`, so supplying `-p` alone ran only the model-free rules. `validate_icon_refs.go` already existed and works (it names the offending icon and the `describe icon collection` command); it returns early when `!ctx.Connected()`, so it simply never ran | `cmd/mxcli/cmd_check.go` (`checkRefs = checkRefs || projectPath != \"\"`, the qualified pass message, help text), `cmd/mxcli/main.go` (flag help) | Make `-p` imply reference resolution — someone who hands the command a project has said what they want — and keep `--references` accepted so existing invocations survive. A run *without* a project now qualifies its verdict by naming what was not resolved. **Generalisable, and the third instance this month**: a clean report that cannot distinguish *checked and clean* from *not checked* is the same defect as a vacuous test assertion. When a feature needs two flags to do its job, the second one is usually a bug. Before adding a validator, check whether one exists and is merely unreachable. mxcli-dbreplication F5 |"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-18-skill-pack-mxcli-ships-teaches-guard-cannot-fire-references.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-18-skill-pack-mxcli-ships-teaches-guard-cannot-fire-references.json new file mode 100644 index 0000000000..3c22161def --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-18-skill-pack-mxcli-ships-teaches-guard-cannot-fire-references.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-18", "symptom": "A skill pack mxcli ships teaches a guard that cannot fire: `references/patterns.md` shows a splice caller doing `IF $Q/Rejected THEN -- fail the request`, and a project wrote and then deleted a Java action for that branch", "cause": "The pack's **own** `QueryObject.java` throws `IllegalArgumentException` when `r.rejected && rejectUnsupported` **before** `Core.instantiate`, so a caller that receives a Query at all always has `Rejected = false`. The code is right and the documentation is wrong — and two further places repeated the claim (`SKILL.md`'s field table said `both`, `failure-modes.md` said \"Pass `true`: `Rejected` comes back set\")", "file": "`.claude/skills/packs/mendix-odata-pushdown/references/patterns.md`, `SKILL.md`, `references/failure-modes.md` (embed under `cmd/mxcli/skillpacks/` is regenerated by `make build`)", "insight": "Delete the dead branch from the pattern and say why it cannot run, quoting the throw; mark `Rejected` as a **bind**-caller field. **Generalisable**: when a doc and the code it documents disagree, grep the whole pack — the claim is rarely in one place, and the two secondary mentions were the ones that would have re-taught it. A documented safeguard that silently is not one is the same defect class as a vacuous assertion. mxcli-owid #30", "refs": ["#30"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-19-run-local-test-local-macos-windows-die-raw-fork-exec-mxcli.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-19-run-local-test-local-macos-windows-die-raw-fork-exec-mxcli.json new file mode 100644 index 0000000000..e51aff9e35 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-19-run-local-test-local-macos-windows-die-raw-fork-exec-mxcli.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-19", "symptom": "`run --local` / `test --local` on macOS (or Windows) die with a raw `fork/exec ~/.mxcli/mxbuild//modeler/mxbuild: exec format error`, while `setup mxbuild` and `docker build` correctly use Studio Pro's binary. `--mxbuild-path` does not help", "cause": "`MxBuildCDNURL` branches on **GOARCH only, never GOOS** — both URLs are Linux tarballs (Mendix ships macOS mxbuild inside Studio Pro, not on the CDN), so an arm64 Mac caches a Linux *aarch64* ELF: the arch matches, so nothing notices until exec. `docker build` resolves via `resolveMxBuild` (PATH → Studio Pro → known locations → cache) and `setup mxbuild` via `NativeMxBuildForSetup`, but the local loop called `DownloadMxBuild` directly at `runlocal.go` and `StartServe` looked only at the cache. `LocalRunOptions.MxBuildPath` was documented as an override and never read for the serve binary, so there was no workaround either", "file": "`cmd/mxcli/docker/mxbuild_platform.go` (new: `ResolveMxBuildForLocal`, `binaryOS`, `verifyRunsHere`) + `runlocal.go` (the `DownloadMxBuild` call) + `mxserve.go` (`StartServe`)", "insight": "Reuse the rule the codebase already had rather than inventing one: `NativeMxBuildForSetup(goos, version)` returns Studio Pro's path, or \"\" meaning \"Linux, download is fine\", or an error plus guidance. Order: explicit path → (non-Linux) Studio Pro → cache/CDN. **Do not stop the download on Windows** — the cache holds a Linux binary there deliberately, for Docker builds; the fix belongs at resolve/exec time. Guard exec with a magic-byte check (ELF / Mach-O incl. fat / PE) and let an unrecognised format through, since a shell wrapper has no magic and refusing on a guess would block a working setup. **Inject `goos` into the helpers** (`resolveMxBuildForLocalOn`, `verifyRunsOn`): the whole bug is platform-specific and is otherwise untestable from a Linux runner. Repro without a Mac: plant a Mach-O magic at the cache path and call `StartServe` — reproduces the reporter's message verbatim. Tests `TestBinaryOS`, `TestVerifyRunsOn_LinuxBinaryOnMac`, `TestResolveMxBuildForLocal_*`; reverting makes `ExplicitPathWins` fail **by downloading from the CDN for 34s**, which is the ignored override made visible. **Unverified:** no macOS host was available, so the Studio Pro discovery path itself (`resolveStudioProDirMacOS`) is exercised only by its own existing tests. Issue #916", "refs": ["#916"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-19-windows-run-local-behaves-though-java-were-undetected-local.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-19-windows-run-local-behaves-though-java-were-undetected-local.json new file mode 100644 index 0000000000..aab411f0e0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-19-windows-run-local-behaves-though-java-were-undetected-local.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-19", "symptom": "Windows: `run --local` behaves as though Java were undetected, and a local run can fail looking for Gradle", "cause": "Two separate things. (a) **The `.exe` suffix**: `isJDK21` appended it, every CONSUMER did not — `--java-exe-path` handed to mxbuild and the path `exec.Command` runs to boot the runtime were both `\\bin\\java`, so a correctly-detected JDK was passed on in a form that need not resolve. Five sites built it by hand. (b) **Gradle is not mxcli's**: it ships inside the mxbuild bundle (`modeler/tools/gradle`, 8.5) and mxbuild invokes it — mxcli never calls gradle, so \"Gradle missing\" indicates a foreign/incomplete mxbuild bundle, usually #916", "file": "`cmd/mxcli/docker/javaexe.go` (new `JavaExePath`) used from `mxserve.go`, `build.go`, `settle.go`, `localboot.go`, `detect.go`; `jdkSearchPathsFor` in `detect.go`", "insight": "One helper, not five hand-built joins — the sixth call site would have repeated the bug. **Check the platform's own docs before adding a search path**: \"add Studio Pro's JDK\" turned out to be a non-task, because Mendix's install guide says Studio Pro installs **Eclipse Temurin 21** rather than bundling a JDK, so the existing Adoptium glob already IS Studio Pro's JDK; what was genuinely missing was the per-user `%LOCALAPPDATA%\\Programs` install location. Inventing a `Mendix\\\\jdk` path would have been dead code. Make the not-found error list what was searched — \"JDK 21 not found\" alone sends a user reading mxcli's source. Inject `goos` (`jdkSearchPathsFor`, `javaExeName`): the only Windows CI job is the tunnel seam, scoped with `-run`, so it compiles Windows code and executes almost none of it — which is exactly how a missing `.exe` survives. **Unverified**: no Windows host; the fix is code-level with OS-injected tests. Reported via a user relay, not an issue", "refs": ["#916"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-20-cli-flag-accepted-but-does-nothing-mxcli-test-tests-p-app.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-20-cli-flag-accepted-but-does-nothing-mxcli-test-tests-p-app.json new file mode 100644 index 0000000000..0a6d89eede --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-20-cli-flag-accepted-but-does-nothing-mxcli-test-tests-p-app.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-20", "symptom": "A CLI flag is accepted but does nothing: `mxcli test tests/ -p app.mpr --require-assertions` exits 0 on a suite where every test asserts nothing. `--help` lists the flag, it parses, and the summary line even prints \"1 test(s) asserted nothing\" — so the run looks like it noticed and chose to pass", "cause": "The flag was consulted in exactly one of the two result-assembly loops. `vacuousResult` had a single caller, in the endpoint runner's loop (`--local`, `--attach`); the after-startup runner (Docker default, and `--local --legacy-runner`) assembles results in `ParseLogResults(logReader, suite)`, whose signature had no parameter the flag could arrive through — it could not honour it even in principle", "file": "`cmd/mxcli/testrunner/results.go` (`ParseLogResults`, `preRunResult`), `cmd/mxcli/testrunner/runner_endpoint.go` (`runSuite`), `cmd/mxcli/testrunner/runner.go` (the `ParseLogResults` call in `runAfterStartup`)", "insight": "Give the pre-run verdicts — the ones decided by the *parsed test case* rather than by the run — **one** function (`preRunResult`) and route both loops through it, then pin that it is their only caller (`TestPreRunVerdictsHaveOneCallSite`). Same lesson as the `newResult` constructor two rows up: a decision reachable from one path and not the other is the defect, and a single call site is what prevents the next one. **Do not reach for the refusal precedent without checking it applies**: `rejectVerifyOnLegacyRunner` refuses `@verify` on this runner because it genuinely cannot evaluate it (its tests run during boot, there is no seam to query the app at). `AssertionCount()` is a static property of the parsed test case, known before any runner starts — refusing there would have denied a capability the runner has. **Generalisable**: when auditing a flag for dead ends, grep for its *consumers* and count them against the number of code paths that should honour it; a unit test on the helper (`TestRequireAssertionsMakesVacuousTestsErrors` called `vacuousResult` directly) proves the helper works and says nothing about whether any runner calls it. Issue #926", "refs": ["#926"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-21-mxcli-test-test-mdl-file-whose-first-test-preceded-file.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-21-mxcli-test-test-mdl-file-whose-first-test-preceded-file.json new file mode 100644 index 0000000000..23bf3c7353 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-21-mxcli-test-test-mdl-file-whose-first-test-preceded-file.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-21", "symptom": "`mxcli test`: (a) a `.test.mdl` file whose first test is preceded by a **file-level `/** … */` header** reports one test fewer — the first one vanishes with no pass, no fail and no error, and every later test renumbers over it; the workaround (a `--` header) re-triggers it the moment that header's **prose spells out `/**` and `*/`**. (b) `@expect count($Var) = N` on a bare `retrieve` block reported **PASS unconditionally** on v0.18.0, for any N, against an empty table", "cause": "Two independent \"substring instead of structure\" defects in `cmd/mxcli/testrunner`. (a) `extractDocAndBody` took the FIRST `/**`…`*/` in a `/`-separated chunk as the test's doc comment. A header is not separated from the test below it by a `/`, so both are one chunk: the header became the doc, carried no `@test`, and the chunk — real test included — was skipped. The delimiters were found by `strings.Index` with no notion of comment or string state, which is why writing them inside a `--` comment recreated the fusion. Test IDs were numbered by **chunk index**, so a skipped chunk left a gap (`test_2` as a file's only test) and the line number came from searching the whole file for the chunk's first 20 bytes (wrong chunk when two start alike; **panics** on a chunk shorter than that). (b) `count()` is not a Mendix expression *function* — counting a list is an Aggregate list **activity** — so `ParseExpect` could not compile it. Pre-0bf9382 that dropped the annotation, and a test with no assertions passes as long as its body does not throw", "file": "`cmd/mxcli/testrunner/parser.go` (`splitTestBlocks`, `extractDocAndBody`, `scanDocComments`, `skipMDLString`), `expect.go` (`expectAggregate`, `parseAggregate`, `collectAggregates`), `generator_endpoint.go` (`writeExpectAggregates`), `generator.go` (`renameExpect`)", "insight": "Scan the chunk (whitespace / `--` / `/* */` / `'…'`) instead of searching it: the doc is the **last** comment of the leading run, the body starts after it, and the line travels with the chunk from `splitTestBlocks`. Number tests, not chunks. **Two `@test` docs in one chunk is refused**, not resolved — either resolution runs one and drops the other, which is the bug. For (b), hoist the aggregate into the activity the author would write (`$mxtest_count_X = COUNT($X);`) ahead of the decision, and refuse `sum`/`average`/`minimum`/`maximum` (they need an attribute an assertion cannot supply) with the helper-microflow workaround named. In the monolithic runner the generated variable must be renamed per test like the body's own, or two tests counting same-named lists declare it twice. **Verify (b) at the Mendix layer, not the parser's**: `mxcli check` accepts plenty that mxbuild rejects — the generated flows were exec'd into a real 11.13.0 project and `mx check` run with a pristine-copy baseline (1 pre-existing error either side). **Controls**: pre-fix binary on the repro file finds 2 of 4 tests; stubbing `isListAggregate` to false restores \"count() is not a Mendix expression function\". Repro `mdl-examples/bug-tests/927-test-mdl-leading-comment-and-count.test.mdl`. Issue #927", "refs": ["#927"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-21-test-carrying-both-throws-expect-reports-making-two.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-21-test-carrying-both-throws-expect-reports-making-two.json new file mode 100644 index 0000000000..16bff83bb7 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-21-test-carrying-both-throws-expect-reports-making-two.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-21", "symptom": "A test carrying both `@throws` and `@expect` reports making two assertions and makes one: the `@expect` is never evaluated, silently. It cannot fail, whatever it claims", "cause": "`@throws` selects a different generated shape in **both** generators (`writeThrowsFlowBody` / `writeThrowsTestBlock`, where the verdict starts as a failure and only the error handler clears it) and neither emits the `@expect` checks — but `AssertionCount` still counted them. Same silent-absence class as the dropped `@expect` and the unimplemented `@verify`, in the one combination where the assertion cannot be made to work at all: the body was expected not to produce a result to assert on", "file": "`cmd/mxcli/testrunner/parser.go` (`checkThrowsExpect`, called from `parseAnnotations` beside `checkVerifyCleanup`)", "insight": "Move the expects to `AssertionErrors` and clear them, so the test is an ERROR and gets no microflow — the annotation pipeline's fail-closed rule. Keep the `@throws` itself: it is the assertion that still stands. **Prove it end to end, not on the struct**: the visible symptom is `AssertionCount` and the generated MDL, so the test parses a file, checks the count, and greps the generated flow for the microflow's absence. Sibling of the `@verify`-under-rollback refusal, which is the precedent for the message shape (name the annotation, name the one-line change)"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-21-test-mdl-test-gains-assertion-cleanup-strategy-expected.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-21-test-mdl-test-gains-assertion-cleanup-strategy-expected.json new file mode 100644 index 0000000000..80ce9e88e5 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-21-test-mdl-test-gains-assertion-cleanup-strategy-expected.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-21", "symptom": "A `.test.mdl` test gains an assertion, a cleanup strategy or an expected error **nobody wrote**, because a sentence in its doc comment quotes the annotation — writing \"`@expect $x = 1` in a sentence\" gives the test that assertion, and \"@cleanup none would apply here\" changes its cleanup strategy. The invented assertion usually fails to compile, so the test reports an ERROR whose message quotes prose", "cause": "The annotation patterns in `parseAnnotations` were unanchored (`@expect\\s+(.+)`), so they matched anywhere in a line rather than at its start. An annotation is a javadoc **tag**, and a tag opens its line", "file": "`cmd/mxcli/testrunner/parser.go` (the six patterns in the `var (…)` block)", "insight": "Anchor every pattern with `^`. The leading `*` and its indentation are already stripped before they run, so the real spellings — at any javadoc indentation, and the one-line `/** @test x */` form — are unaffected; keep a control asserting that, or a fix that simply stops reading annotations passes. Found while writing the #927 repro file: its header explained the bug, and explaining it created an assertion that then errored against a test nobody had written. **Generalisable**: the sibling of #927's own root cause — a delimiter means nothing outside the structure it delimits, whether that structure is a comment or a line", "refs": ["#927"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-22-bundled-skill-renders-stray-fence-duplicated-name-top-body.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-22-bundled-skill-renders-stray-fence-duplicated-name-top-body.json new file mode 100644 index 0000000000..7e9484b26e --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-22-bundled-skill-renders-stray-fence-duplicated-name-top-body.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-22", "symptom": "A bundled skill renders with a stray `---` fence and a duplicated `name:` at the top of its body, and `mxcli init` ships it that way. `TestEmbeddedSkillsCarryAgentSkillsFrontmatter` passes — the frontmatter it checks is valid", "cause": "The #906 migration **prepended** a `name`/`description` block to every skill, including the one file that already had one (`custom-widgets`, under its pre-rename name `mendix-custom-widgets`). Everything after the FIRST `---` block is body, so the old block became visible text. The test's `frontmatter.FindSubmatch` matches the first block and validates it correctly, which is precisely why it is blind to a second one", "file": "`.claude/skills/mendix/custom-widgets/SKILL.md` (two blocks merged into one, keeping the old block's trigger words — child slots, `.def.json`, engine internals); guard added to `cmd/mxcli/init_skills_standard_test.go`", "insight": "**A first-block check cannot see a rename-and-prepend**, and that is the exact shape of a migration that renames things — so a bulk frontmatter edit needs a guard against a SECOND block, not a better check of the first. **Measure the pre-state before a bulk prepend**: 1 of 68 files already had frontmatter, which was known and written down, and the migration script still did not branch on it. **Do not attribute a defect to the change you are looking at** — this was reported against PR #222, whose diff touches only the `description:` inside the first block and shows the stray `---` as unchanged context; `git log -S 'name: mendix-custom-widgets'` settles it in one command. Two adjacent traps from the same report: `make test` did not depend on `sync-all` while `make build` did, so a bare `go build` left the generated `cmd/mxcli/skills` stale and six tests failed — and the failure message named the `go:embed` directive, which was fine, instead of the missing build step. A **differential across refs is only as sound as the procedure both sides share**: the same stale build made main fail 4 and the branch fail 6, which looked like a regression and was a test-count difference. mxcli-formula1 finding 68", "refs": ["#222", "#906"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-applying-several-themes-set-never-idempotent-mxcli-fonts-ofl.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-applying-several-themes-set-never-idempotent-mxcli-fonts-ofl.json new file mode 100644 index 0000000000..f4c797958c --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-applying-several-themes-set-never-idempotent-mxcli-fonts-ofl.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "Applying several themes as a set is never idempotent — `mxcli-fonts/OFL.txt` reports `updated` on every run, forever — and the installed licence covers the wrong fonts", "cause": "Every theme vendored a different font family and wrote its SIL OFL text to the *same* path. With one theme installed the last apply won and it settled; with a set, each theme overwrote the previous one on every pass", "file": "`cmd/mxcli/theme/assets/*/files/theme/web/mxcli-fonts/OFL-.txt` (renamed per theme)", "insight": "**Two themes writing different content to one verbatim path is a licensing bug before it is a churn bug** — the churn is just the symptom that made it visible. `TestThemesDoNotClaimTheSameVerbatimPathWithDifferentContent` hashes every non-fenced file across every theme and fails on a collision; controlled by renaming them back, which reproduces the original failure"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-brand-palette-hand-edited-into-theme-web-custom-variables.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-brand-palette-hand-edited-into-theme-web-custom-variables.json new file mode 100644 index 0000000000..52c7921204 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-brand-palette-hand-edited-into-theme-web-custom-variables.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "A brand palette hand-edited into `theme/web/custom-variables.scss` works once, and then `mxcli theme apply` reports the file as `skipped` and refuses to touch it — the project has silently taken the theme out of mxcli's hands to change one colour", "cause": "The theme registry was embed-only: `List`/`Get` read `assetsFS` and nothing else, so there was nowhere to put a fourth theme and the generated block was the only place to put a palette", "file": "`cmd/mxcli/theme/source.go` (the two-source registry), `cmd/mxcli/theme/create.go` (`theme create`), `cmd/mxcli/theme/tokens.go`", "insight": "**A digest fence that refuses an edit is a signal that the thing being edited should have been authorable.** Project-local themes live in `theme/mxcli-themes/` — committed (`.mxcli/` is gitignored by `mxcli init`) and not compiled (mxbuild's entry point is `theme/web/main.scss`; it does not glob `theme/`, verified against a real 11.13 build that nothing under it reaches `deployment/`). Scaffolding **must** rename the identifiers built from the theme name (`@mixin mxcli--`, the `@import`) — a plain copy collides the moment both themes exist. Verified end to end: seeded palette → `mxbuild --target=deploy` → measured `--brand-primary` in a real browser in both variants. Tests `cmd/mxcli/theme/create_test.go`, `cmd/mxcli/cmd_theme_test.go`"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-design-derived-mxt-token-written-into-theme-theme-applies.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-design-derived-mxt-token-written-into-theme-theme-applies.json new file mode 100644 index 0000000000..6a0cb16fd8 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-design-derived-mxt-token-written-into-theme-theme-applies.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "A design-derived `--mxt-*` token is written into a theme, the theme applies with every file reporting `created`, and the app renders exactly as before — indistinguishable from the design never having been applied", "cause": "Nothing reads an unrecognised custom property, so a typo (`--mxt-brand-color` for `--mxt-brand`) is a no-op with no error anywhere in the chain", "file": "`cmd/mxcli/theme/create.go` (`validateTokens`, `knownTokens`)", "insight": "**Validate a token vocabulary at authoring time or the failure has no symptom.** `theme create --from` refuses a `--mxt-*` name the base theme does not declare and names it; `mxcli theme show ` prints the vocabulary. Related trap: seed only the *scoped* extras into the alt-palette mixin — a variant-independent token like `--mxt-radius` belongs in the palette, and copying it into both means retuning it twice"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-javascript-action-parameter-throws-param-not-defined-first.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-javascript-action-parameter-throws-param-not-defined-first.json new file mode 100644 index 0000000000..86163935aa --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-javascript-action-parameter-throws-param-not-defined-first.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "A JavaScript action with a parameter throws ` is not defined` the first time a user clicks it. `mx check` reports 0 errors, `mxcli check` says nothing, the build is clean, and the generated file cannot be patched — it is rewritten on every build", "cause": "mxbuild **lowers the first letter** of a parameter when it generates the wrapper: `Skin: String` reaches the body as `skin`. A body written with the modelled spelling is a ReferenceError, and nothing between MDL and the click can see it — Mendix treats the body as opaque user code", "file": "`cmd/mxcli/theme/switcher.go` (both parameterised actions); the same rule applies to any `create javascript action` with parameters", "insight": "**Model the parameter capitalised, read it lowercased.** Verified against a real 11.13 build (`export async function SetAppSkin(skin)`) and in a browser by importing the generated module and calling it — `ReferenceError: Skin is not defined` before, the correct return after. The guard walks *every* parameter of *every* generated action, so a new one cannot reintroduce it. Note the trap in writing that guard: a regex anchored on `javascript action` also matches `call javascript action` in the nanoflows, which made the first version run past a real declaration and pass against the very defect it was written for — anchor on `create or modify javascript action` and assert the match count. ako/mxcli-ledger #135", "refs": ["#135"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-mxcli-lint-format-json-jq-fails-invalid-numeric-literal-line.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-mxcli-lint-format-json-jq-fails-invalid-numeric-literal-line.json new file mode 100644 index 0000000000..331fd5c66b --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-mxcli-lint-format-json-jq-fails-invalid-numeric-literal-line.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-25", "raw": "| `mxcli lint --format json \\| jq .` fails with `Invalid numeric literal at line 1`; `--format sarif > results.sarif` writes a corrupt file. stdout begins `Connected to: …` / `Loading cached catalog…` / `✓ Catalog ready` before the payload | The executor is constructed with `executor.New(os.Stdout)` via `newLoggedExecutor`, so every progress line shares the stream the payload is written to. Independent of which formatter runs — the chatter is emitted during connect and catalog build, before the formatter is reached | `cmd/mxcli/output_stream.go` (`isMachineReadableFormat`, `progressSink`), `cmd/mxcli/main.go` (`newLoggedExecutorTo`), `cmd/mxcli/cmd_lint.go`, `cmd/mxcli/cmd_report.go` | Pick the executor's writer from the resolved format: stderr when stdout carries a machine-readable payload (json/sarif/html/markdown), stdout otherwise. Route to **stderr, not io.Discard** — the progress is wanted on a terminal and in CI logs, it was merely on the wrong stream. `mxcli report` needs the extra condition that `-o ` frees stdout, so progress goes back to stdout there. Text mode must be left alone; assert that, or the fix silently degrades interactive use. Two commands share this because both build a catalog before formatting; any future command that prints a payload to stdout needs `progressSink` too. Issue #904 (residue) |", "refs": ["#904"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-mxcli-test-panics-slice-bounds-out-range-1-extractdocandbody.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-mxcli-test-panics-slice-bounds-out-range-1-extractdocandbody.json new file mode 100644 index 0000000000..f4c62b1296 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-mxcli-test-panics-slice-bounds-out-range-1-extractdocandbody.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "`mxcli test` **panics** — `slice bounds out of range [:-1]` in `extractDocAndBody` — on a `.test.mdl` saved with **CRLF** line endings; the identical file with LF works. Separately, **one unparseable file aborts the whole directory**: `mxcli test tests/` (and `--list`) reports the bad file and runs none of the others", "cause": "Two independent defects behind one report. (1) The line-number estimate searched the **raw file bytes** for the first 20 characters of a block that had already been rebuilt by joining scanner lines with `\"\\n\"` — under CRLF the needle can never match, `strings.Index` returns -1, and `content[:-1]` panics. Fixed as a side effect of #927's block/line pairing (`testBlock` carries its own `Line`), which left it **untested**. (2) `ParseTestDir`/`parseTestFiles` returned on the first file that failed to parse, so an unrelated malformed file cost every other test in the directory", "file": "`cmd/mxcli/testrunner/parser.go` (`TestSuite.FileErrors`, `FileError`, `ParseTestDir`), `cmd/mxcli/testrunner/runner.go` (`parseTestFiles`, `Run`, `dispatchRun`, `reportFileErrors`, `suiteFileErrorResults`), `cmd/mxcli/testrunner/watch.go`; tests `cmd/mxcli/testrunner/parser_crlf_test.go`, `cmd/mxcli/testrunner/parser_file_isolation_test.go`", "insight": "**Never search reconstructed text inside the original bytes** — normalising line endings on the way in and then indexing the raw content is the whole bug, and it is silent on LF machines. **A CRLF test must write the bytes at runtime** (`os.WriteFile`), not live in a checked-in fixture: git's line-ending handling would normalise it and the test would pass vacuously. **Assert on line NUMBERS, not just \"does not panic\"** — the defect lived in a line estimate, so a smoke test would not have detected a wrong answer; the LF file is the control. For (2), the fail-closed rule this package is built around is **ERROR, not abort**: an uncompilable `@expect` becomes a `StatusError` result counted with the failures, and an unparseable file now gets the same treatment — red exit code, file named, other tests still run", "refs": ["#927"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-mxcli-theme-create-lists-theme-web-custom-variables-scss.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-mxcli-theme-create-lists-theme-web-custom-variables-scss.json new file mode 100644 index 0000000000..cc80198320 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-mxcli-theme-create-lists-theme-web-custom-variables-scss.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "`mxcli theme create` lists `theme/web/custom-variables.scss` and `theme/web/main.scss` as `created`, on a project that has hand-written versions of both. Nothing was touched", "cause": "The reported paths were relative to the scaffold, not to the project, so they collided exactly with the app paths — printed by the one command whose whole purpose is to write into `theme/`", "file": "`cmd/mxcli/theme/create.go` (`CreateResult.Files` carries scaffold-root-relative paths), `cmd/mxcli/cmd_theme.go` (prints them under the scaffold root)", "insight": "**A path in output is a claim about a location.** Two files in different trees sharing a relative path means the output has to carry the root or it asserts the wrong one. `theme.json` sits at the scaffold root and everything else under `files/`, so the result carries that distinction rather than the printer re-deriving it. ako/mxcli-ledger #139", "refs": ["#139"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-theme-s-scss-fails-compile-error-expected-expression.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-theme-s-scss-fails-compile-error-expected-expression.json new file mode 100644 index 0000000000..1a26e2f164 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-25-theme-s-scss-fails-compile-error-expected-expression.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-25", "symptom": "A theme's SCSS fails to compile with `Error: Expected expression` pointing at a `$…-scope:` line, and every Go test is green — `mx check` says nothing either, because the failure is in mxbuild's Sass pass, before any model validation", "cause": "A selector was assigned to a Sass variable bare (`$s: :root` or `$s: a, b`). Neither is a Sass expression; only a **quoted string** is, and `#{}` drops the quotes at the point of use", "file": "`cmd/mxcli/theme/theme.go` (`expand`, the `{{SCOPE}}` substitution quotes; `{{PALETTE_SCOPE}}` does not, because that file is plain CSS)", "insight": "**An asset-driven feature whose correctness depends on a compiler the tests never run needs one real compile in the loop.** This shape passed every unit test and broke the build on the first `mxbuild --target=deploy` — the same class as recurring-finding #20. Two placeholders that look interchangeable are not: one lands in plain CSS, one in a Sass expression", "refs": ["#20"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-26-test-written-bare-throws-fails-very-exception-written-expect.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-26-test-written-bare-throws-fails-very-exception-written-expect.json new file mode 100644 index 0000000000..f9200bf7cc --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-26-test-written-bare-throws-fails-very-exception-written-expect.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-26", "symptom": "A test written with a bare `@throws` fails on the very exception it was written to expect, reporting \"exception during execution\" and \"no assertions\"", "cause": "`mxcli test` treats a test as a throws-test only when the annotation carries a NON-EMPTY message (`generator_endpoint.go`, `if tc.Throws != \"\"`), so a bare `@throws` is silently ignored and the exception propagates as a failure. The message itself is **never compared** — `@throws 'ZZZ-NOT-THE-REAL-MESSAGE'` passes against any error — so it documents intent rather than asserting it", "file": "`cmd/mxcli/testrunner/generator_endpoint.go` (`writeThrowsFlowBody`, `rewriteBodyForThrows`)", "insight": "Write `@throws ''` and give the test its meaning with a CONTROL — the same call with input that must NOT throw — because the annotation alone only proves something was raised, not what. Two separate defects to fix: a bare `@throws` should be a parse error rather than silently dropped, and the message should either be matched or documented as unmatched (the annotations reference shows `@throws 'validation failed'`, which reads as a match). Found while asserting #261's `or error` at run time", "refs": ["#261"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-ci-s-build-test-fails-every-open-pr-compile-error-test-go.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-ci-s-build-test-fails-every-open-pr-compile-error-test-go.json new file mode 100644 index 0000000000..7c942b5571 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-ci-s-build-test-fails-every-open-pr-compile-error-test-go.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-27", "symptom": "CI's `build-and-test` fails on every open PR with a **compile** error in a `_test.go` file — e.g. `undefined: resolveJDK21` — while `make build`, `make test` and `make lint` are all green locally, and the failing job burns ~30 minutes before reporting it", "cause": "The file is behind `//go:build integration`. A bare `go vet ./...` and `go test ./...` do not compile it, so a rename can leave it uncompilable and the entire fast gate stays green. The break lands on **main** and every open PR inherits it, which makes it look like N separate PR failures", "file": "The stale call site (here `cmd/mxcli/docker/build_integration_test.go`), and the gate that missed it: `Makefile` (`vet`)", "insight": "**Fix it on a branch off `main`, not in the PRs** — they all inherit it, and fixing it in one leaves the others red. Then close the gap: `make vet` now vets once per build-tag set (`\"\"` and `integration`), which type-checks the tagged files in seconds without needing mx, a JDK or Docker. Grep for stale references with `go vet -tags integration ./...` before renaming anything an integration test touches. Where the renamed symbol became version-aware, do NOT just pass the old constant back in — `resolveJDK(21)` would reinstate exactly the assumption the rename removed; resolve from the project (`ProjectJavaMajor`), which for a skip guard means moving it after the fixture is scaffolded."} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mendix-11-14-project-will-not-build-error-release-version-25.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mendix-11-14-project-will-not-build-error-release-version-25.json new file mode 100644 index 0000000000..415b2e7ebc --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mendix-11-14-project-will-not-build-error-release-version-25.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-27", "raw": "| A Mendix 11.14 project will not build (`error: release version 25 not supported`) or, if built elsewhere, will not boot (`UnsupportedClassVersionError … class file version 69.0`) | mxcli resolved a JDK **21 unconditionally** and refused any other `JAVA_HOME`. Studio Pro 11.14 supports Java 25 and its blank app asks for it, so the Java release is now a per-project variable (Settings > Model > JavaVersion) rather than a constant | `cmd/mxcli/docker/javaversion.go` (`ProjectJavaMajor`, `ParseJavaMajor`), `cmd/mxcli/docker/detect.go` (`resolveJDK`, `isJDK`, `jdkSearchPathsFor`); the four call sites are `build.go`, `settle.go`, `mxserve.go`, `localboot.go` | **Resolve the JDK from the project, not a constant** — and use the SAME major for the build and the runtime, because class files from a newer release cannot be loaded by an older JVM. Two traps. (1) The version glob is matched against `java -version` text, so the major must be followed by a dot, whitespace or the closing quote — a bare `21` also matches `210`; there is a test. (2) Zero must mean the default, or every options struct that forgets the field silently changes behaviour. The control worth running: boot a Java **21** project with `JAVA_HOME` pointing at a JDK **25** — it must ignore it and find the 21, and `ps` per-process is the only reliable way to check, since a stale JVM from an earlier run will fool `ps | head -1`. ako/mxcli-maintenance #1 |", "refs": ["#1"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mxcli-run-local-mendix-11-14-app-dies-bundling-web-client-no.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mxcli-run-local-mendix-11-14-app-dies-bundling-web-client-no.json new file mode 100644 index 0000000000..37bb92dcdf --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mxcli-run-local-mendix-11-14-app-dies-bundling-web-client-no.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-27", "symptom": "`mxcli run --local` on a Mendix 11.14 app dies at `bundling web client: no rollup.config.mjs in …/deployment/web (run a serve Deploy build first)` — and a serve Deploy build HAS just run", "cause": "11.14 bundles the browser client itself. `BuildWebClient` exists to close a gap that 11.13 and earlier left (client source + a rollup config, but no bundler run), and its gate tested for the CONFIG, so it failed on the absence of a file whose purpose had been served. Measured on a blank app, same build target: 11.13 `rollup.config.mjs` PRESENT / `dist/index.js` ABSENT; 11.14 exactly inverted", "file": "`cmd/mxcli/docker/webclient.go` (`BuildWebClient`), called fatally at `runlocal.go:664` and `:1091`", "insight": "**Gate a compatibility step on the GAP it closes, not on the shape one version happened to leave behind.** Skip when `WebClientBundled(deployDir)` is already true; keep requiring the config when it is not, because then rollup is the only thing that can produce a bundle. Do NOT make it an unconditional no-op when `dist/` exists — `runlocal.go:1091` re-bundles when the file is present but not SERVED, and a blanket skip would break that recovery. The doc comment called it \"no-op-safe\" and it was not; it is now. Affects EVERY 11.14 app, so a version-gated workaround is wrong — the condition is about the bundle, not the version. ako/mxcli-ledger #146", "refs": ["#146"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mxcli-test-case-written-bare-throws-reports-fail-exception.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mxcli-test-case-written-bare-throws-reports-fail-exception.json new file mode 100644 index 0000000000..1e5c32eba0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mxcli-test-case-written-bare-throws-reports-fail-exception.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-27", "raw": "| A `mxcli test` case written with a bare `@throws` reports **FAIL … exception during execution** — it fails on the very error it was written to expect — and separately, `@throws 'ZZZ-NOT-THE-REAL-MESSAGE'` reports **PASS** | Both generators keyed a throws test on `tc.Throws != \"\"`, and a bare annotation leaves `Throws` empty — the same value an ABSENT annotation leaves, so the two states were indistinguishable and the bare form was dropped in silence. The message half was decorative: the generated `ON ERROR` handler cleared the pre-set failing verdict for **any** exception, so a stale or simply wrong expectation kept passing against a completely different error. The regexp required the quotes, so an unquoted message matched nothing and removed the `@throws` entirely | `cmd/mxcli/testrunner/parser.go` (`parseThrows`, `TestCase.ExpectsThrow`, `expectsThrow()`), `generator_endpoint.go` (`throwsHandler`), `generator.go` (`throwsFlagHandler`) | **`$latestError/Message` is available inside an `ON ERROR` handler and compiles** — measured at 0 errors on mxbuild 11.6.0 and 11.13.0, with `$noSuchThing/Message` as the control (**CE0109** \"Undefined variable\"). Match with `contains`, not equality: a real Mendix message carries an activity name or an object id the test cannot predict (`Could not find object of type 'RtMap.Person'`), so equality would make most expectations unwritable. Two fields can say \"this is a throws test\" and they must not disagree — read them through **one accessor** (`ExpectsThrow || Throws != \"\"`), or every existing construction site that sets only the message silently becomes a non-throws test. Prove it at RUNTIME, not on the generated MDL: the control (`if want == \"\"` → `if true`, rebuilt) reproduces both reported symptoms verbatim against `mdl-examples/bug-tests/mapping-runtime-proof.test.mdl` under `run --local --test-endpoint` + `test --attach`. That suite's own `@throws 'the object was not found'` was **wrong and passing** — a fixed matcher turns a wrong expectation red, so expect to correct existing tests. ako/mxcli#301 |", "refs": ["ako/mxcli#301"], "ce": ["CE0109"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mxcli-test-dir-prints-found-0-test-s-1-file-s-then-no-tests.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mxcli-test-dir-prints-found-0-test-s-1-file-s-then-no-tests.json new file mode 100644 index 0000000000..15d042c5b6 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-mxcli-test-dir-prints-found-0-test-s-1-file-s-then-no-tests.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-27", "symptom": "`mxcli test ` prints `Found 0 test(s) in 1 file(s)` then `no tests found in the provided files`, for a directory that visibly contains a `.mdl` test", "cause": "Discovery matches on the file NAME (`*.test.mdl` / `*.test.md`), and nothing said so. The `1 file(s)` was the DIRECTORY counted as a path, so it read as though a file had been opened and found wanting", "file": "`cmd/mxcli/testrunner/runner.go` (`emptySuiteError`, `parseTestFiles`), `cmd/mxcli/testrunner/parser.go` (`TestSuite.FilesRead`, `ParseTestDir`)", "insight": "**Count files read, not paths given** — one directory is one path and zero-to-many files. When the suite is empty, name the `.mdl`/`.md` files that were skipped for their name: a `.mdl` in a test directory is almost always the intended test misnamed, so listing it turns a dead end into a rename. Do NOT list unrelated extensions (`.txt`), and on a genuinely empty directory do NOT claim files were skipped — describe the format instead. Both branches have tests, and `FilesRead` has a positive control so it cannot pass by never being incremented. ako/mxcli-maintenance §5"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-project-made-mxcli-new-version-11-14-fails-very-first-build.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-project-made-mxcli-new-version-11-14-fails-very-first-build.json new file mode 100644 index 0000000000..cdfc750280 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-27-project-made-mxcli-new-version-11-14-fails-very-first-build.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-27", "symptom": "A project made by `mxcli new --version 11.14` fails its very first build with `error: release version 25 not supported`, and installing a JDK 25 moves the failure to boot (`UnsupportedClassVersionError … class file version 69.0`)", "cause": "Mendix 11.14's `mx create-project` writes `JavaVersion = 25`, but mxcli builds and launches the runtime on **JDK 21** and validates it (`resolveJDK21` refuses any other `JAVA_HOME`). Compiling for 25 and running on 21 cannot work in either direction — and 11.14's own runtime bundles are still Java 21 class files (major version 65)", "file": "`cmd/mxcli/cmd_new_java.go` (`alignJavaVersion`, `runtimeMaxJavaVersion`), called from `cmd_new.go` **before** the first build; the launcher side is `cmd/mxcli/docker/detect.go` (`resolveJDK21`)", "insight": "**Lower the project, do not raise the JDK.** Set `JavaVersion` to what the runtime launcher actually uses, and only when the project asks for MORE — so it is a no-op on 11.13 and disappears by itself when the launcher becomes version-aware. Order matters: align BEFORE `SettleGeneratedSources`, or that build is the thing that fails. Mendix then warns \"Java versions below 25 are deprecated\", which is a deprecation on a project that builds rather than an error on one that does not. An unparseable version is left alone rather than overwritten. `runtimeMaxJavaVersion` and `resolveJDK21` must move together — there is a test asserting it. ako/mxcli-maintenance #1, #3", "refs": ["#1", "#3"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-28-documented-mxcli-mxbuild-modeler-mx-check-app-mpr-fails-verb.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-28-documented-mxcli-mxbuild-modeler-mx-check-app-mpr-fails-verb.json new file mode 100644 index 0000000000..5c539b9f49 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-28-documented-mxcli-mxbuild-modeler-mx-check-app-mpr-fails-verb.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-28", "symptom": "A documented `~/.mxcli/mxbuild/*/modeler/mx check app.mpr` fails with `Verb '/root/.mxcli/mxbuild//modeler/mx' is not recognized`, and it looks like a broken mxbuild install", "cause": "The shell expands the `*` to every cached version and `mx` reads the second path as an argument. Both binaries are fine; it is purely the command line, and the failure mode is misleading enough that an earlier report recorded the wrong binary as broken", "file": "`cmd/mxcli/init_claudemd.go` (the generated project docs), plus ~15 `.md` files including three SYNCED skills (`manage-security`, `custom-widgets`, `alter-page`)", "insight": "**Document `mxcli docker check -p .mpr` first** — it resolves the version from the project, which is the whole problem. Where `mx` must be called directly, name a version (``), never glob. Note the split: `filepath.Glob` in Go (`cmd/mxcli/docker/detect.go`) is CORRECT and must not be \"fixed\" — it globs then picks, whereas a shell passes all matches through. This reaches every user, since anyone who has used two Mendix versions has two cached. ako/mxcli-maintenance-2"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-28-test-fails-only-ci-never-locally-e-g-testwatcherdebounce.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-28-test-fails-only-ci-never-locally-e-g-testwatcherdebounce.json new file mode 100644 index 0000000000..cdb26e804f --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-28-test-fails-only-ci-never-locally-e-g-testwatcherdebounce.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-28", "raw": "| A test fails only on CI and never locally — e.g. `TestWatcherDebounce`: `expected 1 debounced message, got 2` | The test ASSUMED a timing premise instead of checking it. A debounce test needs the whole burst of writes to land inside the debounce window; each write resets the timer, so a gap larger than the window splits the burst and it debounces as two. On a loaded runner one gap between five `os.WriteFile` calls exceeded 500ms | `cmd/mxcli/tui/watcher.go` (`newWatcherDebounced`, the window is a field not a constant), `cmd/mxcli/tui/watcher_test.go` | **Get the observed value out of the CI log before theorising** — `gh run view --log \\| grep `. I guessed \"count 0, event delivery too slow\" and built two controls for it; both passed, because the log said `got 2` and the real mechanism was the opposite one. Three fixes, each aimed at the measured failure: a window far longer than production (2s vs 500ms) so headroom is orders of magnitude rather than 2×; the premise **measured and skipped on** (`if burst >= debounce { t.Skip }`) so a machine too slow to emit a burst reports its own limitation instead of a product defect; and the message **polled for, not slept for**, so delivery latency cannot decide the outcome. The grace after the first message must be a FULL window — a split's second timer fires one window after the last write, so a shorter wait stops looking before the evidence arrives. Control the mechanism deterministically (100ms window, 150ms gaps → count 5) rather than trying to reproduce load, and verify the skip path actually fires (temporarily set the window to 1ns). This is the checklist item \"tests don't rely on `time.Sleep` for synchronization\" — the original even named the hazard in a comment without removing it. |"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-28-under-run-local-watch-after-roughly-ten-hot-applies-page.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-28-under-run-local-watch-after-roughly-ten-hot-applies-page.json new file mode 100644 index 0000000000..ffca5cb070 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-28-under-run-local-watch-after-roughly-ten-hot-applies-page.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-28", "symptom": "Under `run --local --watch`, after roughly ten hot-applies a page dies with the runtime's generic error dialog and `runtime.log` fills with `404 - file not found for file: dist%2Fchunks%2F.js`. The model is clean, `mx check` is clean, and a plain restart fixes it with no model change", "cause": "The browser bundle is internally inconsistent: `index.js` imports a content-hashed chunk that is not on disk. `ensureClientServed` probes **only `/dist/index.js`**, which is served with a 200 in this state — so the apply is reported as successful and the fault reads as an application bug", "file": "`cmd/mxcli/docker/webclient_chunks.go` (`danglingClientChunks`), called first from `ensureClientServed` in `runlocal.go`", "insight": "**Walk the import graph from `index.js`, and follow only chunks that EXIST.** An orphan left by an earlier bundle references old hashes, so scanning every file in `chunks/` reports a healthy bundle as broken. One level is not enough either — a real rollup bundle has `index.js` importing a single chunk with the rest hanging off it, so a one-level check misses almost every real breakage. The safety control matters more than the detection here, because this gates an automatic re-bundle: run it against several REAL built bundles and require zero findings, then delete one referenced chunk from a copy and require detection. ako/mxcli-maintenance-2"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-29-codeql-go-incorrect-integer-conversion-high-strconv.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-29-codeql-go-incorrect-integer-conversion-high-strconv.json new file mode 100644 index 0000000000..d5095af0d2 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-29-codeql-go-incorrect-integer-conversion-high-strconv.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-29", "symptom": "CodeQL `go/incorrect-integer-conversion` (high) on `strconv.ParseUint(…, 16, 32)` narrowed to `int`", "cause": "The parse width was chosen from the *field's* width in the file, not the *type's*. A TCP port is 16 bits and the kernel writes it `%04X`, so a 32-bit parse accepts values the caller then compares as if they were ports; where `int` is 32 bits the conversion wraps (`FFFFFFFF` → `-1`, `ok=true`)", "file": "`cmd/mxcli/docker/portowner_linux.go` (`hexPort`)", "insight": "**Parse at the target type's real width and let the parser do the range check** — that is the whole fix, and it turns an unchecked narrowing into a rejection at the boundary. Establish reachability before rating it: the input here is `/proc/net/tcp`, written by the kernel, so no hostile value reaches it and the consequence of the wrap is a *missed* port match (the diagnostic falls back to its generic message), not memory unsafety. Two things make it worth fixing anyway — a range check that lives in the parser cannot be forgotten by the next caller, and the alert otherwise stays open forever. Measure the wrap rather than asserting it: a `GOARCH=386` binary **runs on an amd64 host**, so the 32-bit behaviour is directly observable, and the same run showed the repo does not currently build for 386 at all (`mdl/settingsoverlay` has `9007199254740992` as an untyped int constant) — worth knowing before claiming a 32-bit impact. A test written at 64 bits still catches it: at width 32 `hexPort` returns `4294967295, true` where it should refuse. Keep a 32-bit-safe test literal (`int(^uint32(0))`, not `4294967295`, which is not a valid `int` constant on 386)."} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-29-codeql-go-unsafe-unzip-symlink-tar-extractor-already-checks.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-29-codeql-go-unsafe-unzip-symlink-tar-extractor-already-checks.json new file mode 100644 index 0000000000..5e09388356 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-29-codeql-go-unsafe-unzip-symlink-tar-extractor-already-checks.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-29", "raw": "| CodeQL `go/unsafe-unzip-symlink` on a tar extractor that already checks the joined path and the symlink target — looks like a false positive | It is not. A **chain of two symlinks** escapes: the guard resolves each link against its **lexical** parent while the OS resolves it against its **real** one. `sub/up -> ..` genuinely is the root (allowed, correctly); `sub/up/w -> ..` therefore really is `root/w`, pointing at the root's **parent**, and lexically looks like `root/sub`. Then `sub/up/w/pwned` contains no `..` and passes every check | `cmd/mxcli/docker/download.go` — one `extractTarGzInto` on `*os.Root` behind both `extractTarGz` and `extractTarGzStrip1`, plus `tarEntryPath` / `symlinkStaysInside` / `pathWithin` | **Try to break it before rating it, and keep trying after the first six attempts fail.** Six single-step archives (absolute link, `../..` link, absolute name, link-to-root, …) were all correctly refused, which is exactly what makes \"false positive\" tempting; the seventh — two chained up-links — wrote a file into the extraction root's parent, and an eighth reached any sibling directory. The fix is `os.OpenRoot` + `Root.OpenFile`/`MkdirAll`/`Symlink` (Go 1.24/1.25), which hands containment to the kernel (`openat2` `RESOLVE_BENEATH`) instead of to string comparison — the failure is then `path escapes from parent` from the OS. Keep a **link-target** check as well and resolve its parent with `EvalSymlinks`: `os.Root` stops *mxcli* following an outward link, not mxbuild or the JVM that read the tree afterwards. Two things the rewrite fixed for free: `strings.Contains(name, \"..\")` silently dropped legitimate files (`foo..bar` never appeared, no error) — `filepath.IsLocal` is the right test, on path *elements* — and a `HasPrefix` containment test with no separator puts `/cache/runtime-evil` inside `/cache/runtime`. Settle \"would refusing break real downloads?\" by measurement, not caution: the 11.13.0 CDN tarballs are 3,646+279 (runtime) and 33,188+5,036 (mxbuild) entries with **zero symlinks**, listed with `curl \\| tar -tzv` without saving the archive. Control both directions — the ordinary-archive test must pass, and against the old extractor the chained cases must actually write outside (they do, in both the stripping and non-stripping variants). |"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-30-mxcli-run-local-hub-sits-several-hundred-percent-cpu-hours.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-30-mxcli-run-local-hub-sits-several-hundred-percent-cpu-hours.json new file mode 100644 index 0000000000..afe0ed2ec2 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-30-mxcli-run-local-hub-sits-several-hundred-percent-cpu-hours.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-30", "raw": "| `mxcli run --local`/`--hub` sits at several hundred percent CPU for hours while the app is gone; `ps -o stat` shows the runtime JVM as `Z` (zombie) under it, `curl localhost:` returns `000`, and the hub preview URL still answers | `cmd/mxcli/docker/localboot.go` (`watchExit`, `alive`, `stopProcess`), `cmd/mxcli/docker/runlocal.go` (`waitForInterruptOrExit`, `runtimeStoppedError`) | Two mechanisms. (1) **Nothing ever called `Wait()`** on the runtime process, so an exited JVM stayed an unreaped zombie — and `alive()` asked `Signal(0)`, which **succeeds on a zombie**. Measured (Linux 6.18, go1.26): proc state `Z`, `Signal(0)` → nil; after `Wait()` → \"process already finished\". So the liveness check reported a runtime that had terminated itself hours earlier as alive. `Signal(0)` is only a correct liveness test *because* something reaps — removing `watchExit` silently breaks that line, which is why the control is on the reaper, not on `alive()`. (2) **After boot, `run` waited on a signal and nothing else**, so a correct answer had no one asking; it now waits on the signal OR `rt.Exited()` and returns a **non-zero** error, because returning 0 after the app has gone is what let a supervisor conclude all was well. **Why it happens at all**: the local standalone runtime uses a development licence with a maximum run time and terminates *itself* (measured lifetimes 3h52m and 5h07m — not a fixed number, and shorter than a working session); `runtimeExitReason` lifts that from the runtime's own log, and reports nothing rather than guessing when it cannot tell. **Two waiters on one process deadlock**, so `stopProcess` consults the reaper's channel instead of taking its own `Wait`. The CPU spin itself was NOT reproduced and is not claimed fixed by name — it lived in the tunnel client, under a supervisor blind to its dead child; what is fixed is the state it occurred in, since mxcli now exits and takes the tunnel with it. **Generalisable**: a 200 from a tunnelled URL is not evidence the app is alive — the tunnel outlives the runtime. Reported as mxcli-formula1 FINDINGS §60 |"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-30-widget-keyword-grammar-accepts-absent-from-mxcli-syntax-page.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-30-widget-keyword-grammar-accepts-absent-from-mxcli-syntax-page.json new file mode 100644 index 0000000000..fbec8666c8 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-30-widget-keyword-grammar-accepts-absent-from-mxcli-syntax-page.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-30", "raw": "| A widget keyword the grammar accepts is absent from `mxcli syntax page widgets`, so it is concluded not to exist and worked around at length (reported for `tabcontainer`, which cost two days and five hand-rolled pages) | `cmd/mxcli/syntax/features_page.go`, `cmd/mxcli/syntax/widget_keywords_drift_test.go` | The lesson the reporter drew — \"absence from the documentation is not absence from the grammar\" — is true and is a bad thing for the docs to require. `TestEveryWidgetKeywordIsInAPageSyntaxTopic` makes it false instead: it reads the `widgetTypeV3` rule out of the **committed** `.g4` (only the *generated parser* is uncommitted, and the grammar is the authority the reporter was told to consult) and fails when a keyword appears in no `page.*` topic. It found **18**, not one. Exemptions go in `documentedElsewhere` **with the topic that owns them** — layout constructs (`scrollcontainer`, `region`, `navigationtree`, `menubar`, `placeholder`) and pluggable-widget object-list keywords (`group`, `series`, `marker`, …) are not page widgets; an entry with no home is the same defect. The guard carries its own vacuity control: a keyword that does not exist must not match, and one that does must. **Do not document a keyword without running it** — probing all 18 on 11.13 found four the parser accepts and the *default engine refuses* (`statictext`, `staticimage`, `dynamicimage`, `dropdown` → \"widget *pages.X not yet supported by the modelsdk engine\"), two refused on both engines (`referenceselector`, `legacydatagrid`), and one whose bare form emits **CE0463** (`image`). Reported as mxcli-formula1 FINDINGS §69 |", "ce": ["CE0463"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-31-every-open-pr-goes-red-once-build-test-failure-package-none.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-31-every-open-pr-goes-red-once-build-test-failure-package-none.json new file mode 100644 index 0000000000..cc9f6eff82 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-31-every-open-pr-goes-red-once-build-test-failure-package-none.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-31", "symptom": "Every open PR goes red at once on `build-and-test` with a failure in a package none of them touched — `--- FAIL: TestSessionLog_PersistAndPrune`, \"after reload+prune: 0 records, want 1\" — and the same test fails on a clean checkout of `main`", "cause": "A **time bomb in the test**, not a regression: the fixture pinned `base := time.Date(2026, 8, 1, ...)` against a 30-day retention window, and `NewSessionLogFile` prunes inside `load()` — *before* the test can assign `log2.now`, so the reload prune runs on the real `time.Now()` whatever clock is injected afterwards. It passed for 30 days and then failed permanently, on every branch simultaneously", "file": "`cmd/mxcli/tunnelhub/sessions_test.go` (`TestSessionLog_PersistAndPrune`), `cmd/mxcli/tunnelhub/sessions.go` (`load` → `pruneLocked` → `clock`)", "insight": "**First establish it is not yours**: run the failing test on a clean `origin/main`. Several PRs failing on one unrelated test is the signature. Then make the fixture relative — `base := time.Now().UTC()` — so the record ages, not the calendar, decide the outcome; the other tests in the file keep their fixed base legitimately, because they use `NewSessionLog` and inject the clock before recording. **A date fixture is only safe where no code path reads the real clock**; the moment a constructor prunes, expires or compares against `time.Now()` before the seam is in place, an absolute date has a fuse on it. Control the repair: stub `pruneLocked` to a no-op and confirm the test still fails (2 records, want 1), or the fix is just a test that stopped testing"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-31-mxcli-test-local-cannot-start-runtime-any-project-error.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-31-mxcli-test-local-cannot-start-runtime-any-project-error.json new file mode 100644 index 0000000000..265c9392f8 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-08-31-mxcli-test-local-cannot-start-runtime-any-project-error.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-08-31", "symptom": "`mxcli test --local` cannot start a runtime for ANY project: `Error: local runtime: runtime admin API did not come up: runtime process exited during startup`, then `java.lang.IllegalArgumentException: Path '/.mxcli/deployment-test/model/bundles' cannot be resolved in base path '/.mxcli/deployment-test'`. The tree has `data/` and no `model/`", "cause": "**mxbuild writes the deployment to `/deployment` and has no option to move it** \u2014 measured, not inferred: `--target=deploy` on a project whose `deployment/` had just been deleted recreated it there, `mxbuild --help` lists no deployment-path flag, and `BuildRequest` carries none. So giving the test boot a `DeployDir` of its own moved where the RUNTIME reads and not where the BUILD writes, and it booted against an empty directory. `StartLocalApp` now refuses a `DeployDir` the build will not populate, naming mxbuild as the constraint. The \u00a762 blanking the scratch tree was meant to prevent is fixed the only way the constraint allows: `preserveWebClientBundle` copies `deployment/web/dist` aside before the boot and puts it back after (a few MB of copy, against the ~30s re-bundle that made warning the earlier choice), restoring only when the bundle is actually gone so a fresher one is never clobbered.", "file": "`cmd/mxcli/docker/localapp.go` (`checkDeployDirIsBuildable`, the `preserveWebClientBundle` call), `cmd/mxcli/docker/webclient_preserve.go` (new), `cmd/mxcli/docker/localapp_integration_test.go` (new), `cmd/mxcli/testrunner/localapp_options.go`, `cmd/mxcli/testrunner/runner_local.go`, `cmd/mxcli/testrunner/runner.go`", "insight": "**The lesson is in the tests that did not catch it**, and two reporting projects drew it independently (mxcli-ledger \u00a7150, mxcli-sudoku \u00a751): four unit tests asserted `DeployDir` was set, was under `.mxcli/`, was per-project and was not the dev loop's \u2014 all four passed against a build that could not start, because every one was about the OPTION while the symptom lived in what is on disk after mxbuild runs. A fix that redirects a consumer without redirecting the producer moves the failure rather than removing it. The guard is now an integration test asserting the artefact: after a real build, the directory the runtime boots against holds `model/bundles`. Note the reporters' prescribed fix \u2014 thread `DeployDir` through the build \u2014 is not available; mxbuild has no such parameter, which is why the shared tree is accepted and the bundle carried across instead. Controls, both end-to-end on a real 11.13 project: the pre-fix binary reproduces the reported JVM error byte for byte while the fixed one passes; and with `preserveWebClientBundle` stubbed the sentinel bundle is destroyed by a test run that still reports all tests passed. Reinstating the regression fails the new integration test naming both missing paths."} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-01-after-mxcli-test-local-live-mxcli-run-local-serving-same.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-01-after-mxcli-test-local-live-mxcli-run-local-serving-same.json new file mode 100644 index 0000000000..6808e9d71c --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-01-after-mxcli-test-local-live-mxcli-run-local-serving-same.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-01", "symptom": "After `mxcli test --local`, a live `mxcli run --local` serving the SAME project starts answering **HTTP 200 with a zero-byte body** on every microflow-backed resource \u2014 not a 500, not an error page \u2014 while source-backed ones keep working, so half the app looks fine. The runtime log shows `java.lang.NoClassDefFoundError` on a project class. In a two-app solution it surfaces as tests failing in the OTHER app.", "cause": "The test run recompiles the project's Java into `deployment/run/bin`, which is the classpath the running JVM is holding open. Measured on a real 11.13 project: after one test run all 134 class files have **new inodes and byte-identical content** \u2014 every one deleted and rewritten. A JVM loads classes lazily, so one it has not reached yet can fail permanently. mxcli cannot prevent this: mxbuild's Gradle pass owns the compile and the deployment directory cannot be moved (ledger \u00a7150). So it warns instead, which is what was missing.", "file": "`cmd/mxcli/devloop_recompile_warning.go` (new \u2014 `warnIfDevLoopServing`, `recompileWarning`), `cmd/mxcli/cmd_test_run.go`; reads the existing `cmd/mxcli/devloop_handshake.go`", "insight": "**The mechanism already existed and a duplicate would have broken it.** `mxcli run --local` publishes `devLoopHandshake` at `.mxcli/run-local.json` for `mxcli constant set --apply` \u2014 same path, same pid-liveness staleness check, plus the `adminPass` and `bootConfig` that `--apply` and `--attach` depend on. A second state file was written at that path before this was noticed; it parsed fine (JSON ignores unknown fields) but its WRITER would have silently dropped those two keys. Grep the path before inventing a file. **The liveness check is the feature**: a `run --local` killed or ended by its development licence (\u00a760, measured lifetimes under six hours) leaves the file behind, and a warning driven by the file alone fires forever \u2014 one that is always wrong teaches the reader to skip it. It **warns rather than refuses**, since the warm loop exists so an app can stay up while you work and the reporting project runs two apps that way; neither `--attach` nor `--skip-build` builds, so neither warns. The finding's cost was diagnosis, not breakage \u2014 108 log lines and a wrong hypothesis about a different app, for something whose remedy is one restart \u2014 so the warning names the symptom (HTTP 200, empty body), the part nobody guesses. Controls, end-to-end against a real `run --local`: the warning carries that app's actual pid and port and its handshake still has adminPass and 9 bootConfig keys afterwards; with the loop stopped, and with a stale dead-pid handshake, the same command is silent. Reported as mxcli-formula1 FINDINGS \u00a781."} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-03-mxcli-brain-check-exits-1-requirement-simply-not-built-yet.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-03-mxcli-brain-check-exits-1-requirement-simply-not-built-yet.json new file mode 100644 index 0000000000..a2c9f314f5 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-03-mxcli-brain-check-exits-1-requirement-simply-not-built-yet.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-03", "symptom": "`mxcli brain check` exits 1 on a requirement that is simply not built yet — the entry is correct and current, and the check reports its anchor as NOT FOUND", "cause": "Requirements were recorded as ordinary brain entries, but an entry's anchor was assumed to point BACKWARD at something that exists. A decision's unresolved anchor means the decision is stale; a requirement's unresolved anchor means the work is not done. Same syntax, opposite meaning, and the store had no way to tell them apart", "file": "`cmd/mxcli/brain/entry.go` (`Kind`), `cmd/mxcli/brain/check.go` (`checkSlice`)", "insight": "Before adding a record type to an existing store, ask what a FAILED validation means for it — not just what it looks like. Requirements and decisions share the anchor syntax exactly, which is what made them look like the same thing; they differ only in the direction the anchor points, and that difference is the whole lifecycle. Measured before designing: one unbuilt requirement filed as a decision took `brain check` to exit 1, which settled it in one command. The inversion then pays for itself — a requirement is 'built' when its anchors resolve, so `brain plan` reports progress derived from the model (measured 0/1 -> 1/0 after creating the microflow, with the plan file untouched) instead of a status column that goes stale silently", "refs": ["ako/mxcli#385"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-03-mxcli-brain-check-reports-entry-misfiled-even-though-entry.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-03-mxcli-brain-check-reports-entry-misfiled-even-though-entry.json new file mode 100644 index 0000000000..b97ad9a2bf --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-03-mxcli-brain-check-reports-entry-misfiled-even-though-entry.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-03", "symptom": "`mxcli brain check` reports an entry as MISFILED even though the entry is correct and its anchor points at a real document — the anchor's target is simply of a document type the catalog's `objects` view does not index", "cause": "Misfiling was decided by comparing the shard against the modules of *resolved* anchors. An entry whose only anchor came back NotIndexable had an empty resolved-module list, so the comparison found no match and reported it misfiled — reintroducing, through the misfiling axis, exactly the false staleness that the NotIndexable state exists on the anchor axis to prevent", "file": "`cmd/mxcli/brain/entry.go` (`MisfiledIn`)", "insight": "When a check has two axes, an 'unknown' outcome on one of them must not be read as a negative on the other. The fix is to make misfiling *undecidable* rather than false when nothing resolved: with no resolved anchor there is no evidence about where the entry belongs, and an anchor that truly names nothing is already a failure on its own axis. Caught in development by a table test whose control stubbed the guard to `if false` — a control that deletes the block instead fails to compile on unused variables, which is not a control", "refs": ["ako/mxcli#385", "PROPOSAL_project_brain.md A1"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-03-mxcli-test-local-expect-syntactically-valid-but-only-fails.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-03-mxcli-test-local-expect-syntactically-valid-but-only-fails.json new file mode 100644 index 0000000000..66f4656a65 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-03-mxcli-test-local-expect-syntactically-valid-but-only-fails.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "cause": "BuildResult parsed only status/restartRequired/message and left everything else in Raw, so a failed build was reported by dumping the whole response body; nothing looked at the per-problem severity/errorCode/locations mxbuild actually returns. The consistency error that stopped the build therefore arrived unmarked among the warnings, and no test was named.", "ce": ["CE0109", "CE0117"], "date": "2026-09-03", "file": "cmd/mxcli/docker/mxserve.go (BuildProblem/BuildLocation/Errors/ErrorSummary/BuildFailedError), cmd/mxcli/testrunner/build_attribution.go (new), wired at cmd/mxcli/testrunner/runner_endpoint.go", "insight": "**The serve /build response already carries everything needed to attribute a build failure, and nothing was reading it.** Measured on 11.13: `problems` is an OBJECT whose inner `problems` list holds each consistency message with severity, errorCode and locations[] {module, document, element} \u2014 the document being `Microflow 'Test_test_3'` WITHOUT its module. So an error in a generated test microflow maps back exactly. The ratio is the point: a failing blank app returns 18 problems of which 1 is the error, and printing the body meant 11,580 bytes in which nothing marked the line that mattered; filtering severity==Error renders it as one line naming the test AND the decision. **Do not guess a response shape \u2014 POST to the serve API and look.** `mxbuild --serve --host=127.0.0.1 --port=N` plus a curl to /build is the whole harness, and no fixture in the repo had ever recorded a FAILING build.\n\n**The abandoned half is the more useful lesson.** Catching these earlier \u2014 refusing an unbound variable in an IF condition at injection time \u2014 was built, passed 465 check-mdl scripts and the whole unit suite, and was WRONG. `mxcli test` execs its microflows, and the microflow validator's scope model tracks variables where they are ASSIGNED; reusing it to check READS refuses valid work. Two independent holes, both found only by `make test-integration`: `$latestHttpResponse` is a Mendix system variable that no MDL statement declares, and a loop iterator is registered only when the list's type is known (`if listType, ok := fb.varTypes[...]`). Both refuse a microflow `mx check` accepts at 0 errors. **Generalisable: a variable model built for checking writes is not a variable model for checking reads** \u2014 the write side only has to know the names being bound, the read side has to know every name that can legally be in scope, including ones the platform supplies. Before reusing any scope model in the opposite direction, enumerate what populates it and assume the list is incomplete.\n\n**Process: `make check-mdl` is NOT the over-reach guard for an exec-path change.** It runs `mxcli check` with no project, so it exercises syntax only; a new refusal on the exec path sails through all 465 scripts. `make test-integration` (what CI runs, and runnable locally with mxbuild cached) execs every doctype script against a real project and runs `mx check` on the result \u2014 that is the guard, and skipping it cost a red CI. Also note the exec/check validator split (#833): a validator fix wired only into ValidateMicroflowBody looks correct under `check -p` and does nothing under `exec`.", "refs": ["ako/mxcli-sudoku FINDINGS #46 follow-up"], "symptom": "`mxcli test --local`: an @expect that is syntactically valid but only fails inside mxbuild takes down the ENTIRE run \u2014 `Error: local runtime: build failed: The project cannot be deployed, because it contains errors.` No test results at all, valid tests in the same file never run, and the cause arrives as ~200 lines of mxbuild JSON in which the real error sits among dozens of unrelated Atlas warnings."} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-10-mxcli-run-local-watch-cannot-start-mendix-11-14-app-starting.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-10-mxcli-run-local-watch-cannot-start-mendix-11-14-app-starting.json new file mode 100644 index 0000000000..2827968e31 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-10-mxcli-run-local-watch-cannot-start-mendix-11-14-app-starting.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-10", "symptom": "`mxcli run --local --watch` cannot start a Mendix 11.14 app: `starting web client bundler: no rollup.config.mjs in \u2026/deployment/web (run a serve Deploy build first)` \u2014 while plain `run --local` on the SAME project works. A serve Deploy build has just run", "cause": "The identical defect as ako/mxcli-ledger #146, in the SECOND copy of the gate. 11.14 bundles the browser client itself and emits no rollup config; #146 moved the gate onto the gap the step closes, but only in `BuildWebClient`. `StartWebClientWatch` carries its own independent `os.Stat(rollup.config.mjs)` check that the fix never touched \u2014 so the non-watch path started working and `--watch` kept dying on the same absent file, one call earlier. #146's own commit message claimed \"both call sites are fatal\"; the two it meant were both BuildWebClient's", "file": "`cmd/mxcli/docker/webclient_watch.go` (`StartWebClientWatch`), called fatally at `runlocal.go:659`", "insight": "**When a gate is duplicated, `grep` for the predicate before declaring the fix done** \u2014 the sibling here was one `os.Stat` of the same filename in the same package, and the commit that fixed one asserted it had fixed both. Fix shape: return a **nil watcher**, not an error, when `WebClientBundled(deployDir)` \u2014 there is no bundler to keep hot because mxbuild's serve build writes web/dist. That is only half the fix: the watch loop calls `Generation`, `WaitForRebuild` and `Log`/`Stop` with no nil branch, so the methods must be nil-receiver-safe or the startup refusal is merely traded for a SIGSEGV on the first file change (the control test panics against the unfixed build, which is what proves it). Keep a control asserting the 11.13 path still LAUNCHES the bundler, or the fix degrades to \"never watch anything\"", "refs": ["#146"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-10-mxcli-run-local-watch-mendix-11-14-app-first-build-succeeds.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-10-mxcli-run-local-watch-mendix-11-14-app-first-build-succeeds.json new file mode 100644 index 0000000000..d417448c7b --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-10-mxcli-run-local-watch-mendix-11-14-app-first-build-succeeds.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-10", "symptom": "`mxcli run --local --watch` on a Mendix 11.14 app: the FIRST build succeeds and the app boots, then every rebuild fails inside mxbuild \u2014 `Could not find a part of the path '\u2026/deployment/web/pages/.js'` when the change touched a page, or `Compilation of the app bundle failed` / `ERR_MODULE_NOT_FOUND \u2026 web/rollup.config.mjs` when it did not. Reads as a corrupt `deployment/`", "cause": "**mxbuild 11.14's own defect, not mxcli's.** The first build in an `mxbuild --serve` process does not leave the deployment in a state its own incremental build can continue from: neither the bundler config (`web/rollup.config.mjs`) nor the per-document client dirs (`web/pages/`, `web/layouts/`) survive it, and the next build needs both. Which one it dies on is just how far it gets \u2014 model changed \u2192 page export, model untouched \u2192 the bundler. Note `--serve`, `--host` and `--port` are in `mxbuild --help` but NOT on docs.mendix.com/refguide/mxbuild/, so this is an undocumented interface with no compatibility promise", "file": "`cmd/mxcli/docker/webclient_legacy_paths.go` (`legacyClientBuildHint`), called from the build-failure branch of `watchAndApply` in `runlocal.go`", "insight": "**Drive the vendor's API directly before blaming or absolving your own wrapper.** Three controls placed this in mxbuild in about ten minutes: (1) POST the SAME `/build` request twice over curl with the model untouched \u2014 Success then Failure, no mxcli in the picture; (2) run one-shot `mxbuild --target=deploy` TWICE into the same deployment dir \u2014 BUILD SUCCEEDED both times, so the 11.14 deployment shape is not the trigger, the serve process's own state is; (3) flip `EnableRspackBundler` in the model and repeat \u2014 identical failure naming `rspack.config.mjs`, which kills 'switch bundlers' as a workaround AND disproves the tempting 'it falls back to the legacy Rollup pipeline' story. Without (3) the diagnosis was plausible, quotable and wrong. mxcli cannot fix it, so the deliverable is a hint that fires on the failure's own SHAPE (message names the artifact AND the artifact is genuinely absent) rather than on a version number \u2014 so a fixed mxbuild goes quiet by itself, and it says what does NOT work (`rm -rf deployment/`, switching bundlers), because both are what a user tries first || FOLLOW-UP: the tempting workaround does NOT work, and it is worth knowing why before anyone tries again. The deleted config is recoverable \u2014 it sits on disk ~1.5s mid-build and carries nothing model-specific (input is just index.js; page discovery is delegated to a rollup plugin at build time), so it can be harvested the way `mxcli fix widgets` harvests mxbuild's output. Restoring it makes builds 2 and 3 succeed **with the model unchanged**, and fail the moment a page actually changes (missing web/pages/*.js); adding the missing dirs too moves it to a pluggable-widget export failure on two widgets that ship with a blank app, with web/dist/index.js never moving off its cold-build timestamp. So there are TWO regressions and only the visible one is recoverable. **Test a workaround against the case the feature exists for, not the case that is easy to set up** \u2014 an unchanged-model rebuild is exactly what a warm loop never does, and a green result there would have shipped a fix that helps nobody", "refs": ["#146"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-claude-bootstrap-mxcli-sh-sessionstart-hook-re-downloads-85.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-claude-bootstrap-mxcli-sh-sessionstart-hook-re-downloads-85.json new file mode 100644 index 0000000000..6ba64a8f25 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-claude-bootstrap-mxcli-sh-sessionstart-hook-re-downloads-85.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-11", "symptom": "`.claude/bootstrap-mxcli.sh` (the SessionStart hook) re-downloads ~85 MB of mxcli on EVERY fresh session in an environment where mxcli is already installed on PATH", "cause": "The script gated only on `[ ! -x ./mxcli ]` and never consulted PATH, while the bootstrap skill tells you to `rm -f /mxcli` after moving the project to the repo root \u2014 so in a session image with mxcli pre-installed the guard could never be satisfied by the binary already present, forever", "file": "`cmd/mxcli/init_hook.go` (`bootstrapScriptTemplate`)", "insight": "**A guard that asks 'is the artifact HERE' rather than 'is it AVAILABLE' pays full cost for something already on the machine.** Hardlink a PATH copy in first (what `mxcli new` itself does), falling back to symlink across filesystems then to a copy, and only download when none of those work \u2014 every path leaves ./mxcli working, which the rest of the script and the generated project CLAUDE.md both assume. Keep BOTH controls when testing: nothing on PATH must still download (not silently no-op), and an existing ./mxcli must be left untouched. Reported by ako/ChipCoV1", "refs": ["ako/ChipCoV1 FINDINGS.md"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-mxcli-marketplace-update-id-v-p-app-mpr-exits-0-reports-n.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-mxcli-marketplace-update-id-v-p-app-mpr-exits-0-reports-n.json new file mode 100644 index 0000000000..b60da6807a --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-mxcli-marketplace-update-id-v-p-app-mpr-exits-0-reports-n.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-11", "symptom": "`mxcli marketplace update --to -p App.mpr` exits 0, reports \"N units copied, N element identities preserved, N role grant(s) restored\", and leaves the project at **CE0066** \"Entity access is out of date. Please update security by clicking the 'Update security' button in the domain model editor\" at \"Domain model of module ''\". Its \"Next, repair what a headless update leaves behind\" block names CE0463 and CE6087 only, so the error reads as needing Studio Pro — reported as \"no headless fix available\", breaking unattended upgrade pipelines (mendixlabs/mxcli#1085, against 0.21.0 on Mendix 11.12.4, UserCommons 2.1.3 -> 2.2.0). `marketplace install` has the same gap.", "cause": "A transplant is the ONE mxcli write path that does not reconcile as it writes. `TransplantModule` copies the incoming module's units in verbatim through the raw-unit writer, and `RestoreRoleGrants` (cmd/mxcli/marketplace/grants.go) runs its statements with `ex.Execute(stmt)` in a loop rather than `ExecuteProgram`, so `finalizeProgramExecution` — the only thing that calls `ReconcileMemberAccesses` outside the entity/grant/association handlers — never runs. An access rule that does not cover every member of its entity therefore arrives exactly as the package shipped it. Fixed by `marketplace.ReconcileModuleSecurity` (new, cmd/mxcli/marketplace/security.go) called at the end of `PerformUpdate` and `PerformInstall`, non-fatally (the module is already replaced; aborting there would leave the project mid-update), with the count reported by `reportSecurityReconcile` and the failure path naming `update security `.", "file": "cmd/mxcli/marketplace/security.go (new, ReconcileModuleSecurity), cmd/mxcli/marketplace/update.go (PerformUpdate, PerformInstall, UpdateResult.RulesReconciled/SecurityErr), cmd/mxcli/cmd_marketplace_update.go (reportSecurityReconcile), cmd/mxcli/cmd_marketplace_install.go; tests cmd/mxcli/marketplace/security_test.go; example mdl-examples/bug-tests/marketplace-1085-security-after-transplant.mdl", "insight": "**The control is what localises this one, and it is cheap: transplant the module UNCHANGED.** On the vendored 11.6.6 app (Administration 4.3.2, mxbuild 11.14.0) an unchanged update checks at 0 errors, and removing ONE member entry from the incoming version's rules gives exactly the reported CE0066 — so the machinery does not create the stale rule, it transmits one, and the fix belongs at the point of arrival rather than anywhere in the copy. Three things worth carrying. (1) The reporter's premise was half wrong in a way that matters more than the bug: a headless repair already existed (`UPDATE SECURITY`, #1047), but it appears in NO user-facing surface — not docs-site, not MDL_QUICK_REFERENCE.md, not `mxcli syntax`, not the update's own output — only in a skill and the CHANGELOG. An undiscoverable capability and a missing one are the same bug report. (2) Their binary could not have used it anyway: v0.21.0 (2026-09-06) has the System-skip fix but NOT the inherited-attribute reconcile that landed 2026-09-07 (`collectAttrs` is absent), so where the missing member is inherited from a same-module generalization it would have printed \"All entity access rules are up to date\" over a broken model. Check the reported version against the fix DATE, not against the fix existing. (3) Reconciling unconditionally would be wrong, and the second test is the one that says so: a module whose rules are already complete must come through untouched, because rewriting a marketplace module's domain model makes `marketplace diff` read it as a local edit — and a local edit is what makes the NEXT update refuse. The repair cannot be demonstrated by an .mdl script either: every MDL write path reconciles as it writes, so the stale state has to be built by editing stored BSON in the test.", "ce": ["CE0066"], "refs": ["mendixlabs/mxcli#1085", "mendixlabs/mxcli#1047"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-mxcli-theme-create-from-design-css-seeds-palette-nothing.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-mxcli-theme-create-from-design-css-seeds-palette-nothing.json new file mode 100644 index 0000000000..7f166b3d25 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-mxcli-theme-create-from-design-css-seeds-palette-nothing.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-11", "symptom": "`mxcli theme create --from ` seeds the palette and nothing else: the scaffolded brand theme still described itself in `theme list` as 'Cool slate, one teal signal colour' with Signal's six swatches, and vendored ~500 KB of IBM Plex woff2 the seeded --mxt-font never names, plus a SIL OFL licence for fonts it does not use. Separately, the primary button was never the brand colour: Atlas derives --btn-primary-bg from --brand-primary-600 = color-mix(brand, contrast 20%), so a brand blue #10069F rendered rgb(21,13,140)", "cause": "`manifest()` copied the base theme's Summary and Colorway verbatim and the walk copied every file unconditionally. The Atlas map pinned `--btn-primary-color` to `--mxt-brand-ink` \u2014 an ink each theme picks to sit on `--mxt-brand` (console pairs near-black #04211d with bright teal #2dd4bf) \u2014 while leaving the background to Atlas's derivative, so the pairing the theme designed for was never the pairing that rendered. The map's own comment already called it 'a brand-filled button'", "file": "`cmd/mxcli/theme/create_seeded.go`, `create.go` (`manifest`, the scaffold walk), `assets/*/files/theme/web/_mxcli-atlas-map.scss`", "insight": "**Inheriting a statement ABOUT the base theme into a theme whose palette is no longer the base's is a confident lie; derive it or drop it.** Two traps in the font half. (1) The decision must be made BEFORE the walk: it is taken by reading the partial and applied to files elsewhere in the tree, so doing it inline depended on WalkDir's lexical order putting `_mxcli-.scss` before `mxcli-fonts/` \u2014 true only because of the leading underscore. (2) It touches two halves \u2014 the @font-face rules and the woff2 files \u2014 and getting either alone wrong is silent: a surviving rule for a deleted file 404s, a surviving file nothing loads is the dead weight being removed. Unit tests on each half cannot catch a mismatch; the guard is an integration assertion that a scaffolded theme ships exactly the fonts it loads (control: stubbing the file half fails it with 'X is shipped but no @font-face loads it'). Reported by ako/ChipCoV1", "refs": ["ako/ChipCoV1 FINDINGS.md"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-record-narrated-demo-skill-mandates-mobile-pass-recorded.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-record-narrated-demo-skill-mandates-mobile-pass-recorded.json new file mode 100644 index 0000000000..0d8b2bedb1 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-record-narrated-demo-skill-mandates-mobile-pass-recorded.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-11", "symptom": "The record-narrated-demo skill MANDATES a mobile pass ('Recorded at both a desktop viewport and a real mobile device profile') and its own `take.js` cannot perform one: the 'mobile' take films the DESKTOP app in a narrow window. Separately, the caption plate made any control it covered unclickable \u2014 Playwright retried 30s against 'div.text from div#demo-narration subtree intercepts pointer events' and failed the take", "cause": "`openTake` called `browser.newContext({viewport, recordVideo})` and nothing else, but `userAgent`/`isMobile`/`hasTouch`/`deviceScaleFactor` are CONTEXT options \u2014 and **Mendix picks its navigation profile from the user agent, not the viewport**, so shrinking the viewport never routes to the phone profile. The plate had no `pointer-events: none` (only `#demo-spot` did) while being a full-width bar pinned to the bottom \u2014 exactly where Mendix puts a page footer's buttons", "file": "`.claude/skills/mendix/record-narrated-demo/take.js` (`openTake`), `narrate.js` (`OVERLAY_CSS`); embed mirror under `cmd/mxcli/skills/` is regenerated by `make sync-skills`", "insight": "**When a skill mandates a verification pass, check that the tool it ships can actually perform it** \u2014 this one could not, and nothing said so: the take succeeded, produced a plausible film, and simply filmed the wrong thing. A verification step that cannot fail is worse than no step, because it is counted as done. The fix is a `contextOptions` passthrough spread FIRST so `viewport`/`recordVideo` still win (a device preset carrying its own viewport would letterbox every take). For the plate, `pointer-events: none` makes a covered control CLICKABLE and `body { padding-bottom }` makes it VISIBLE \u2014 the film needs both, since a click landing under an opaque caption is the same dead beat by another route. Reported by ako/ChipCoV1, which shipped an app whose phone walk was impossible while seven browser tests stayed green", "refs": ["ako/ChipCoV1 FINDINGS.md"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-two-opposite-brain-plan-failures-one-root-cause-requirement.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-two-opposite-brain-plan-failures-one-root-cause-requirement.json new file mode 100644 index 0000000000..1613149c5f --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-11-two-opposite-brain-plan-failures-one-root-cause-requirement.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-11", "symptom": "Two opposite `brain plan` failures with one root cause. A requirement anchored at a bare MODULE (`@Maintenance`) reports BUILT the moment the module exists, with none of its work done. A requirement anchored at a MODULE ROLE (`@Maintenance.Coordinator`) reports PLANNED forever, even once the roles exist \u2014 `describe` refuses it ('no describable document named \u2026') and `mxcli refs` finds nothing", "cause": "`catalogResolver.Resolve` in `cmd/mxcli/cmd_brain.go`. A module has a row in the catalog's `objects` view, so it resolves immediately \u2014 fine for a decision (anchors point backward) and useless for a requirement (anchors point forward, so resolution IS the progress signal). A module role is in NEITHER the objects view NOR `FindDocumentUnit`, because it is not a document \u2014 so both lookups miss and it falls through to NotFound, which for a requirement means 'not built yet' permanently", "file": "`cmd/mxcli/cmd_brain.go` (`catalogResolver.Resolve`, `moduleRoleExists` via `GetModuleSecurity`), `cmd/mxcli/brain/entry.go` (`requirementAnchorsArePlannable`)", "insight": "**A resolver's vocabulary has to cover what people actually anchor at, and the two failure directions need opposite fixes.** The module-role gap is a LOOKUP gap \u2014 fixed by consulting `GetModuleSecurity`, case-insensitively because Mendix treats role names that way and an anchor is hand-written. The module-anchor gap is SEMANTIC and cannot be fixed by lookup: the anchor resolves correctly and is still useless, so it is refused at capture time with the alternative named (an author told only 'no' deletes the anchor, which loses the signal entirely rather than fixing it). Keep the control in BOTH directions: a module anchor stays legal on a decision and on a question, or the fix degrades to 'refuse every module anchor' and the negative test still passes. Verified end to end: `@MyFirstModule.User` \u2192 1 anchor 1 resolved; `@MyFirstModule.NoSuchRole` \u2192 NOT FOUND, exit 1, so the resolver did not simply become permissive. Reported by ako/ChipCoV1", "refs": ["ako/ChipCoV1 FINDINGS.md"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-13-build-test-fails-ci.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-13-build-test-fails-ci.json new file mode 100644 index 0000000000..55ad816282 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-13-build-test-fails-ci.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-13", "symptom": "`build-and-test` fails in CI on `TestSettleSourceReturnsPromptlyForOneChange` \u2014 \"a quiet source took 196.975373ms to settle, want under 100ms\" \u2014 while the SAME tree passes in another run of the same workflow minutes earlier", "cause": "The test bounded elapsed wall-clock time as a multiple of the poll interval (`poll * (sourceSettleWindow + 3)`, 100ms against a nominal 40ms). settleSource waits on `time.After(poll)`, which guarantees AT LEAST the duration and nothing about the upper bound, so a loaded runner blows the budget with no defect present.", "file": "cmd/mxcli/docker/runlocal.go (settleSourceWith, the injected tick), cmd/mxcli/docker/runlocal_settle_test.go", "insight": "The property being guarded was a POLL COUNT, not a duration \u2014 'a quiet source costs one extra poll' \u2014 so the fix is to make polls countable (inject the timer) rather than to widen the budget, which only moves the flake threshold. Diagnosis shortcut worth reusing: the same workflow ran twice on the same tree, once from the push event and once from the pull_request merge commit, and disagreed \u2014 two runs of one tree is direct evidence of nondeterminism and cheaper than reading the test. Two things the controls settled that reasoning did not: (1) the assertions are written in terms of `sourceSettleWindow`, so WIDENING that constant leaves both tests green \u2014 they assert the loop honours whatever window is declared, never the number itself, and the real control is a loop that costs one poll MORE than it declares (both fail). (2) Each tick call must return a freshly-armed channel; returning one shared channel makes the multi-file test HANG rather than miscount, so the re-arm is load-bearing and not a style choice. The seam also made a previously untestable guarantee expressible: the window must be sourceSettleWindow CONSECUTIVE quiet polls, and dropping `quiet = 0` from the change branch was green against every pre-existing test in the file.", "refs": ["ako/mxcli#449"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-13-mxcli-run-local-ensure-db-cannot-provision-postgresql-non.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-13-mxcli-run-local-ensure-db-cannot-provision-postgresql-non.json new file mode 100644 index 0000000000..a3a71e0df9 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-13-mxcli-run-local-ensure-db-cannot-provision-postgresql-non.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-13", "symptom": "`mxcli run --local --ensure-db` cannot provision PostgreSQL in a non-root devcontainer (Debian/Ubuntu base, remoteUser vscode): reported as a bare \"PostgreSQL did not become ready at 127.0.0.1:5432 within 20s\", or as `exec: \"initdb\": executable file not found in $PATH`. Fires on every fresh Claude Code session in an initialized project, since `mxcli init` wires `run --local --setup --ensure-db` into the SessionStart hook", "cause": "THREE independent defects on one path, each sufficient to block it. (1) `service postgresql start` ran unelevated: Debian's /etc/init.d/postgresql runs under `set -e` and calls create_socket_directory FIRST, which chmods /var/run/postgresql — refused for a non-root user, so the script aborts before it looks at a single cluster. (2) The #823 user-owned-cluster fallback was INERT on Debian/Ubuntu: postgresql-common wraps only the CLIENT tools (psql, pg_isready, pg_ctlcluster) into /usr/bin, while initdb/pg_ctl live in /usr/lib/postgresql//bin — so the safety net for a failed service start could never deploy on the platform that needs it most. (3) `resolveSuperuser` used `sudo -n -u postgres psql`, but mcr.microsoft.com/devcontainers/base grants its user sudo to root ONLY (`vscode ALL=(root) NOPASSWD:ALL`, confirmed in devcontainers/features main.sh) — so the target is refused even though the user is effectively an administrator. Plus a diagnostic defect: the 20s readiness timeout discarded the service-manager output the package had already collected", "file": "`cmd/mxcli/docker/ensuredb.go` (`serviceStartAttempts`, `postgresServerBinDir`/`postgresTool`, `superuser.viaRoot`, `withServiceDiag`)", "insight": "mxcli GENERATES the broken environment — `generateDockerfile` emits that exact base image, installs postgresql, and runs as vscode — so this was not user misconfiguration, and fixing it in code (not the template) also repairs projects already scaffolded. Root may target any account, so `sudo -n -- sudo -n -u postgres` reaches postgres under a root-only sudoers policy; try the direct form first and nest only on refusal. Resolve initdb and pg_ctl from the SAME bin directory and rank majors NUMERICALLY — a lexical sort puts \"9\" above \"16\", and a data directory made by one major cannot be started by another. **Measurement trap that cost the most time**: reasoning about which error the user would see is unreliable here — four plausible code paths produce four different messages, and the reported wording was reproducible by none of them on Ubuntu 24.04/PG16. What settled it was building a harness that calls `EnsureDatabase` directly and running it as a real non-root user with the real sudoers rule, then isolating each defect with a one-variable control (widen sudoers to `(ALL)` and nothing else changes → provisioning succeeds; prepend /usr/lib/postgresql/16/bin → the fallback completes). Each of the four fixes was reverted individually and its test re-run: two controls initially failed to COMPILE rather than reproducing the symptom, which proves nothing — they were redone faithfully before being believed", "refs": ["mendixlabs/mxcli#984", "#823"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-15-porting-cmd-mxcli-docker-off-sdk-mpr-moved-two-write-paths.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-15-porting-cmd-mxcli-docker-off-sdk-mpr-moved-two-write-paths.json new file mode 100644 index 0000000000..61f033511f --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-15-porting-cmd-mxcli-docker-off-sdk-mpr-moved-two-write-paths.json @@ -0,0 +1 @@ +{"area":"cmd/mxcli","date":"2026-09-15","symptom":"Porting cmd/mxcli/docker off sdk/mpr moved two WRITE paths (ensureDemoUsers, applyHarvest) onto the codec backend. A baseline diff of `docker check` showed the project byte-identical across 421 files — which proved nothing, because the run had not written anything.","cause":"docker check's widget-update harvest is a no-op on an already-clean fixture, so an output+filetree diff against a pre-port binary exercises only the READ paths. Coverage then showed ensureDemoUsers at 0.0% — a write path the port touched that no test in the package ran.","file":"cmd/mxcli/docker/build.go","fix":"Added TestEnsureDemoUsers_CreatesAdminWhenNoneExist and _SkipsWhenUsersExist, plus a clearDemoUsers helper that establishes the precondition. Coverage 0.0% -> 76.5%. The read paths keep the baseline-diff evidence; applyHarvest was already at 76.9% via TestRunUpdateWidgets_RestoresV2AfterConversion.","insight":"A byte-identical baseline diff is strong evidence for a READ port and near-worthless for a WRITE port, because the natural control (nothing changed) is also what a no-op produces. The two need different instruments, and the cheap way to tell which you have is `go test -coverprofile` + `go tool cover -func` grepped for the functions you touched: it answers 'did my port's code even run' in one command, where a passing suite does not. Here it separated applyHarvest (76.9%, genuinely exercised including its UpdateRawUnit) from ensureDemoUsers (0.0%) inside the same package, so the gap was specific rather than a general absence of tests. Second trap, hit while fixing it: the shared v2 fixture ALREADY HAS two demo users, so the create-path test skipped and the idempotence test asserted the wrong count. Skipping on an unmet precondition is the #808 shape — set the precondition up instead (RemoveDemoUser in a helper, then assert the helper actually emptied it before proceeding). Third: read back through a FRESH connection, since asserting on the value the writer still holds passes against a write that never reached disk."} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-15-porting-last-cmd-mxcli-readers-off-sdk-mpr-cmd-extract.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-15-porting-last-cmd-mxcli-readers-off-sdk-mpr-cmd-extract.json new file mode 100644 index 0000000000..05baf048e0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-15-porting-last-cmd-mxcli-readers-off-sdk-mpr-cmd-extract.json @@ -0,0 +1 @@ +{"area":"cmd/mxcli","date":"2026-09-15","symptom":"Porting the last cmd/mxcli readers off sdk/mpr, cmd_extract_templates.go compiled with a type error (RawType/RawObject are bson.D on sdk/mpr, any on types.RawCustomWidgetType). Casting past it would have compiled — and broken the command at runtime, because FindCustomWidgetType is UNIMPLEMENTED on the codec backend.","cause":"mdl/backend/modelsdk/unimplemented_gen.go carries FindCustomWidgetType; measured at runtime it returns 'FindCustomWidgetType is not implemented on the model engine. This should be unreachable'. cmd_extract_templates.go was calling it through a concrete *mpr.Reader, so it was reachable only by NOT going through the backend.","file":"cmd/mxcli/cmd_extract_templates.go","fix":"Left this one file on sdk/mpr with a comment saying why and what would fix it (implement FindCustomWidgetType on the codec backend), and ported the other five. cmd/mxcli is otherwise clean; importers 13 -> 8.","insight":"The type error was the lucky part. A compile error is the ONLY reason this did not ship as a runtime failure — the cast that silences it is one line, and nothing else would have objected. When a port hits a type mismatch at a backend boundary, check whether the backend method is implemented at all before reconciling the types: `grep -n '' mdl/backend/modelsdk/unimplemented_gen.go` answers it in one command, and a runtime probe (connect read-only, call it, log the error) confirms it in under a minute. Note the direction of the trap: the unimplemented method's own error says 'This should be unreachable', and porting a caller to the backend is precisely what MAKES it reachable — so the #477 census blind spot (callers holding a concrete reader are invisible) cuts both ways. Second, smaller measurement trap in the same slice: a baseline diff of `check --post-migration` showed 50 lines vanishing, which looked like a regression and was not — the FIRST run built and cached a catalog inside the project, so the second run reused it. Two binaries must each get their own fresh copy of the fixture, exactly as for a write port; a command that caches into the project directory makes consecutive runs non-independent even when nothing is being written on purpose."} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-16-mendixlabs-mxcli-1103-retrieve-limit-inside-test-mdl-block.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-16-mendixlabs-mxcli-1103-retrieve-limit-inside-test-mdl-block.json new file mode 100644 index 0000000000..7f24feb1e1 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-16-mendixlabs-mxcli-1103-retrieve-limit-inside-test-mdl-block.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-16", "symptom": "mendixlabs/mxcli#1103: a RETRIEVE with LIMIT inside a .test.mdl block was reported as `mismatched input 'LIMIT' expecting {GROUP_BY, SELECT, HAVING}` — the OQL follow set — on the statement `mxcli syntax microflow.retrieve` prints as its own example. The reporter concluded the test-block path routes microflow statements into the OQL parser.", "cause": "It does not. The generated MxTest.Test_* microflow parses fine (measured end-to-end against a real 11.6.6 project: the flow was created with the LIMIT intact). The message came from `mxcli check`/the LSP being pointed at the .test.mdl file itself, which they parsed as top-level MDL. A test block is a MICROFLOW BODY: DECLARE is not a top-level statement, the parser resyncs, RETRIEVE is a NON-RESERVED keyword so it is swallowed as an identifier, and the leftover `FROM …` starts oqlQueryTerm's FROM-first alternative (mdl/grammar/domains/MDLCatalog.g4), whose follow set is exactly {GROUP_BY, SELECT, HAVING}.", "file": "cmd/mxcli/testrunner/check_source.go", "fix": "testrunner.CheckSource renders each block as the microflow it becomes, padded so every body keeps its SOURCE line numbers (wrapper fragments go on the lines the doc comment and the '/' separator occupied). cmd_check.go and lsp_diagnostics.go translate before parsing, so all downstream rules apply unchanged and no diagnostic needs remapping. .test.mdl files joined `make check-mdl`; `.fail.test.mdl` names one whose annotations are deliberately unusable.", "insight": "Two lessons. First: the reporter's diagnosis was precise, confident and wrong, and the fastest way to find that out was to run the pipeline rather than read it — dumping GenerateTestFlows' output and feeding it to visitor.Build took one throwaway test and settled in seconds what an hour of grepping had not. Their error message was real; the command that produced it was not the one they named. Second, the general shape: a tool that OWNS a file format must not hand that format to a parser for a different one. The VS Code extension binds MDL to `.mdl`, which `.test.mdl` matches, so every test file in the editor was a wall of squiggles — 9 of this repo's 10 test files reported errors, one of them 392, and nobody had noticed because nobody runs `mxcli check` on a test file. When adding a derived file format, check what the EXISTING tooling makes of it; the answer is rarely 'nothing'. Line-preserving padding is what makes the translation honest: render into a slice of the source's own length and place wrapper fragments only on lines the original spent on comments or separators, and a diagnostic's line:col is the author's without a mapping table to drift."} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-16-mendixlabs-mxcli-1104-mxcli-test-attach-reported-only-build.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-16-mendixlabs-mxcli-1104-mxcli-test-attach-reported-only-build.json new file mode 100644 index 0000000000..d78ae8c921 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-16-mendixlabs-mxcli-1104-mxcli-test-attach-reported-only-build.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-16", "symptom": "mendixlabs/mxcli#1104: `mxcli test --attach` reported only 'build failed: The project cannot be deployed, because it contains errors.' on an injection failure, and afterwards EVERY later run of ANY test file failed the same way until a leftover document was found by hand.", "cause": "Two independent defects. (1) The parsed problems were in hand and discarded: runner_attach.go and LocalApp.Rebuild both built their error with `fmt.Errorf(\"build failed: %s\", build.Message)`, and Message is identical for every failing build. Attribution (build_attribution.go, BuildResult.ErrorSummary) existed but was wired only into the --local BOOT, so --attach and every --watch rebuild lost it. (2) Generated names are positional — MxTest.Test_test_1, _2, … from the test's index in its file — and every test file reuses them, while cleanup dropped only the CURRENT suite's names. A run with fewer tests than the last one therefore left the surplus behind, and under --attach the MxTest module always pre-exists (the dev loop installed it) so the whole-module drop never fires.", "file": "cmd/mxcli/testrunner/cleanup_leftovers.go", "fix": "buildFailure()/resultsForBuildFailure() shared by both runners; cleanup keys on what the project HOLDS (SHOW MICROFLOWS IN MxTest, filtered by the generated prefix) with the suite only as a fallback; reportCleanup names every surviving document and prints its DROP.", "insight": "Measured, not reasoned: planted one bad MxTest.Test_test_2 in a project, ran a known-good one-test suite, and watched it fail and leave the leftover in place — then after the fix watched run 1 fail and CLEAN, and run 2 pass. A self-healing sequence is the control that 'cleanup works' cannot be argued into. The general rule for generated artefacts: derive what to remove from what EXISTS, never from what you intended to create. Keying on the suite was wrong in both directions at once — it missed leftovers AND issued DROPs for flows a part-way injection never created, and those failures made cleanup report 'the project has been left modified' for a project it had just cleaned, which is a false alarm that sends the reader hunting for damage. Positional names (index-in-file) guarantee collisions across files and are worth avoiding, but as long as they exist the prefix is the only safe key. Also worth pinning: once BuildFailedError.Error() renders the errors, a hint that repeats them prints everything twice — assert on the message the READER sees, not on the hint in isolation."} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-macos-matching-studio-pro-installed-run-local-test-local.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-macos-matching-studio-pro-installed-run-local-test-local.json new file mode 100644 index 0000000000..c43b369226 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-macos-matching-studio-pro-installed-run-local-test-local.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-17", "symptom": "On macOS with the matching Studio Pro installed, `run --local` and `test --local` fail in the build step: either `Project version '11.12.2' does not exactly match MxBuild version '11.14.0'` or `mxbuild at ... is a linux binary and cannot run on darwin`. `mxcli setup mxbuild` prints Studio Pro's path and changes nothing, so the remedy every error suggests is a loop", "cause": "Two resolvers answered 'which mxbuild', and the wrong one won. The local entry points call `ResolveMxBuildForLocal` (#916) and then pass the result ONLY to the web-client bundler; both `ServeOptions` literals omitted `MxBuildPath`, so `StartServe` re-resolved from `~/.mxcli/mxbuild/` and, on a miss, took `AnyCachedMxBuildPath()` \u2014 the NEWEST cached version, unrelated to the project's. `test --local` was worse: it never resolved at all, still calling `DownloadMxBuild` directly, the exact call #916 removed from `runlocal.go` and left in `localapp.go`", "file": "`cmd/mxcli/docker/mxserve.go` (new: `serveOptionsFor`, `resolveServeMxBuild`, `noServeMxBuildError`) + `runlocal.go` + `localapp.go` (the two ServeOptions literals)", "insight": "The half-populated cache directory that defeats `CachedMxBuildPath` is **mxcli's own work, two steps earlier in the same command**: `ensureMxBuildRuntimeSibling` does `MkdirAll` on `~/.mxcli/mxbuild//` to plant the runtime symlink, and on macOS nothing ever puts a `modeler/` beside it because mxbuild comes from Studio Pro. So this is not a user with a damaged cache \u2014 it is the guaranteed state for every macOS project whose version was not already downloaded, which is why it reads as 'run --local has never worked here'. Repro without a Mac: plant a cache entry for one version and call `StartServe` asking for another. **The control matters more than usual**: asserting only that the call FAILS proves nothing, because the old code also failed \u2014 one exec later, with `exec format error` \u2014 so the assertion has to be that the refusal names the missing version and happens before any exec. A first draft of the modeler-less test passed against unfixed code for exactly that reason. Fix shape, per docs-wiki/bug-patterns/duplicate-resolver-drift.md: remove the second answer rather than synchronise them \u2014 one `serveOptionsFor` both call sites go through, and a resolver that refuses to substitute a near-miss version instead of handing mxbuild a binary it will reject minutes later, after a cold model load. Issues #1122, #1124", "refs": ["#1122", "#1124", "#916"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-mxcli-new-version-10-24-25-mxcli-setup-mxbuild-version-10-24.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-mxcli-new-version-10-24-25-mxcli-setup-mxbuild-version-10-24.json new file mode 100644 index 0000000000..75b32bd27f --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-mxcli-new-version-10-24-25-mxcli-setup-mxbuild-version-10-24.json @@ -0,0 +1 @@ +{"area": "cmd-mxcli", "date": "2026-09-17", "symptom": "`mxcli new --version 10.24.25` (and `mxcli setup mxbuild --version 10.24.25`) dies with `HTTP 404 from https://cdn.mendix.com/runtime/mxbuild-10.24.25.tar.gz`. Every 9.x and 10.x version probed 404s while 11.6.0/11.12.1/11.13.0 return 200 from the same host and path, which reads as 'Mendix 10 is no longer on the CDN'.", "cause": "Mendix 9 and 10 publish FOUR-part artifact names carrying a build number the release notes never mention: the release called 10.24.25 is `mxbuild-10.24.25.122571.tar.gz`. Mendix 11 publishes three parts. `MxBuildCDNURL` interpolates whatever string it is handed and nothing resolved a partial version, so a hand-typed 10.x version named no artifact at all. Project-driven paths were never affected — the MPR's `_ProductVersion` already carries all four parts (`10.24.25.122571`) and `parseVersion` takes the first three for major/minor/patch while the full string goes to the URL.", "file": "`cmd/mxcli/docker/version_resolve.go` (ResolveCDNVersion, highestBuild, CDNReleasesFor); wired at the two entry points where a user types a version, `cmd/mxcli/cmd_new.go` and `cmd/mxcli/setup.go`. Tests `cmd/mxcli/docker/version_resolve_test.go`.", "insight": "**A uniform 404 across a whole major version is evidence about the NAME, not about availability.** The conclusion drawn from it — 'Mendix 10 cannot be downloaded here' — blocked a verification for an entire session, and the fix was one listing call: the CDN is an S3 bucket that answers ListObjectsV2 (`?list-type=2&prefix=runtime/mxbuild-10.24.`), so what exists is enumerable rather than guessable. When a probe fails identically for every input in a class, question the query before concluding the class is empty. Three traps in the resolution itself, each a test: the `.sha256` sidecar beside every archive must not be picked as an artifact; the prefix needs its trailing dot or `10.24.2` swallows `10.24.20`..`10.24.26`; and build numbers are not zero-padded, so a text sort puts 99999 above 122571 and 10.24.9 above 10.24.26. Resolve at the entry point and thread the RESOLVED string onward — `mxcli new` checks the created project's stamp against the requested version, and `mx create-project` stamps four parts, so resolving late would fail that postcondition.", "refs": ["#1121"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-mxcli-run-local-mxbuild-path-x-p-app-mpr-answers-error.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-mxcli-run-local-mxbuild-path-x-p-app-mpr-answers-error.json new file mode 100644 index 0000000000..1d838e12c7 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-mxcli-run-local-mxbuild-path-x-p-app-mpr-answers-error.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-17", "symptom": "`mxcli run --local --mxbuild-path /x -p app.mpr` answers `Error: unknown flag: --mxbuild-path`, while the shipped run-local skill, runlocal.go's own comment and two resolution error messages all tell the user to pass it", "cause": "The flag was never registered on `runCmd`; it exists only on the four `docker` subcommands. Everything BEHIND it was already wired \u2014 `LocalRunOptions.MxBuildPath` is declared and `ResolveMxBuildForLocal` honours it \u2014 so the gap was one missing `Flags().String` and one missing field assignment, invisible to every test because nothing exercised the command's flag set", "file": "`cmd/mxcli/cmd_run.go` (flag registration + LocalRunOptions.MxBuildPath) + `.claude/skills/mendix/run-local/SKILL.md` + `docs-site/src/tools/run-local.md`", "insight": "The interesting defect is not the missing flag, it is that **the error messages recommending it were the only documentation of it** \u2014 guidance naming an option the command does not accept is worse than no guidance, because it reads to the user as their own mistake, and on macOS it was the only advertised way out of a platform mismatch. The regression test to write is therefore not 'the flag exists' but the invariant: scan the resolution sources for `--flag` strings they tell users to pass, and assert `run` registers each one (`TestErrorGuidanceNamesAFlagThatExists`). Watch the parse test \u2014 `-p` is PERSISTENT on rootCmd, so `runCmd.Flags().Parse` rejects it with `unknown shorthand flag: 'p'` and the test fails for a reason unrelated to the fix; resolve through `rootCmd.Find` to reproduce the reporter's line honestly. Issue #1125", "refs": ["#1125", "#916", "#1122"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-mxcli-run-local-project-settings-web-ui-optimizedclient-no.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-mxcli-run-local-project-settings-web-ui-optimizedclient-no.json new file mode 100644 index 0000000000..eaf5396797 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-17-mxcli-run-local-project-settings-web-ui-optimizedclient-no.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-17", "symptom": "`mxcli run --local` on a project with Settings > Web UI > OptimizedClient = No exits 1 after the cold build with `no rollup.config.mjs and no bundle at .../web/dist/index.js ... the build did not produce a client` \u2014 about a deployment whose client is sitting in the same directory. `mxcli docker run` works", "cause": "The client gate tested for exactly two shapes (a rollup config to run, or a bundle mxbuild already wrote) and treated everything else as a failed build. A classic (Dojo) deployment is a legitimate third shape with NEITHER: there is no bundling step for the classic client in any Mendix version. Nothing in the local loop read UseOptimizedClient, so the mode was invisible", "file": "`cmd/mxcli/docker/webclient_plan.go` (new: `planWebClient`, `isClassicWebClient`, `noWebClientError`) + `webclient.go` (`BuildWebClient`, `ensureWebClientBundle`) + `webclient_watch.go` (`StartWebClientWatch`) + `runlocal.go` (`ensureClientServed`)", "insight": "Measured on one blank 11.12.2 app built twice, changing only the setting: **mxbuild swaps which client lands in `deployment/web/` and parks the other beside it** \u2014 OptimizedClient=Yes gives `web/` the React client (+`rollup.config.mjs`) and parks Dojo in `dojo-web/`; =No gives `web/` the Dojo client (`index.html` loading `mxclientsystem/mxui/mxui.js`, no rollup config, no dist) and parks React in `react-web/` WITH its own rollup config. So detect from the DEPLOYMENT, not from the model's setting: the deployment is what gets served, the two disagree exactly when the setting has just changed, it needs no plumbing through the five call sites, and it covers MigrationMode without predicting what that mode emits. Detect on POSITIVE evidence (the entry point names its client) \u2014 inferring classic from the absence of the React shapes would make every genuinely broken deployment look classic and silently skip the bundle, which is the black screen the gate exists to prevent; keep a control test that a clientless deployment still fails. **The gate had FIVE consumers, not one**: boot, the `--watch` bundler, the post-boot re-bundle guard, and `ensureClientServed`, which probes that `/dist/index.js` is *served* \u2014 measured 404 on a classic app, so fixing only the boot moves the failure to every applied change under `--watch`. Two of them carried hand-copied duplicates of the same gate and had already drifted once (the 11.14 fix, ako/mxcli-ledger #146, landed on `BuildWebClient` only, so `run --local` worked on 11.14 and `run --local --watch` did not) \u2014 so the fix collapses them into one `planWebClient` rather than adding a third copy. Repro from Linux with no Mac and no Studio Pro: `mxcli new` an 11.12.2 app, flip `UseOptimizedClient` to `No` on `Forms$WebUIProjectSettingsPart` (note the `Forms$` prefix, not `Settings$`), build, and keep the unflipped copy as the control. Patching that BSON with a Go `map` corrupts the file \u2014 mxbuild refuses it with `Expected '$ID' as the first property of a storage object` \u2014 because map iteration loses key order; use `bson.D` throughout. Verified in a browser, not just at the gate: `mx` global present, real page content, zero console errors. Issue #1123", "refs": ["#1123", "ako/mxcli-ledger#146"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-18-mendixlabs-mxcli-1025-mxcli-syntax-advertises-mxcli-syntax.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-18-mendixlabs-mxcli-1025-mxcli-syntax-advertises-mxcli-syntax.json new file mode 100644 index 0000000000..3f5f74a049 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-18-mendixlabs-mxcli-1025-mxcli-syntax-advertises-mxcli-syntax.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-18", "symptom": "mendixlabs/mxcli#1025: `mxcli syntax` advertises `mxcli syntax workflow user-task targeting` in its own help and answers `Unknown topic: workflow user-task targeting`. Same for `workflow user-task` and `workflow parallel-split`, all of which `mxcli syntax workflow` lists as sub-topics; `--json` was the only route that reached them.", "cause": "The CLI built its path with `strings.Join(args, \".\")` and never split an argument, so a topic handed over as ONE string — a quoted copy-paste, a tool wrapper, `sh -c` — became the path `workflow user-task targeting`, which matches nothing. The REPL's `help` had resolved multi-word topics since it was written (`resolveHelpPath`, greedy hyphen-joining): one question, two answers, and the CLI held the weaker copy. The #955 segment-match fallback could not save it either — it passed the DOTTED path to `BySegmentMatch`, and no segment contains a '.', so that fallback was silently dead for every multi-word query.", "file": "cmd/mxcli/syntax/topic.go (new: Lookup, topicWords, resolvePath), cmd/mxcli/help.go, mdl/executor/cmd_misc.go (resolveHelpPath deleted), mdl/grammar/domains/MDLSettings.g4 (helpStatement, helpTopicWord), mdl/visitor/visitor_query.go (ExitHelpStatement); tests cmd/mxcli/cmd_syntax_test.go, cmd/mxcli/syntax/topic_test.go, mdl/executor/cmd_misc_test.go, mdl/visitor/visitor_help_topic_test.go; example mdl-examples/bug-tests/syntax-1025-topic-drilldown.mdl", "insight": "**The spaces in the reported error message were the whole diagnosis, and reading them as a paraphrase cost an hour.** The command prints the path it built, and the CLI joins on '.', so `Unknown topic: workflow user-task targeting` cannot come from the command as documented — it can only come from the topic arriving as a single argument. Every line of the report follows from that and nothing else does: `syntax workflow` works (one word), `--json` works (the flag is not part of the topic), the three multi-word forms fail. Take a quoted error message literally, character for character, before assuming the reporter retyped it. **The reported version is downloadable and settles it in one run**: `mxcli setup mxcli`'s own URL shape (`releases/download//mxcli-linux-amd64`, NOT the goreleaser `_Linux_x86_64.tar.gz` that 404s) fetched v0.20.0, where the unquoted command works and the quoted one reproduces the message verbatim — so 'fixed since' and 'never broken' were both wrong. **The guard that matters is not the three cases from the report** but `TestEveryRegisteredPathIsReachableBySpelling`: every registered path, tried dotted, as separate arguments, and as one string. The registry prints dotted paths and then tells the reader to drill down with words, so a spelling that does not resolve is the command contradicting its own output; a per-case test would have passed the day someone added a topic with a new shape. Control: stub the whitespace split in `topicWords` and it fails with the reported path, spaces and all. **The grammar half has a trap the CLI half does not, and only the EXISTING suite caught it.** `helpStatement: IDENTIFIER (identifierOrKeyword)*` is the grammar's catch-all — a statement that is just an identifier and some words — so whatever it can swallow, it swallows from the statement that should have had it. Widening it to `(DOT? helpTopicWord)*` to take `help workflow.user-task` made `Sec.ApiUser` a complete statement of its own, and `create module role Sec.ApiUser` then parsed, WITH NO PARSE ERROR, as CREATE MODULE (named \"role\") followed by a help topic — two statements, wrong types, six unrelated security tests red. `(helpTopicWord (DOT? helpTopicWord)*)?` — a topic word before any dot — leaves `.ApiUser` unconsumable and restores the old disambiguation. Bisect a grammar regression by SHAPE, not by reading the ATN: adding the unused rule alone was clean, the hyphen alone was clean, the leading optional DOT was the whole of it, and three regenerations said so in about a minute. **When widening a permissive rule, the test to add is not for the new spelling but for what the rule must still NOT swallow** (TestHelpRuleDoesNotSwallowATrailingQualifiedName).", "refs": ["mendixlabs/mxcli#1025", "#955"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-18-mxcli-report-scores-project-against-rules-team-disabled-lint.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-18-mxcli-report-scores-project-against-rules-team-disabled-lint.json new file mode 100644 index 0000000000..7d31dc477d --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-18-mxcli-report-scores-project-against-rules-team-disabled-lint.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-18", "symptom": "`mxcli report` scores a project against rules the team disabled in `lint-config.yaml`. `mxcli lint` honours the config, the report's SCORE does not move, so the score cannot be calibrated at all. Reported at 66/100 against a 99/100 blank-app baseline, where 61 of 86 findings were two deliberately-accepted rules", "cause": "`cmd_report.go` never called `linter.FindConfigFile`/`LoadConfig` — it went straight from `linter.New` to `BuildReport`. Separately it carried its own INLINE copy of the built-in rule list, one rule behind `builtinLintRules()` (missing MDL-FLOW01), so the two commands scored one project against two rule sets. One root cause: report re-implemented lint's setup instead of sharing it", "file": "`cmd/mxcli/cmd_report.go`, `cmd/mxcli/cmd_lint.go`, new `cmd/mxcli/lint_setup.go` (`projectLintRules`, `applyLintConfig`), `mdl/linter/linter.go` (`RuleEnabled`)", "insight": "Same class as #904 in the opposite direction: there a silently reduced rule set made the score falsely HIGH, here an unread config makes it falsely LOW — and both are invisible because a score carries no provenance. **A value test cannot guard the inline copy**: both commands build rules inside a cobra RunE, so nothing a unit test can call notices a second list being re-added. The guard is therefore structural — grep `cmd_report.go` for `lint.AddRule(rules.New` — with a POSITIVE CONTROL first (assert `builtinLintRules` still constructs rules) so it cannot pass vacuously, the same shape as `scripts/check-tunnel-deps.sh`. Take the LintContext out of `applyLintConfig`'s signature: `NewLintContext(nil, nil)` panics, and a nil-guard added only to make a test compile is how a helper acquires behaviour nothing needs", "refs": ["#525", "#904"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-20-mxcli-marketplace-install-p-app-mpr-project-named-relative.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-20-mxcli-marketplace-install-p-app-mpr-project-named-relative.json new file mode 100644 index 0000000000..d81e5644e9 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-20-mxcli-marketplace-install-p-app-mpr-project-named-relative.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli/marketplace", "date": "2026-09-20", "symptom": "`mxcli marketplace install ... -p app.mpr` (project named by a RELATIVE path) fails with `install the package's bundled files: package entry \"manifest.json\" would write outside the project` \u2014 after the module has already been transplanted into the model. `SHOW MODULES` lists the module, `mx check` is clean, but no bundled file (themesource/, widgets/) landed and the command exited 1. Absolute `-p` paths work.", "cause": "`InstallPackageFiles` builds `dst := filepath.Join(projectDir, clean)` and then checks `strings.HasPrefix(filepath.Clean(dst), filepath.Clean(projectDir)+os.PathSeparator)`. With projectDir == \".\" (from `filepath.Dir(\"app.mpr\")`), Join drops the dot, so dst is `manifest.json` and the prefix is `./` \u2014 every legitimate entry fails the zip-slip guard. The guard was checking the joined path (the shape CodeQL recognises) but never anchored the project directory first.", "file": "`cmd/mxcli/marketplace/update.go` (`InstallPackageFiles`: `filepath.Abs(projectDir)` before the loop), test `cmd/mxcli/marketplace/install_relative_dir_test.go`", "insight": "A containment guard has two inputs and both must be canonical \u2014 the entry AND the root. The traversal tests only ever passed an absolute t.TempDir(), so the root was canonical by accident and the relative case had no coverage; the first real CLI invocation with `-p app.mpr` hit it. Worse, the transplant runs BEFORE the file step, so the failure lands on a half-installed module: the model has it, the disk does not, and the exit code says failure. Order the steps so the cheap, reversible file copy can be validated before the model write, or at least say in the error that the model was already changed. Prove-by-revert done: the new test fails on the unpatched function with the exact reported message.", "refs": []} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-arch002-arch003-never-report-anything-mxcli-lint-runs-them.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-arch002-arch003-never-report-anything-mxcli-lint-runs-them.json new file mode 100644 index 0000000000..276011f168 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-arch002-arch003-never-report-anything-mxcli-lint-runs-them.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-22", "symptom": "ARCH002 and ARCH003 never report anything. `mxcli lint` runs them, prints no error and lists them under --list-rules, yet a project whose entities have no business key and no data-change microflow comes back clean.", "cause": "Both rules guard with `if entity.entity_type != \"PERSISTENT\": continue`. The catalog column stores PERSISTENT, but LintContext.Entities normalizes it (CASE WHEN 'PERSISTENT' THEN 'Persistent') before Starlark sees it, so the guard is true for every entity and the body never runs. The write-lint-rules skill documented a third spelling again, \"persistent\", in both its field table and its worked example.", "file": ".claude/lint-rules/entity_business_key.star, .claude/lint-rules/data_change_microflows.star, mdl/linter/context.go", "insight": "A Starlark rule that compares a field to a value that never occurs fails open: no error, no output, and --list-rules still shows it, so it looks like a project with nothing to report. The literals a rule may compare against are worth asserting in a test that loads the shipped rule file itself; measured on a generated app, fixing one spelling took ARCH003 from 0 findings to 5.", "refs": ""} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-make-check-mdl-error-1-ci-right-after-618-empty-script-guard.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-make-check-mdl-error-1-ci-right-after-618-empty-script-guard.json new file mode 100644 index 0000000000..17f5223e33 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-make-check-mdl-error-1-ci-right-after-618-empty-script-guard.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli/check", "date": "2026-09-22", "symptom": "`make check-mdl` Error 1 in CI right after the #618 empty-script guard landed. The failing fixture, mdl-examples/doctype-tests/15-fragment-examples.test.mdl, got: 'produced no statements, but it is not empty. The parser could not begin reading it. First line that did not parse: create module FragTest;' \u2014 for a 416-line file that parses perfectly well (18 statements) when copied to a plain .mdl name. The filename was the whole difference.", "cause": "Two defects stacked. (1) MINE: cmd_check.go renders a .test.mdl through testrunner.CheckSource \u2014 a test block is a microflow BODY, so check parses the RENDERING, not the file \u2014 but the #618 guard was given `string(content)`, the file as read. A file with no @test block renders to nothing, so the guard saw zero statements against non-empty ORIGINAL text and quoted a source line the parser had never been handed. (2) PRE-EXISTING: that fixture declares no @test at all. It is a syntax demo misnamed .test.mdl (its sibling 15b-fragment-slots-examples.mdl is plain), so CheckSource rendered it to nothing, check printed 'Check passed!' on zero statements, and `make check-mdl` had been reporting PASS over 416 lines nothing ever read \u2014 concealing a real MDL-PAGE20 violation (page param $Customer, url with no {Customer} segment).", "file": "`cmd/mxcli/cmd_check.go` (guard takes `source`, the parsed text, not `content`; empty rendering from non-empty content gets its own branch), `cmd/mxcli/empty_script.go` (`noTestsDeclaredError`), fixture renamed to `mdl-examples/doctype-tests/15-fragment-examples.mdl` with the url fixed, tests `cmd/mxcli/empty_script_test.go`", "insight": "A guard that reports on text OTHER than what the parser consumed will eventually quote a line the parser never saw, and it reads as authoritative precisely because it names a line. Wherever a command transforms its input before parsing \u2014 a renderer, a preprocessor, a macro pass \u2014 every diagnostic downstream must be fed the transformed text, or it describes a file that was never compiled. The wider lesson is about what the guard FOUND: #618 is 'a silent no-op is the worst outcome', and the repo's own check-mdl suite contained an instance \u2014 a file passing because nothing read it. A suite that reports PASS per file cannot distinguish 'checked and clean' from 'not checked'; the tell was available all along in the statement count, which was 0. When a new guard fails CI, check whether it found a second instance of its own bug before assuming it is a false positive: here it was BOTH, and only fixing the diagnosis would have left the misnamed fixture green and unread. Prove-by-revert done end-to-end: restoring `string(content)` reproduces the CI message verbatim on the same bytes under the .test.mdl name.", "refs": ["ako/mxcli#618", "ako/mxcli#619", "ako/mxcli#1103"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-mxcli-diag-loop-report-json-reports-failed-0-across-real-442.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-mxcli-diag-loop-report-json-reports-failed-0-across-real-442.json new file mode 100644 index 0000000000..500fb3362a --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-mxcli-diag-loop-report-json-reports-failed-0-across-real-442.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli/diag", "date": "2026-09-22", "symptom": "`mxcli diag loop-report --json` reports `\"failed\": 0` across a real 442-invocation log while runs were genuinely exiting non-zero. Read next to `\"unclosed\": 10` in the same object it says 'nothing failed', which is the opposite of the truth. The text report never printed the field at all, so the misreading was reachable only through --json, where no surrounding prose corrects it.", "cause": "The field counted a different population than its name claimed. `rep.Failed++` fires only for an invocation that CLOSED (wrote session_end) whose summary carried errors_count > 0, and errors_count is diaglog's STATEMENT-level counter \u2014 so the only path reaching it is a run that kept going after a failed statement, i.e. `exec --continue-on-error`. A run that actually fails exits through os.Exit, which skips the deferred Close() and PersistentPostRun, writes no session_end, and lands in `unclosed`. The two populations are disjoint by construction and `failed` is near-always zero. Nothing was broken; the name was.", "file": "`cmd/mxcli/diag_loop_report.go` (`loopReport.Failed` -> `StatementErrors`, json tag `failed` -> `runs_with_statement_errors`; renderLoopReport prints it only when non-zero and takes io.Writer so the text output is testable), tests `cmd/mxcli/diag_loop_report_test.go` (TestStatementErrorsIsDisjointFromUnclosed, TestJSONKeyNamesWhatItMeasures, TestStatementErrorLineIsPrintedOnlyWhenNonZero)", "insight": "A metric that is always zero fails silently in the one direction nobody checks: it is indistinguishable from good news, so it is never investigated. This one survived review and a whole feature PR because every local test run genuinely had no --continue-on-error invocations, so 0 was CORRECT in the test set and wrong in the field \u2014 only a 442-invocation log from a real project surfaced it. Two rules fell out. (1) Assert the JSON key literally, not the Go field: the key is the interface the wrong conclusion was drawn through, and a Go-side rename leaves the tag behind. (2) Never print a counter's zero beside a related non-zero counter; suppress it, or the pair reads as a comparison. The larger fix \u2014 making `failed` mean failed \u2014 needs an exit-code path through ~250 os.Exit sites in cmd/mxcli, since Go has no atexit; `unclosed` already carries that signal and the report explains it. Prove-by-revert done both ways: restoring the tag fails the key test with `\"failed\":2` in the payload, relaxing the guard to >= 0 prints `Finished with failed statements: 0` directly under `Did not close: 2`, which is the reported symptom exactly.", "refs": ["ako/mxcli#617", "ako/mxcli#620"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-windows-process-regression-fails-intermittently.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-windows-process-regression-fails-intermittently.json new file mode 100644 index 0000000000..30f8800e8e --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-22-windows-process-regression-fails-intermittently.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli/docker", "date": "2026-09-22", "symptom": "`windows-process-regression` fails intermittently on TestKillProcessGroup_ReapsGrandchildAndUnblocksWait: `cmd.Wait() did not return after killProcessGroup`, ~20.5s (the select deadline), and the GitHub runner then logs `Terminate orphan process: pid (NNNN) (PING)`. killProcessGroup reports no error. Reruns pass, so it reads as 'Windows CI is flaky' and gets attributed to whatever PR happened to be red.", "cause": "The test's readiness marker named the wrong process. The `spawn` helper mode started `cmd /c ping -n 60 127.0.0.1` and wrote `grandchild-started` immediately after `gc.Start()` returned — but Start() only guarantees `cmd.exe` was CREATED; `ping.exe`, which is what ends up holding the inherited stdout pipe, does not exist yet. The test raced ahead to killProcessGroup, `taskkill /F /T` enumerated a tree `ping.exe` had not joined, returned 0, and ping survived holding the write end, so cmd.Wait() never saw EOF.", "file": "`cmd/mxcli/docker/procgroup_windows_test.go` (helper gains a `grandchild` mode that announces ITSELF, with its pid, over the inherited pipe; the test then asserts processAlive on that pid before killing), parser split to `cmd/mxcli/docker/procgroup_marker_test.go` + TestGrandchildPID", "insight": "A readiness marker is only worth what it proves about the process the test is ABOUT. Emitting it from the parent after Start() proves the parent reached a line of code, which is the one thing never in doubt. Emit it from the process under test, over the channel under test — the unix half already did exactly this (`sh -c 'sleep 60 & echo $!; wait'` plus kill(gpid,0)) and the Windows half had silently diverged, so the fix was porting the sibling's handshake rather than inventing one. Two second-order traps: a deadline bump cannot fix this (the grandchild is never killed, so no amount of waiting helps) and would have buried it; and the marker parser must reject a line not yet terminated by \\n, since a truncated pid parses as a plausible different pid. The same-commit control that settled blame: sha 0710968d ran the identical workflow twice, `push` (35715042749) green and `pull_request` (35715076433) red — when a job is suspected flaky, look for two runs of one commit before reading the diff.", "refs": ["ako/mxcli#594", "ako/mxcli#597", "ako/mxcli#601"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-mxcli-diag-loop-report-showed-all-5-test-runs-did-not-close.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-mxcli-diag-loop-report-showed-all-5-test-runs-did-not-close.json new file mode 100644 index 0000000000..0dcc78eeda --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-mxcli-diag-loop-report-showed-all-5-test-runs-did-not-close.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli/diag", "date": "2026-09-23", "symptom": "`mxcli diag loop-report` showed all 5 `test` runs as 'did not close' although every test passed, and inflated the `-c` (111) and `exec` (180) counts in the same log. Reported as 'the command apparently skips the summary record on success too' \u2014 which is not what happens: `test` returns normally, PersistentPostRun fires, and the session_end IS written.", "cause": "mxcli runs mxcli. Measured from a real `mxcli test` with MXCLI_LOG_DIR pointed at a scratch dir: one parent session_start (pid 1071) followed by THREE child session_starts \u2014 `-c DESCRIBE SETTINGS`, `-c SHOW MODULES`, and an `exec` of the generated runner \u2014 before a single test executes. `new`, `eval`, `tui` and the LSP self-spawn the same way (six os.Executable() sites). buildInvocations segmented on 'next session_end OR next session_start, whichever comes first', so a child's start closed the parent's invocation and the parent's own end landed on whatever was open by then. session_end carried no pid, so pairing by process was impossible.", "file": "`mdl/diaglog/diaglog.go` (pid on session_end; parentPIDEnv marker set once in Init and inherited by every child), `cmd/mxcli/diag_loop_report.go` (buildInvocations pairs by pid with the positional rule as fallback; spawned runs excluded from the table and wall time, counted on their own line), tests `cmd/mxcli/diag_loop_report_test.go`", "insight": "The segmentation rule documented itself as exact 'for sequential invocations, which is what an agent loop produces' \u2014 and the thing that breaks that assumption is the tool itself, not concurrency by the user. When a tool can invoke itself, EVERY per-process measurement over it needs a parent link, not just a pid: a pid alone fixes the pairing but still counts three phantom agent calls per test run. The marker belongs on the ENVIRONMENT, not at each spawn site: exec.Command inherits the parent's environment (explicitly via os.Environ(), implicitly when Cmd.Env is nil), so one os.Setenv in Init covers all six self-spawn sites and any added later \u2014 six edits that would each have to be remembered become zero. Second-order trap: spawned runs must be excluded from WALL TIME too, not just the count, because a child's seconds are already inside its parent's; the test asserts 10s for a parent with three 1s children, and the reverted code says 3. Prove-by-revert done on the measured record shape: the positional rule gives Invocations=4 (want 1), Unclosed=1 for a parent whose tests all passed, Wall=3 (want 10).", "refs": ["ako/mxcli#617", "ako/mxcli#629"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-mxcli-syntax-page-datasource-documented-datasource-microflow.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-mxcli-syntax-page-datasource-documented-datasource-microflow.json new file mode 100644 index 0000000000..403052635a --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-mxcli-syntax-page-datasource-documented-datasource-microflow.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli/syntax", "date": "2026-09-23", "symptom": "`mxcli syntax page datasource` documented `DataSource: MICROFLOW Module.MF($P)`. That form is a parse error: `dataview dv (datasource: microflow M.DS_X($State))` gives 'line 2:15 no viable alternative at input datasource'. Only the NAMED form `M.DS_X(State: $State)` parses. Hit in a real build, diagnosed from the error rather than the doc.", "cause": "The syntax entry was written from the intended shape rather than from something that had been run through the parser. Nothing checks it: the Syntax and Example fields are free text.", "file": "`cmd/mxcli/syntax/features_page.go` (page.datasource entry now shows `MICROFLOW Module.MF(Param: $P)` and states that the positional form is a parse error)", "insight": "CLAUDE.md deliberately points at `mxcli syntax` instead of restating syntax, so that it cannot go stale \u2014 which makes a wrong entry there worse than a wrong entry in prose, because it is the thing consulted INSTEAD of checking. The cost lands in the agent loop: read it, write it, fail to parse, diagnose, retry. Measured before reaching for the systemic guard: 42 of 164 syntax examples fail `mxcli check` today, but the large majority are fragments by design (a microflow body like `IF \u2026`, a widget snippet like `DATAGRID \u2026`, an OQL fragment) and are legitimately not standalone top-level MDL \u2014 so a blanket 'every example must parse' test would be mostly noise, and making it useful needs a way to mark which examples are standalone. Measuring that first is what stopped a plausible-sounding guard from being built wrong.", "refs": ["ako/mxcli#630"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-mxcli-test-mxcli-check-mxcli-exec-no-arguments-wrote-0.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-mxcli-test-mxcli-check-mxcli-exec-no-arguments-wrote-0.json new file mode 100644 index 0000000000..5bd0bbdf55 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-mxcli-test-mxcli-check-mxcli-exec-no-arguments-wrote-0.json @@ -0,0 +1 @@ +{"area":"cmd/mxcli","date":"2026-09-23","symptom":"`mxcli test`, `mxcli check`, `mxcli exec` with no arguments wrote 0 session_start records (`mxcli check /nonexistent.mdl` wrote 1), so a run that failed cobra's Args validation was invisible to `diag loop-report`.","cause":"#617 put diaglog.Init in the root's PersistentPreRun and described that as 'before argument validation'. Cobra's execute() runs ValidateArgs, and answers --help/--version, BEFORE any hook, so arity failures returned before the session opened. The same move also meant the root's -c and REPL sessions were recorded as mode \"mxcli\" instead of \"batch\"/\"repl\", because PreRun reached the singleton first.","file":"cmd/mxcli/session_start.go","insight":"A cobra hook is not 'before anything': execute() order is ParseFlags → help/version → ValidateArgs → PersistentPreRun, so anything that must see every invocation belongs in main() before Execute, keyed on rootCmd.Find(os.Args[1:]), which needs no parsed command. Moving the open that early moves the close problem with it: --help/--version return nil without running PersistentPostRun, so a close left there turns every help lookup into an 'unclosed' (failed) run. That was the false failure #617 had already paid for once. Close in main() after a nil Execute instead. When a singleton is opened earlier, whichever caller reaches it first picks the mode, so resolve the mode the later callers would have passed (batch/repl) at the early call site. The regression test runs the real main() in a re-executed test binary with MXCLI_LOG_DIR. It is the only layer that sees os.Exit paths, and both controls fail at the right place: stubbing startSession gives 0/0/0 records with the /nonexistent.mdl control still passing, and moving the close back to PostRun gives 0 session_end records for --help/--version.","refs":["ako/mxcli#633","ako/mxcli#617"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-windows-mxcli-test-tests-p-myapp-mpr-local-fails-local.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-windows-mxcli-test-tests-p-myapp-mpr-local-fails-local.json new file mode 100644 index 0000000000..562f852abf --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-23-windows-mxcli-test-tests-p-myapp-mpr-local-fails-local.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli/test", "date": "2026-09-23", "symptom": "Windows: `mxcli test tests/ -p MyApp.mpr --local` fails with `local runtime: starting mxbuild serve: mxbuild --serve did not become ready` after caching a Linux ELF in %USERPROFILE%\\.mxcli\\mxbuild, and there is \"no flag, environment variable, or mechanism to redirect mxcli to the Windows mxbuild.exe already present in the Studio Pro installation\"", "cause": "Two layers. The platform part (downloading/exec'ing the Linux binary, serving from the cache instead of the resolved binary) was already fixed by #916 and #1122, both after the reporter's v0.21.0. What remained on main: `test` never registered `--mxbuild-path` — `run` gained it in #1125, but `test --local` boots through the same `ResolveMxBuildForLocal` and prints the same 'pass --mxbuild-path' guidance while answering `unknown flag`. `RunOptions` had no field and `localAppOptions` never set `LocalAppOptions.MxBuildPath`, though StartLocalApp honoured it. No env override existed anywhere", "file": "`cmd/mxcli/main.go` + `cmd_test_run.go` (flag), `testrunner/runner.go` + `localapp_options.go` (plumbing), `docker/mxbuild_platform.go` (`MxBuildPathEnv`, read in `resolveMxBuildForLocalOn` after the flag)", "insight": "**The guard for #1125 asserted its invariant against one command** — `TestErrorGuidanceNamesAFlagThatExists` checked only `runCmd`, while the guidance it polices is emitted by a resolver two commands share. When a test pins 'the advertised flag exists', enumerate the callers of the code that ADVERTISES it, not the command the report named; it now iterates `run` and `test`. **Before fixing a platform report, date it against the fixes**: the reporter's first two suggestions ('download platform-correct binary', 'auto-discover Studio Pro') were already on main, and re-implementing them would have been churn — only the override was missing. Put the env var in the resolver, not the CLI, so `run --local` and `test --local` both get it from one line; a flag-level env read would have been one more per-command copy to drift. Control: the env test runs as goos=windows with an unmatched version, so without the override it fails fast with the 'Linux binary cannot run natively on windows' refusal instead of hitting the CDN; removing only the `MxBuildPath:` line in localAppOptions fails the plumbing test for both runners. **Unverified**: no Windows host; code-level with OS-injected tests", "refs": ["mendixlabs/mxcli#1086", "mendixlabs/mxcli#1125", "mendixlabs/mxcli#916", "mendixlabs/mxcli#1122"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-25-shipped-starlark-rules-conv009-conv010-qual001-qual003.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-25-shipped-starlark-rules-conv009-conv010-qual001-qual003.json new file mode 100644 index 0000000000..7665eb02a5 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-25-shipped-starlark-rules-conv009-conv010-qual001-qual003.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-25", "symptom": "Shipped Starlark rules CONV009, CONV010, QUAL001, QUAL003, QUAL004 and CUSTOM002 report nanoflows and rules as microflows: \"Microflow 'NF_Foo' has 30 activities\", and documentType \"Microflow\" in the JSON and report output.", "cause": "microflows() yields all three flow flavours (one catalog table, MicroflowType MICROFLOW/NANOFLOW/RULE), and each rule hardcoded document_type=\"Microflow\" and a \"Microflow '...'\" message. The Go rules had already been fixed with Microflow.DocumentNoun() after MPR002 called a rule a microflow, but that method was never exposed to Starlark, so every Starlark rule had to re-derive it and none did.", "file": "mdl/linter/starlark.go, .claude/lint-rules/{conv009_max_microflow_objects,conv010_act_microflow_content,example_microflow,long_microflows,mccabe_complexity,orphaned_elements}.star, mdl/linter/starlark_flow_noun_test.go", "insight": "When a Go-side fix lives in a method, check whether the Starlark projection can reach it; a fix Starlark cannot call is a fix for half the rules. Exposed as document_noun / document_noun_title on the microflow struct, and the test runs every shipped rule over a fixture with one flow per flavour, requiring a finding on each (else vacuous) plus a guard that any new `for x in microflows()` rule joins the list. Skip the wrong turn of excluding nanoflows/rules from QUAL004: rule calls from decisions ARE emitted as 'call' refs (builder_references.go collectRuleCalls), so only the label was wrong.", "refs": "mendixlabs/mxcli#1178"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-25-starlark-rule-api-docs-name-microflow-type-values-linter.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-25-starlark-rule-api-docs-name-microflow-type-values-linter.json new file mode 100644 index 0000000000..7290780c7e --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-25-starlark-rule-api-docs-name-microflow-type-values-linter.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-25", "symptom": "Starlark rule API docs name microflow_type values the linter never returns: the write-lint-rules skill said \"microflow\"/\"nanoflow\", example_microflow.star (copied into every project by mxcli init) said \"Microflow\"/\"Nanoflow\", only mccabe_complexity.star had \"MICROFLOW\"/\"NANOFLOW\", and none listed \"RULE\". The skill's entity table also omitted has_created_date/has_changed_date/has_owner/has_changed_by.", "cause": "LintContext.Microflows() passes the catalog's MicroflowType through raw (MICROFLOW/NANOFLOW/RULE) while Entities() CASE-normalizes EntityType to TitleCase, so the two adjacent iterators have opposite conventions and each doc author guessed. Nothing tied the documented literals to what the API emits, so two passes over the same skill file (59db6e7b, #1165) fixed instances and left this one.", "file": ".claude/skills/mendix/write-lint-rules/SKILL.md, .claude/lint-rules/example_microflow.star, .claude/lint-rules/mccabe_complexity.star, mdl/linter/starlark_documented_values_test.go", "insight": "Fix the class, not the row: observe the emitted field names and enum values by running a Starlark rule (dir(e), e.entity_type) over a fixture holding every stored kind, then assert the skill tables, the shipped rules' `# .field - ...` headers, and every `.entity_type/.microflow_type ==` comparison in shipped rules against that observed set. Equality, not subset, for the docs -- an undocumented value (RULE) is a flavour a rule silently mistreats. Normalizing MicroflowType instead would have broken every user rule already comparing \"MICROFLOW\" correctly.", "refs": "mendixlabs/mxcli#1178, mendixlabs/mxcli#1164"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-26-json-output-did-not-parse-mxcli-refs-callers-callees-impact.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-26-json-output-did-not-parse-mxcli-refs-callers-callees-impact.json new file mode 100644 index 0000000000..96afaf88a4 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-26-json-output-did-not-parse-mxcli-refs-callers-callees-impact.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-26", "symptom": "`--json` output did not parse. `mxcli refs|callers|callees|impact|structure|show|search … --json` and `mxcli -c \"select … from CATALOG.x\" --json` printed \"Connected to:\", catalog load/build progress (`✓ Table: N`, 40+ lines on a cold cache), a \"References to X\" header and a \"Found N\" count ahead of the array; an empty answer was the sentence \"(no references found)\" / \"(no results)\" instead of []; `context` ignored --json and printed markdown; `check --format json` wrote its document to STDERR (one per phase), leaving only executor chatter on stdout; `show widgets|data transformers|import/export mappings|catalog status` had no JSON path for empty or at all; `diff --json` silently printed a text diff. Found by an agent-orientation audit on a 140MB app (Evora Factory Management)", "cause": "The executor has ONE writer, ctx.Output, and it carries both the payload and every status line. The #904 fix (progressSink) only works when the payload is written by the COMMAND (lint/report format their own result to os.Stdout, so the executor's writer can be pointed at stderr); for every query subcommand the payload is written by the executor itself, through the same ctx.Output as the chatter, so no choice of writer at the cmd layer can separate them. The split has to be inside the executor, per line, by what the line is", "file": "`mdl/executor/exec_context.go` (`Diagnostics` field + `diagnostics()`, identical to ako/mxcli#716, plus `progress()`: Output in text mode, diagnostics() in JSON mode), `mdl/executor/format.go` (`writeEmptyResult`), `mdl/executor/cmd_catalog.go` (connect/load/build/warn/count lines, `writeCatalogStatusJSON`), `mdl/executor/cmd_search.go`, `cmd_context.go`, `cmd_widgets.go`, `executor_connect.go`, `reference_target.go`; `cmd/mxcli/cmd_check.go` (one structured document on stdout via `finish`), `cmd/mxcli/cmd_query.go` (search: `--json` canonical, `--format json` deprecated alias sets the executor format), `cmd/mxcli/output_stream.go` (`refuseJSONFlag`, used by diff/diff-local); tests `cmd/mxcli/json_output_purity_test.go`, `mdl/executor/progress_stream_test.go`", "insight": "**Classify each line by what it IS, not where it is printed**: an answer goes to Output, commentary about the run goes to `ctx.progress()`. Text mode is byte-for-byte unchanged because progress() IS Output there — assert that too (`TestTextModeKeepsProgressOnStdout`), or the next fix 'cleans' interactive output by sending it all to stderr. **The empty case is where JSON contracts break**: every handler that early-returns a sentence on zero rows must go through writeEmptyResult (or the existing `&& ctx.Format != FormatJSON` idiom) — the sweep found 8 such sites and most list handlers already had the idiom, so grep for the ones that do not. **--json is a ROOT persistent flag**, so every subcommand accepts it; a command with no JSON output must refuse it (stderr, exit 2), since ignoring it hands text to a parser. **Test in a child process** (reuse TestRunMainHelper): stdout and stderr are then genuinely separate, error paths ending in os.Exit are observable, and no cobra flag state leaks between table rows. Proof: forcing progress() to return Output fails 18/18 --json rows, the search alias and the error test; reverting cmd_check.go fails all five check rows. Left alone: the 10,000-line output guard counts pretty-printed JSON lines, so `show widgets --json` on Evora emits the complete document and then exits 1 'output line limit exceeded'", "refs": ["#904", "ako/mxcli#716"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-26-write-lint-rules-skill-which-starlark-rules-written-from.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-26-write-lint-rules-skill-which-starlark-rules-written-from.json new file mode 100644 index 0000000000..61e431cb12 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-26-write-lint-rules-skill-which-starlark-rules-written-from.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-26", "symptom": "The write-lint-rules skill, which Starlark rules are written from, omitted seven query functions (java_actions, documents, documentable_elements, navigation_targets, queues, module_cycles, database_connections) plus get_option() and struct(), and fields on scheduled_event (repeat, on_overlap, time_zone) and project_security (anonymous_user_role). Four of the missing functions are used by shipped rules.", "cause": "Each builtin and field was added in code with its own PR and a docs-site paragraph at most; nothing compared the skill to buildPredeclared() or to the struct keys, so every API addition drifted from the skill by default. #1178's test pinned only the entity and microflow tables.", "file": ".claude/skills/mendix/write-lint-rules/SKILL.md, mdl/linter/starlark_skill_coverage_test.go", "insight": "Cover the whole surface at once rather than the table that was reported: an internal test ranges over (&StarlarkRule{}).buildPredeclared() for builtins and go/ast-parses every starlarkstruct.FromStringDict(starlark.String(name), dict) call in the package for struct fields (literal dicts and locally-built ones like ppDict), then requires each struct's skill table to match exactly. Structs documented elsewhere are named in explicit maps (helper params, inline struct{...} in a function row, shared permission table) so an exemption is a visible decision. The quick one-off Python diff over-reported (nested password_policy, permissions_for's entity_name) -- the test encodes those layouts instead of guessing.", "refs": "mendixlabs/mxcli#1178"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-28-mxcli-check-x-test-mdl-reported-mdl-depr001-warning-create.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-28-mxcli-check-x-test-mdl-reported-mdl-depr001-warning-create.json new file mode 100644 index 0000000000..6664f75b67 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-28-mxcli-check-x-test-mdl-reported-mdl-depr001-warning-create.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-09-28", "symptom": "`mxcli check x.test.mdl` reported an MDL-DEPR001 warning (`create or replace` is deprecated) on the doc comment of every @test block, for a spelling the author never wrote; the conformance gate (#756) counted 70-odd of them across mdl-examples' test files", "cause": "testrunner.CheckSource renders each test block as a microflow so the top-level grammar can check it, and its wrapper text was `CREATE OR REPLACE MICROFLOW …`, placed on the doc-comment line. The deprecation registry landed after the wrapper, and nothing checked mxcli's own generated MDL against it", "file": "`cmd/mxcli/testrunner/check_source.go` (wrapper spelling), test `TestCheckSourceWrapperIsCanonical` in `check_source_test.go`", "insight": "**Generated MDL is subject to the deprecation registry too**, and a warning on generated text lands on the author's line numbers, where it looks like their mistake. The other generators in testrunner (generator.go, endpoint.go) still write `create or replace` into scripts that are executed, not checked, so they warn nowhere a user sees — left alone. Proof: the new test fails with MDL-DEPR001 on line 1 of the rendering before the one-word fix.", "refs": ["ako/mxcli#756"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-30-mxcli-fmt-upgrade-tests-csv-import-test-mdl-failed-no-viable.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-30-mxcli-fmt-upgrade-tests-csv-import-test-mdl-failed-no-viable.json new file mode 100644 index 0000000000..ecc8b7fa99 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-09-30-mxcli-fmt-upgrade-tests-csv-import-test-mdl-failed-no-viable.json @@ -0,0 +1 @@ +{"date": "2026-09-30", "area": "cmd/mxcli", "symptom": "`mxcli fmt --upgrade tests/csv-import.test.mdl` failed with `no viable alternative at input '/**\\n * @test …'` on a file `mxcli check` passes, so the migration tool could not reach a project's test suite; separately `mxcli check x.test.md` reported a syntax error on every valid markdown test block whose doc comment spans lines.", "cause": "fmt parsed the file as top-level MDL, but a test file is `/** @test */` doc comments and `/` separators around microflow bodies; only check rendered it (testrunner.CheckSource). The markdown parser counted a fence block's lines from the ```mdl-test line instead of the line after it, so BodyLine was one early and the rendering overwrote the doc comment's last line with the body.", "fix": "testrunner.UpgradeSource upgrades CheckSource's line-preserving rendering and maps it back line for line: a line the rendering kept verbatim takes the upgraded text, every other line stays the author's, and a rewrite reaching a wrapper line or changing the line count is refused. fmt routes test files there (plain fmt on one is refused with a pointer to --upgrade). parseMarkdownTests passes blockStart+1 as the chunk line.", "insight": "A test file takes no language header: neither check's rendering nor either runner generator reads a `mdl 1;` line, and one in front of the first @test makes that test's doc comment non-leading, so the runner silently drops it. --header therefore adds no header to a test file and says so, rather than applying mdl 1 rewrites the runner would execute as mdl 0. The markdown BodyLine bug was found only because the upgrade's corpus test re-parsed the rendering of mdl-examples/doctype-tests/microflow-spec.test.md; it also shrank the conformance allowlist.", "issue": "ako/mxcli#837", "file": "cmd/mxcli/testrunner/upgrade_source.go, cmd/mxcli/testrunner/parser.go (parseMarkdownTests), cmd/mxcli/cmd_fmt.go", "test": "cmd/mxcli/testrunner/upgrade_source_test.go, TestCheckSourceMarkdownBodyLine, cmd/mxcli/cmd_fmt_upgrade_test.go TestFmtUpgrade_TestFile"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-fmt-upgrade-over-stub-then-real-set-still-split-pair-stub-s.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-fmt-upgrade-over-stub-then-real-set-still-split-pair-stub-s.json new file mode 100644 index 0000000000..15481e782d --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-fmt-upgrade-over-stub-then-real-set-still-split-pair-stub-s.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "fmt --upgrade over a stub-then-real set still split the pair when the stub's file carried a written `mdl 0;`: the real file took `mdl 1;`. With the real file already `mdl 1;`, fmt printed 'no language header added' about it while the pair stayed split.", "cause": "canTakeHeader returned true for ANY written header (langver.ScanWrittenHeader's bool), reading a pinned `mdl 0;` as 'already has the header'; and the group decline was applied to a file that already carries the header, which fmt never removes.", "fix": "canTakeHeader: a written header can take it only when it is langver.Latest. decideSetHeaders: a file already under the header is not declined; fmt says the pair stays under different headers and to drop the stub or take the header off.", "insight": "ScanWrittenHeader's bool means 'a header is written', not 'the header is mdl 1'; a written mdl 0 pin is the strongest 'cannot take it' there is. Test the group decision with every header state of each file, not only headerless ones.", "issue": "ako/mxcli#905", "file": "cmd/mxcli/cmd_fmt.go", "test": "cmd/mxcli/script_set_test.go TestFmtUpgrade_ScriptSetPinnedMdl0StubHoldsTheRealFileBack, TestFmtUpgrade_ScriptSetAlreadySplitPairIsReportedAsSplit"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-lsp-s-create-microflow-create-nanoflow-create-enumeration.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-lsp-s-create-microflow-create-nanoflow-create-enumeration.json new file mode 100644 index 0000000000..768d667ccc --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-lsp-s-create-microflow-create-nanoflow-create-enumeration.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "The LSP's CREATE MICROFLOW / CREATE NANOFLOW / CREATE ENUMERATION snippet completions inserted MDL that does not parse under any language version (`missing '(' at 'BEGIN'`, a quoted enumeration value name), the CONSTANT snippet the deprecated clause form (MDL-DEPR136), and PAGE/SNIPPET a statement mdl 1 refuses for its missing `;`; completion also offered alias-only keywords (SHOW_PAGE, DELETE_BEHAVIOR, DEFINE) and the `show entities` listings after SHOW.", "cause": "The snippets and keyword lists were hand-written once and never parsed; the grammar moved on (R2 property lists, R8 words, `list` for `show`) and nothing tied the completion text to it. The keyword list is generated from the lexer alone, which cannot tell an alias token from a canonical one: that is said by the `/* @alias MDL-DEPRnnn */` markers in the parser grammar.", "fix": "Snippets rewritten to canonical mdl 1. cmd/gen-completions reads the parser grammar too and drops a token every parser-rule use of which carries an @alias marker (the `keyword` rule, which lists tokens usable as names, does not count). `list` gets the listings; `show` offers page/message/home page.", "insight": "A completion text is MDL that ships in the binary, so it is held to what docs are held to: TestCompletionSnippetsAreMdl1 parses every snippet, expanded with its defaults, under `mdl 1;` and requires no deprecation. Deciding alias-only from the markers is the registry's own data; a token-swap rewrite word (`snippet`, `column`, `comment`) is NOT alias-only \u2014 those words stay canonical elsewhere \u2014 and neither is SHOW (show page, show message).", "issue": "ako/mxcli#714 (decision 5)", "file": "cmd/mxcli/lsp_completion.go, cmd/gen-completions/aliases.go", "test": "cmd/mxcli/lsp_mdl1_test.go TestCompletionSnippetsAreMdl1, TestCompletionOffersNoDeprecatedSpelling, TestCompletionListAndShowContinueIntoMdl1; cmd/gen-completions/aliases_test.go"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-check-exec-fmt-diff-fail-script-saved-windows.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-check-exec-fmt-diff-fail-script-saved-windows.json new file mode 100644 index 0000000000..a57ecefc26 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-check-exec-fmt-diff-fail-script-saved-windows.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "`mxcli check`/`exec`/`fmt`/`diff` fail on a script saved by Windows PowerShell 5.1: a UTF-8 BOM gives `line 1:0 token recognition error at: '\\ufeff'` (an invisible character), UTF-16LE gives a token error on almost every character; the same through stdin and in .test.mdl files", "cause": "Every script reader passed the raw file bytes to the lexer, which reads UTF-8 without a BOM; there was no shared reader (fmt, diff, the multi-file check pass, the test runner and EXECUTE SCRIPT each called os.ReadFile on their own)", "fix": "New mdl/srctext.Decode (strip a leading UTF-8 BOM, decode UTF-16LE/BE by BOM); readMDLSource calls it and fmt, diff and parseScriptSet now read through readMDLSource; testrunner.ParseTestFile and EXECUTE SCRIPT call it directly", "insight": "A BOM also hides a `mdl 1;` header from langver.ScanWrittenHeader, so stripping it in the parser alone would have left the language version wrong: decode where the bytes are read, before anything inspects the text. Enumerate the readers (grep os.ReadFile / io.ReadAll(os.Stdin)), not just the one the report names", "issue": "mendixlabs/mxcli#1253", "file": "mdl/srctext/srctext.go; cmd/mxcli/mdlsource.go", "test": "mdl/srctext/srctext_test.go; cmd/mxcli/mdlsource_encoding_test.go"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-lsp-stdio-exits-rc-2-panic-only-file-uris-supported.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-lsp-stdio-exits-rc-2-panic-only-file-uris-supported.json new file mode 100644 index 0000000000..d3c20cde19 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-lsp-stdio-exits-rc-2-panic-only-file-uris-supported.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "`mxcli lsp --stdio` exits rc=2 with `panic: only file URIs are supported, got mendix-mdl` on textDocument/didOpen of a `mendix-mdl:` virtual document (the VS Code extension's describe previews); VS Code restarts it, it crashes again, and after 5 crashes it stops restarting the server", "cause": "checkableDocument (diagnostics) and CodeAction called go.lsp.dev/uri URI.Filename(), which panics on any scheme but file, to decide whether the document is a .test.mdl", "fix": "documentPath(uri) returns Filename() only for file: URIs and the URI's path component otherwise; runSemanticCheck (which shells out `mxcli check `) skips non-file documents; virtual documents are still diagnosed in memory", "insight": "Third-party helpers that panic on unexpected input are a crash path in a long-running server; every URI an LSP client sends is untrusted shape. A test with a non-file URI plus a file-URI control (same diagnostics) proves the virtual case is handled, not skipped", "issue": "mendixlabs/mxcli#1245", "file": "cmd/mxcli/lsp_helpers.go (documentPath, isFileURI); cmd/mxcli/lsp_diagnostics.go; cmd/mxcli/lsp_language.go", "test": "cmd/mxcli/lsp_virtual_uri_test.go"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-p-app-mpr-c-opens-interactive-repl-generator-spawning.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-p-app-mpr-c-opens-interactive-repl-generator-spawning.json new file mode 100644 index 0000000000..0bf910f4fe --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-p-app-mpr-c-opens-interactive-repl-generator-spawning.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "`mxcli -p App.mpr -c \"\"` opens the interactive REPL (a generator spawning mxcli with an open stdin hangs at `mdl>`); `-c \"describe entity System.User; describe entity String; describe entity System.FileDocument\"` stops at statement 2 with `module name is required: objects must be created within a module` and the third statement is silently never run", "cause": "Root Run tested `commands != \"\"` to choose -c over the REPL, so an empty flag value was indistinguishable from no flag; the -c path used ExecuteProgram, which returns the first error without its position, and describe entity/association reached findModule(\"\"), whose message is written for the create path", "fix": "`cmd.Flags().Changed(\"command\")` selects the one-liner path; runCommandLine (cmd/mxcli/oneliner.go) refuses empty input, reports `statement N of M` and how many later statements were not run (via new Executor.ExecuteProgramReportingStop), and takes --continue-on-error like exec; execDescribe names an unqualified entity/association name", "insight": "A flag's zero value is not its absence: use Changed() whenever an empty value must mean something other than not given. Decided semantics: -c is fail-fast like exec (a later statement may depend on an earlier one), but a stop is never silent", "issue": "mendixlabs/mxcli#1218", "file": "cmd/mxcli/oneliner.go; cmd/mxcli/main.go; mdl/executor/executor.go; mdl/executor/executor_query.go", "test": "cmd/mxcli/oneliner_test.go; mdl/executor/describe_unqualified_name_test.go"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-version-fails-unknown-command-version-mxcli-while.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-version-fails-unknown-command-version-mxcli-while.json new file mode 100644 index 0000000000..171525c379 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-mxcli-version-fails-unknown-command-version-mxcli-while.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "`mxcli version` fails with `unknown command \"version\" for \"mxcli\"` while `mxcli --version` works", "cause": "cobra's `Version:` field registers only the --version flag; no subcommand existed, though shouldSuppressWarning already listed a `version` argument", "fix": "Add a `version` subcommand (cmd/mxcli/cmd_version.go) printing exactly cobra's --version text (name, version, build time)", "insight": "Code that special-cases an argument (warning suppression) is evidence of an intended command; a test pinning the subcommand output to the flag output keeps the two from drifting", "issue": "ako/mxcli#534", "file": "cmd/mxcli/cmd_version.go", "test": "cmd/mxcli/cmd_version_test.go"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-stub-then-real-script-set-two-files-each-create-modify.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-stub-then-real-script-set-two-files-each-create-modify.json new file mode 100644 index 0000000000..148700a126 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-01-stub-then-real-script-set-two-files-each-create-modify.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "cmd/mxcli", "symptom": "A stub-then-real script set (two files each with `create or modify microflow X`) upgraded with `fmt --upgrade -p` one file at a time ended up split: the stub's file declined the header (MDL-V1-REBUILD) and the real flow's file took it. Run in order, the mdl 0 stub rebuilt the stored real flow and the mdl 1 real statement was refused, every run; mx check 0 errors, the app running the placeholder.", "cause": "Each command judged one file against the model as stored; check/fmt took exactly one file, so nothing could see a flow declared twice across the set, and a per-file header decision is wrong for a pair whose files must agree.", "fix": "check and fmt accept several files as one script set (cmd/mxcli/script_set.go): findFlowRedeclarations finds a flow declared by two create-or-modify statements; check warns MDL-STUB01 naming both; fmt --upgrade groups files sharing such a flow and declines the header for all of them when any one cannot take it (canTakeHeader = the per-file verdict). One file alone is unchanged.", "insight": "A per-file verdict is only right when the files are independent; the run is the unit when two files write the same document. The symptom hides because each half is individually correct (decline is right for the stub, header is right for the real file) - only the combination is wrong, so test the set, with the single-file decision as the control.", "issue": "ako/mxcli#905", "file": "cmd/mxcli/script_set.go", "test": "cmd/mxcli/script_set_test.go TestFmtUpgrade_ScriptSetDecidesStubThenRealHeaderTogether, TestCheck_ScriptSetWarnsOnStubThenReal"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-check-p-prints-1-issues-0-errors-1-warnings-then-all.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-check-p-prints-1-issues-0-errors-1-warnings-then-all.json new file mode 100644 index 0000000000..ead7ddd72d --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-check-p-prints-1-issues-0-errors-1-warnings-then-all.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`check -p` prints `1 issues: 0 errors, 1 warnings` then `✓ All references valid` then `1 issues: 1 errors, 0 warnings` — reads as an error next to a passing run, and neither line is the total", "cause": "each tier of runCheckFile (semantic, project verdicts, legacy widgets) formatted its batch with a TextFormatter that printed its own summary line", "file": "`cmd/mxcli/cmd_check.go` (`runCheckFile`), `mdl/linter/output.go` (`TextFormatter.NoSummary`, `WriteSummary`)", "insight": "A multi-stage report needs one summary at exit over everything printed; the per-batch formatter's summary is only right for single-batch callers (lint, exec preflight)", "refs": []} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-exec-script-flow-change-refuses-cannot-spliced-under-mdl-1.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-exec-script-flow-change-refuses-cannot-spliced-under-mdl-1.json new file mode 100644 index 0000000000..2ddf0f7d2f --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-exec-script-flow-change-refuses-cannot-spliced-under-mdl-1.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`exec` of a script with a flow change it refuses (\"cannot be spliced\" under mdl 1) writes every statement before the refused one and none after it; `check -p` had predicted the refusal", "cause": "`execPreflight` ran the semantic pass, references and name clashes but not `CheckFlowVerdicts`, which `check -p` and `fmt` already run", "file": "`cmd/mxcli/exec_preflight.go` (`execPreflight`)", "insight": "Every verdict check can compute must also gate exec's pre-flight, or the two disagree and exec half-applies. --continue-on-error keeps running every other statement, so the verdict is printed there but does not refuse", "refs": []} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-check-tests-x-test-mdl-p-app-mpr-references-fails.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-check-tests-x-test-mdl-p-app-mpr-references-fails.json new file mode 100644 index 0000000000..6a30f942b9 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-check-tests-x-test-mdl-p-app-mpr-references-fails.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`mxcli check tests/X.test.mdl -p app.mpr --references` fails with `module not found: MxTest`; a body calling a microflow that does not exist is never reported", "cause": "`CheckSource` wraps each test block as `CREATE OR MODIFY MICROFLOW MxTest.…` but nothing in the rendering created MxTest (only a run does), and the CreateMicroflowStmt reference check returns on the missing module before `validateFlowBodyReferences`", "file": "`cmd/mxcli/testrunner/check_source.go` (`CheckSource`)", "insight": "A rendering checked against a project must declare everything the runner would create; an early return on the container hides every finding inside it, so a false positive here was also a false negative. `CREATE MODULE` on the first wrapper's line keeps line numbers", "refs": []} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-check-tests-x-test-mdl-vs-code-lsp-warns-mdl-v1-slash.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-check-tests-x-test-mdl-vs-code-lsp-warns-mdl-v1-slash.json new file mode 100644 index 0000000000..44edc743b1 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-check-tests-x-test-mdl-vs-code-lsp-warns-mdl-v1-slash.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`mxcli check tests/X.test.mdl` (and the VS Code LSP) warns MDL-V1-SLASH once per test, on the doc-comment line, although the skill says a test file takes no `mdl 1;` header", "cause": "`CheckSource` renders each block as a microflow and closed the wrapper with `END; /`; the rendering is parsed headerless, so the visitor recorded a V1 slash note on a `/` mxcli itself wrote. The author's `/` separators are blanked and never reach the parser", "file": "`cmd/mxcli/testrunner/check_source.go`", "insight": "Generated MDL must be canonical under every language version, or the diagnostics blame the author's file. `TestCheckSourceWrapperIsCanonical` asserted no Deprecations but not LanguageNotes; it now asserts both", "refs": []} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-check-x-test-mdl-p-app-mpr-reports-module-not-found.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-check-x-test-mdl-p-app-mpr-reports-module-not-found.json new file mode 100644 index 0000000000..a2a03010ef --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-check-x-test-mdl-p-app-mpr-reports-module-not-found.json @@ -0,0 +1 @@ +{"date": "2026-10-02", "area": "cmd/mxcli/check", "symptom": "`mxcli check x.test.mdl -p app.mpr` reports `module not found: MxTest` once per test and exits 1 on a file `mxcli test` runs green — a test file can never pass check, and a real missing reference in a body is hidden behind it", "cause": "CheckSource wraps each test body in a `MxTest.Check_*` microflow (the runner's module), and the reference pass resolved that module against the project; the runner creates MxTest as the first statement of every generated script and drops it afterwards, so the project never has it", "fix": "testrunner.WithRunnerModule appends `create module if not exists MxTest` to the program used for the reference and project-conflict pass of a test file (appended so `statement N` still numbers the tests; definitions are collected program-wide; `if not exists` keeps a user's own MxTest from reading as a conflict)", "insight": "Seed the module the way the runner does, rather than exempting the file: the control (a body with a real missing entity) must still fail, and before the fix it showed only the MxTest error — the false positive was also masking true ones", "issue": "ako/mxcli#677", "file": "cmd/mxcli/testrunner/check_source.go (WithRunnerModule); cmd/mxcli/cmd_check.go", "test": "cmd/mxcli/check_test_file_refs_test.go"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-exec-script-report-pages-prints-35-mdl-widget15-info.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-exec-script-report-pages-prints-35-mdl-widget15-info.json new file mode 100644 index 0000000000..453588822e --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-exec-script-report-pages-prints-35-mdl-widget15-info.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`mxcli exec` on a script with report pages prints ~35 MDL-WIDGET15 info notes (one per container with adjacent inline dynamictexts) on every run, burying the warnings and errors that matter", "cause": "execPreflight formatted every ValidateProgram violation with linter.TextFormatter, no severity filter — the same report `check` prints, on a command that is re-run", "file": "`cmd/mxcli/exec_preflight.go` (printPreflightViolations), `mdl/linter/output.go` (TextFormatter.OmittedInfos)", "insight": "Filter at the exec printer, not at the rule: `check` is where a script is reviewed and keeps every note, and lowering a rule's frequency would hide it there too. The formatter's summary line had to learn about the omitted notes, or it read `0 info` above the line saying N were hidden. exec has no structured diagnostics output, so JSON/SARIF (check --format) are untouched. `--verbose` on exec and diff restores the full report", "refs": []} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-test-local-fails-chain-project-s-after-startup.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-test-local-fails-chain-project-s-after-startup.json new file mode 100644 index 0000000000..c93adf450d --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-mxcli-test-local-fails-chain-project-s-after-startup.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`mxcli test --local` fails to chain the project's after-startup microflow, then cleanup restores `AfterStartupMicroflow: 'AfterStartupMicroflow: ''Mod.Flow'` and the project is left pointing at `MxTest.RegisterEndpoint`", "cause": "`parseSettingValue` scraped DESCRIBE SETTINGS text and split only on `=`; the describe rewrite changed the output to `Key: 'value'`, so the whole line was kept as the value. The #803 tests used the old `=` lines only and stayed green against output describe no longer emits", "file": "`cmd/mxcli/testrunner/runner.go` (`parseSettingValue`, `getAfterStartup`)", "insight": "A parser of another command's human-readable output breaks silently when that output changes; the table test must carry the line the producer emits *today*. Split on the first `:` or `=`, unescape `''`, and match the key as a line prefix. Longer term, read the setting through the backend instead of scraping describe", "refs": []} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-run-local-watch-mendix-11-13-page-added-while-loop-runs.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-run-local-watch-mendix-11-13-page-added-while-loop-runs.json new file mode 100644 index 0000000000..a4eaf9115b --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-run-local-watch-mendix-11-13-page-added-while-loop-runs.json @@ -0,0 +1 @@ +{"date": "2026-10-02", "area": "cmd/mxcli", "symptom": "`run --local --watch` on Mendix 11.13: a page added while the loop runs is never bundled — the apply reports success (often 'applied via reload'), but opening the page 404s on dist/pages/..js and the page stays blank; restarting the loop fixes it", "cause": "mxbuild's tools/node/rollup-plugin-mendix-pages.mjs (byte-identical in 11.13.0 and 11.14.0) globs web/pages only at bundler start: watchChange compares path.relative(cwd, id) against PAGES_FOLDER=\"./pages\" + \"/\", a './' prefix relative() never yields, so shouldRefreshPageFiles never flips. mxcli could not see it: ensureClientServed probes index.js and its static imports, and pages are dynamic imports", "fix": "missingPageChunks compares web/pages/**/*.js against web/dist/pages/**/*.js (shape-gated: needs web/pages and web/dist/index.js, so 11.14 prebuilt and classic are no-ops); under --watch watchAndApply restarts the WebClientWatcher (a fresh rollup run re-globs) and reports one line; ensureClientServed does a one-shot BuildWebClient for the same condition. RunLocal stops whichever watcher is current at exit", "insight": "A stand-alone rollup watch repro (pages/A.js, then add pages/B.js) isolates it in seconds: the 'change pages/B.js create' event fires but the next bundle still emits only A; a fresh watcher, or the plugin with PAGES_FOLDER=\"pages\", emits both. Restart rather than one-shot: the stale watcher would also never rebuild the new page when it is edited later. A structural change (navigation) clears web/dist and the old index.js fallback re-bundles everything, masking the bug; a page-only add applied via reload is the reproducing case. E2E 11.13: pre-change binary 404 on the new chunk; fixed binary 200 + page text, and a follow-up edit of the same page re-bundled incrementally with no restart (control)", "file": "cmd/mxcli/docker/webclient_pages.go (missingPageChunks, recoverMissingPages); cmd/mxcli/docker/runlocal.go (watchAndApply, ensureClientServed, RunLocal watcher defer)", "test": "cmd/mxcli/docker/webclient_pages_test.go"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-test-mdl-starting-mdl-1-loses-first-test-mxcli-test-list.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-test-mdl-starting-mdl-1-loses-first-test-mxcli-test-list.json new file mode 100644 index 0000000000..5e9f62cd9f --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-test-mdl-starting-mdl-1-loses-first-test-mxcli-test-list.json @@ -0,0 +1 @@ +{"date": "2026-10-02", "area": "cmd/mxcli", "symptom": "A `.test.mdl` starting with `mdl 1;` loses its first test: `mxcli test --list` finds 41 of 42 with no message; `mxcli check` on it reads the bodies as mdl 0 (MDL-V1-SLASH / MDL-V1-LIMIT1 warnings fire under a file that says mdl 1); the generated runner scripts end every flow with `/`, which mdl 1 refuses; and `fmt --upgrade --header` adds no header to a test file", "cause": "Nothing in the test format read the header. It was body text in the first `/`-chunk, so that chunk's `/** @test */` was no longer a LEADING doc comment (scanDocComments) and parseMDLTests skipped it silently; CheckSource rendered only the bodies (dropping the header line) and put `END; /` on separators; GenerateTestRunner/GenerateTestFlows always wrote `/` and `create or replace`", "fix": "takeLanguageHeader reads the header with langver.HeaderSpan (the grammar's own rule: first token after trivia) and blanks it in place so lines/columns hold; TestCase carries Version + HeaderLine (markdown: per mdl-test block); CheckSource renders the header on its line and closes wrappers with `END;` (no `/`, any version); the generators write the header, `create or modify` and no `/` under mdl 1; parseTestFiles refuses a suite mixing versions (one suite = one script = one header); UpgradeSource honours AddHeader and writes the header at file top / inside each mdl-test block; UpgradeSource maps the upgraded rendering back with a line diff (difflib opcodes, every changed region must be body lines) instead of an index walk, because the MDL-V1-ESCAPE rewrite of `\\n` writes a real line break and adds a line; writeBodyLines no longer indents a body line that starts inside a string literal", "insight": "A header is not just a flag to pass along — in a format that is NOT parsed as one script, every consumer that splits the text has to take it out first, or it becomes content in whichever chunk it lands in. The silent drop came from the same 'leading doc comment' rule as #927; a per-test Version also forced the question 'what is a suite's version', which only refusal answers safely. The runtime run was what found the last defect: unit tests and `mx check` were green while an upgraded test asserting length 3 saw 5 — the generators indented the continuation line of a multi-line string literal, so the indent became part of the value. Compare an mdl 0 file, its fmt --upgrade output, and a headerless control in the running app (both runners)", "issue": "ako/mxcli#847", "file": "cmd/mxcli/testrunner/parser.go (takeLanguageHeader, suiteLanguageVersion); check_source.go; generator.go; generator_endpoint.go; upgrade_source.go; mdl/langver/langver.go (HeaderSpan)", "test": "cmd/mxcli/testrunner/language_header_test.go; e2e: mxcli test --local on a fresh 11.13.0 app, endpoint and --legacy-runner"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-theme-create-from-design-base-console-writes-mxcli-name-scss.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-theme-create-from-design-base-console-writes-mxcli-name-scss.json new file mode 100644 index 0000000000..1edf5cae89 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-02-theme-create-from-design-base-console-writes-mxcli-name-scss.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-02", "symptom": "`theme create --from --base console` writes `_mxcli-.scss` with a stray `}}`/`}` where the @font-face section was (27 `{` vs 29 `}`); the theme does not compile. Same for --base signal and ledger", "cause": "`dropFontFaces` matched each `@each $weight { @font-face { … } }` block with `[^}]*\\}[^}]*\\}`; the `src: url(\"…-#{$weight}-…\")` interpolation's `}` ended the first span early, so the match stopped at the @font-face close and left the @each's `}`", "file": "`cmd/mxcli/theme/create_seeded.go` (`dropFontFaces`, now `dropFontFaceBlock`)", "insight": "Never match nested SCSS blocks with `[^}]*`; count braces (interpolations are balanced). The existing test used the real shape but asserted only absence of @font-face — any rewrite of a stylesheet should assert brace balance on every shipped asset. Not copying the fonts folder is intended when the design's font stacks name none of the vendored families", "refs": []} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-ako-mxcli-962-item-3-vs-code-two-calls-stored-void.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-ako-mxcli-962-item-3-vs-code-two-calls-stored-void.json new file mode 100644 index 0000000000..43711e34ca --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-ako-mxcli-962-item-3-vs-code-two-calls-stored-void.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "ako/mxcli#962 item 3: in VS Code, two calls to a stored void JavaScript action with the same output name (Studio Pro's `$ReturnValueName`/`$RefreshEntity` shape) are squiggled MDL063, while `mxcli check -p` passes them", "cause": "runSemanticValidation called executor.ValidateMicroflow/ValidateNanoflow, which pass a nil void-action resolver: without the project every call output counts as a declaration", "file": "`cmd/mxcli/lsp_diagnostics.go` (runSemanticValidation); `mdl/executor/validate_void_code_calls.go` (FlowRules, CodeActionCache)", "fix": "executor.NewFlowRules(prog, s.findMprPath(), s.codeActions): resolves actions through the script and the workspace project (opened lazily), treats an unresolvable action as possibly void (unknownIsVoid) for MDL063 but never for MDL093, and shares project answers across keystrokes for 30s since one action read costs ~300ms on PedApp", "insight": "An entry point that wraps a richer internal API with nil arguments (ValidateMicroflow = validateMicroflowWith(stmt, nil)) silently gives every caller the weakest behaviour; a fix landed in the richer API (#958) does not reach them. Grep the exported wrapper's callers when the internal one gains a parameter. Measure the cost before putting project I/O on a keystroke path", "test": "`cmd/mxcli/lsp_void_calls_test.go` (PedApp: void pair clean with and without project, Boolean pair control, MDL093 only with project); `mdl/executor/validate_void_code_calls_test.go` (TestCodeActionCache_SharesProjectAnswersAcrossRuns)"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-conv006-emits-one-finding-per-entity-x-role-x-create-delete.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-conv006-emits-one-finding-per-entity-x-role-x-create-delete.json new file mode 100644 index 0000000000..e163660894 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-conv006-emits-one-finding-per-entity-x-role-x-create-delete.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "CONV006 emits one finding per entity x role x CREATE/DELETE (111 on a mid-sized app), the same advice repeated per role, and the per-finding Security score is driven by role count rather than by entities", "cause": "The Starlark rule appended a violation inside the permissions_for() loop", "file": "`.claude/lint-rules/conv006_no_create_delete_rights.star` (synced to `cmd/mxcli/lint-rules/`)", "insight": "Group per entity and per right with de-duplicated sorted roles (a role can hold several access rules on one entity). Test both rule copies (.claude and the embedded one) like SEC008's test does", "refs": ["ako/mxcli#953"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-every-mxcli-test-run-prints-2x-mdl-depr001-2x-mdl-v1-slash.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-every-mxcli-test-run-prints-2x-mdl-depr001-2x-mdl-v1-slash.json new file mode 100644 index 0000000000..69036e5f45 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-every-mxcli-test-run-prints-2x-mdl-depr001-2x-mdl-v1-slash.json @@ -0,0 +1 @@ +{"date": "2026-10-03", "area": "cmd/mxcli/test", "symptom": "every `mxcli test` run prints 2x MDL-DEPR001 and 2x MDL-V1-SLASH about a script the user never wrote", "cause": "GenerateEndpointMDL emitted a headerless mdl 0 script with `create or replace` and `/` terminators; the test-flow generators had already moved to the version-aware writeScriptHeader/createFlow/writeFlowEnd", "fix": "GenerateEndpointMDL writes mdl 1 through the same helpers (header, create or modify, `;` only); endpoint script is independent of the suite's version", "insight": "A generated script is checked like a user's one; pin it with a test that parses it and asserts ValidateProgram returns nothing. Verified end to end with `mxcli test --local` on a fresh 11.13 app", "issue": "ako/mxcli#943", "file": "cmd/mxcli/testrunner/endpoint.go", "test": "cmd/mxcli/testrunner/endpoint_clean_test.go"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-check-p-prints-mdl067-bare-commit-now-events-create.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-check-p-prints-mdl067-bare-commit-now-events-create.json new file mode 100644 index 0000000000..32aa947675 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-check-p-prints-mdl067-bare-commit-now-events-create.json @@ -0,0 +1 @@ +{"date": "2026-10-03", "area": "cmd/mxcli/check", "symptom": "`mxcli check -p` prints MDL067 (bare commit now WITH events) for a create-or-modify flow already stored with events, which `exec -p` no longer prints", "cause": "cmd_check ran ValidateProgram without the DropSettledCommitNotes filter exec_preflight applies; the project was only connected later, for the reference tier", "fix": "check connects to the project before the semantic report when -p is given, applies DropSettledCommitNotes and StoredTaskClaimViolations, and reuses that connection for the reference tier", "insight": "Two gates over one rule set drift whenever a post-filter lives in only one of them; grep for every caller of ValidateProgram when adding a filter. Control: a flow stored without events still notes", "issue": "ako/mxcli#943", "file": "cmd/mxcli/cmd_check.go", "test": "cmd/mxcli/check_stored_semantics_test.go"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-docker-build-docker-run-reload-which-call-rewrote.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-docker-build-docker-run-reload-which-call-rewrote.json new file mode 100644 index 0000000000..0aac3a1c45 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-docker-build-docker-run-reload-which-call-rewrote.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli/docker", "date": "2026-10-03", "symptom": "`mxcli docker build` (and docker run / reload, which call it) rewrote an MPRv1 project's .mpr, rewrote every MPRv2 .mxunit (restored with new mtimes), and wrote theme-cache/, deployment/, javasource/ proxies, the .launch file, .classpath and .project into the project; the TUI checker and `mxcli eval`'s mx_check wrote theme-cache/web/ and deployment/sass/ on every run", "cause": "update-widgets ran on the project under a snapshot that restored only v2 storage, and mx check / MxBuild ran on the project itself; the TUI and eval runner ran a bare `mx check `", "file": "`cmd/mxcli/docker/build.go` (`buildOnCopy`), `cmd/mxcli/docker/check.go` (`MxCheckOnCopy`), `cmd/mxcli/tui/checker.go` (`runMxCheck`), `cmd/mxcli/evalrunner/checks.go` (`checkMxCheck`)", "insight": "Run update-widgets, mx check and MxBuild on one temporary copy (copyProjectToTemp) and write only the output directory. Measured on the 11.14 testapp: the PAD from the copy differs from the in-place build in the same 9 files as two in-place builds of identical copies differ (cache-bust stamps, operation ids, the native metro paths), so the output is equivalent; rebuild time unchanged (58s vs 59s), because a PAD build gains nothing from the project's deployment/. 11.14's blank app needs JDK 25, so the real-mx build test skips without it; run it with 11.13's mx.", "refs": ["ako/mxcli#961", "ako/mxcli#951", "mendixlabs/mxcli#808"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-docker-check-check-rewrote-mprv1-project-s-mpr.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-docker-check-check-rewrote-mprv1-project-s-mpr.json new file mode 100644 index 0000000000..74847333c5 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-docker-check-check-rewrote-mprv1-project-s-mpr.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli/docker", "date": "2026-10-03", "symptom": "`mxcli docker check` (a check) rewrote an MPRv1 project's .mpr permanently, and on v1 and v2 alike rewrote theme-cache/web/theme.compiled.css(.map) and created deployment/sass/main.scss \u2014 with or without --no-update-widgets", "cause": "update-widgets ran on the user's project, protected only by a snapshot/restore of the v2 storage (.mpr + mprcontents/), so v1 had no protection; and `mx check` itself compiles the theme into theme-cache/ and writes deployment/sass/, which nothing guarded", "file": "`cmd/mxcli/docker/check.go` (`Check`), `cmd/mxcli/docker/check_copy.go` (`copyProjectForCheck`)", "insight": "A snapshot of the files you expect a tool to touch protects only those files; measure with a whole-tree hash+mtime diff before/after, which is what showed that plain `mx check` writes too. The fix is to run both mx steps on a temporary copy (skipping deployment/, releases/, theme-cache/, .git, node_modules) and rewrite the copy's path back in mx output. Control: a CE0117 microflow is still reported on both formats with the tree unchanged. `docker build` keeps runUpdateWidgets because it is expected to write deployment/ \u2014 but it still rewrites a v1 .mpr", "refs": ["ako/mxcli#951", "ako/mxcli#568", "ako/mxcli#646"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-report-could-not-score-project-s-own-modules-lint-has.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-report-could-not-score-project-s-own-modules-lint-has.json new file mode 100644 index 0000000000..f4f7e49cb6 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-mxcli-report-could-not-score-project-s-own-modules-lint-has.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "`mxcli report` could not score a project's own modules: `lint` has --modules, `report` had only --exclude", "cause": "Feature gap; and the LintContext module filter alone would not make the score exact, because project-level findings (CONV008 role mappings, project security) carry no module and are reported regardless", "file": "`cmd/mxcli/cmd_report.go`, `mdl/linter/report.go` (`ScopeToModules`, Report.Modules)", "insight": "Filter the scored violations to those located in a selected module, and print the selection in every format so a module score is not mistaken for the project's", "refs": ["ako/mxcli#953"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-project-whose-claude-md-skills-lint-rules-were-written-newer.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-project-whose-claude-md-skills-lint-rules-were-written-newer.json new file mode 100644 index 0000000000..3550723575 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-project-whose-claude-md-skills-lint-rules-were-written-newer.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-03", "symptom": "A project whose CLAUDE.md, skills and lint rules were written by a newer mxcli is served by an older binary (v0.24.0 on PATH) with no warning: 'mdl 1;' is a parse error and shipped lint rules crash, all reading as project defects", "cause": "Nothing recorded which mxcli wrote the tooling; .claude/bootstrap-mxcli.sh linked whatever mxcli was on PATH; init --sync-skills refreshed only .ai-context/skills, never .claude/lint-rules or CLAUDE.md/AGENTS.md", "fix": "init and every sync write .ai-context/mxcli-tooling.json; root PersistentPreRun warns once on stderr when the binary is provably older (release by number, nightly by tag date, mixed by build date, dev never); sync refuses from an older binary; the bootstrap script carries a POSIX-sh copy of the ordering and neither links an older PATH binary nor keeps an older ./mxcli, downloading via a temp file + mv; sync also refreshes bundled lint rules by name and the CLAUDE.md/AGENTS.md section between mxcli:begin/end markers", "insight": "A binary cannot warn about a stamp it predates, so the guard for already-shipped binaries must live in the generated script, which the newer mxcli regenerates. The sh and Go comparisons share one test table so they cannot drift. curl -o ./mxcli on a symlinked ./mxcli would overwrite the PATH binary — always download to a temp name and rename", "issue": "ako/mxcli#952", "file": "cmd/mxcli/tooling_stamp.go; cmd/mxcli/init_tooling_sync.go; cmd/mxcli/init_hook.go (bootstrapScriptTemplate); cmd/mxcli/main.go; cmd/mxcli/init.go", "test": "cmd/mxcli/tooling_stamp_test.go; cmd/mxcli/init_hook_version_test.go; cmd/mxcli/init_tooling_sync_test.go"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-theme-create-acme-from-design-css-mxt-font-inter-system-ui.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-theme-create-acme-from-design-css-mxt-font-inter-system-ui.json new file mode 100644 index 0000000000..60481749af --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-03-theme-create-acme-from-design-css-mxt-font-inter-system-ui.json @@ -0,0 +1 @@ +{"date": "2026-10-03", "area": "cmd/mxcli/theme", "symptom": "`theme create acme --from design.css` with `--mxt-font: \"Inter\", system-ui, sans-serif` prints nothing about Inter; the theme ships no woff2 and no @font-face for it and renders in the fallback font wherever Inter is not installed", "cause": "planFonts only decided which VENDORED families to drop; a seeded family outside the vendored set was never looked at, so the silent outcome was the default", "fix": "unvendoredSeededFamilies takes the primary (first) family of each seeded font stack, skips generic families and var() and the families the base partial loads, and CreateResult.UnvendoredFonts carries them to cmd_theme.go, which prints a note per family naming mxcli-fonts/ and the partial", "insight": "Only the first family of a stack is the design's choice; flagging the fallbacks (Helvetica, Arial) would make the note noise. The controls are a vendored family (IBM Plex Mono) and a generic stack, which must stay silent", "issue": "ako/mxcli#944", "file": "cmd/mxcli/theme/create_seeded.go (unvendoredSeededFamilies, planFonts); cmd/mxcli/cmd_theme.go", "test": "cmd/mxcli/theme/create_seeded_test.go (TestCreate_NamesSeededFontsItDoesNotVendor)"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-ako-mxcli-970-item-2-theme-create-from-design-css-only-root.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-ako-mxcli-970-item-2-theme-create-from-design-css-only-root.json new file mode 100644 index 0000000000..3e583bc4a0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-ako-mxcli-970-item-2-theme-create-from-design-css-only-root.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-04", "symptom": "ako/mxcli#970 item 2: `theme create --from design.css` with only a :root block (no dark block) wrote the design's light --mxt-ground/--mxt-ink/--mxt-brand into the base theme's dark mixin, whose other surfaces stayed dark; the first injected token also sat on the `@mixin … {` line, unindented", "cause": "Tokens.forVariant returned the base declarations for EITHER variant, and seedTokens applied it to the alt-palette mixin unconditionally; applyTokens matched `(?m)^(\\s*)name`, and \\s* at ^ swallows the preceding newline (and blank line), so the replacement lost its line break and indent", "file": "`cmd/mxcli/theme/create.go` (seedTokens, Create/CreateResult.UnseededVariant); `cmd/mxcli/theme/tokens.go` (applyTokens, Tokens.declares); `cmd/mxcli/cmd_theme.go` (note)", "fix": "seed the alt mixin only when the design declared a block for that variant; otherwise leave it byte-identical to the base and report UnseededVariant, which the CLI prints as a note; match the indent with [ \\t]* instead of \\s*", "insight": "A base palette is the default variant's palette, not 'both': a token set that does not say which variant it describes must not seed the other one. And under (?m), ^\\s* is not 'leading indentation' — it crosses lines; use [ \\t]*. The control for 'mixin untouched' is the same scaffold with no design at all, compared byte for byte", "test": "`cmd/mxcli/theme/create_variant_test.go` (all three bases, base-only vs variant block, indentation); `cmd/mxcli/cmd_theme_test.go` (TestThemeCreate_NotesTheVariantABaseOnlyDesignDidNotSeed)"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-ako-mxcli-982-item-2-run-local-admin-port-8091-printed-query.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-ako-mxcli-982-item-2-run-local-admin-port-8091-printed-query.json new file mode 100644 index 0000000000..565e18e8d2 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-ako-mxcli-982-item-2-run-local-admin-port-8091-printed-query.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-04", "symptom": "ako/mxcli#982 item 2: `run --local --admin-port 8091` printed `Query data: mxcli oql -p app.mpr` and that command failed `cannot connect to Mendix admin API at localhost:8090`; `mxcli log list` failed the same way", "cause": "oql resolved the admin port as flag > ADMIN_PORT env > .docker/.env > 8090 and log used flag defaults 8090/mxcli-local-dev; neither read the .mxcli/run-local.json handshake the loop publishes with its port and password (only `constant set --apply` did)", "file": "`cmd/mxcli/devloop_admin.go` (devLoopAdminOptions); `cmd/mxcli/docker_oql.go`; `cmd/mxcli/cmd_log.go` (logAdminOptions, logConnectionHint); `cmd/mxcli/docker/runlocal.go` (hint)", "fix": "one helper takes port/password from a LIVE handshake for any flag not given (explicit host means 'not this loop'), forces direct loopback HTTP; oql and log both use it; the log hint prints the resolved port", "insight": "A hint printed by the process that knows the port must be runnable without that knowledge: either print the flags or make the consumer read what the producer published. Reading the handshake fixes every consumer at once; the test is an httptest admin API on a random port with a fake run-local.json, controlled by the no-handshake and stale-pid cases", "test": "`cmd/mxcli/devloop_admin_test.go`"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-run-local-watch-mendix-11-13-rollup-bundler-after-while.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-run-local-watch-mendix-11-13-rollup-bundler-after-while.json new file mode 100644 index 0000000000..0e57b532be --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-run-local-watch-mendix-11-13-rollup-bundler-after-while.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-04", "symptom": "`run --local --watch` (Mendix <= 11.13, rollup bundler): after a while every page change fails with `web client rebuild failed: web client watcher exited` (or `client bundle not served after apply: web client re-bundle: web client build timed out after 5m0s`) and nothing reaches the browser until `run --local` is restarted; after adding an entity the next changes fail with `ENOTDIR: not a directory, stat '.../web/pages/.js/package.json'`", "cause": "watchAndApply held a bare *WebClientWatcher: (1) once it exited nothing restarted it (only recoverMissingPages did), so WaitForRebuild failed every later change; (2) a failed incremental build (rollup's commonjs resolver hitting web/pages mid-rewrite by the serve build) left the watcher erroring and the change was dropped; (3) ensureClientServed's recovery ran a one-shot NODE_ENV=production BuildWebClient in the same web/ dir while the watcher was still running — two rollups on web/dist; (4) the 5m limit was hard-coded and a timeout printed nothing about why", "file": "cmd/mxcli/docker/webclient_supervisor.go, cmd/mxcli/docker/runlocal.go (watchAndApply, ensureClientServed), cmd/mxcli/docker/webclient.go (webClientTimeout, webClientBuildLogTail)", "fix": "bundlerSupervisor owns the bundler: EnsureAlive restarts an exited one (backoff 2s..60s after failed starts), AwaitRebuild retries a failed/aborted incremental rebuild once with a fresh bundler, Rebundle = stop+reap then start (never two). ensureClientServed takes the rebundle func; clientRebundler hands it the supervisor under --watch and the one-shot otherwise. --web-client-timeout / MXCLI_WEB_CLIENT_TIMEOUT; timeout errors append the last 30 lines of deployment/log/web-client-build.log. sessionNotice prints that a restart dropped sessions", "insight": "Killing the runner (`kill `) reproduces the dead-watcher state in seconds — no need to wait for it to die on its own. A fresh bundler is the universal recovery under --watch: its first build is a full bundle of the current source, so it covers missing pages, dangling chunks, a dist/ deleted by Gradle, and a transient incremental failure alike, without a second process on web/dist. Note: exec.Cmd.Wait called a second time concurrently with the reaper did block until exit on this Go version, so the old Stop was not the overlap — the one-shot in ensureClientServed was", "test": "cmd/mxcli/docker/webclient_supervisor_test.go (TestBundlerSupervisor_RestartsExitedBundler, _AwaitRebuildRecovers, _RebundleNeverOverlaps, TestEnsureClientServed_WatchModeRebundleIsExclusive, TestBuildWebClient_TimeoutShowsLogTail); live: 11.13 scratch app, 7 consecutive changes incl. a killed runner"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-run-local-watch-mxcli-exec-save-made-while-change-still.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-run-local-watch-mxcli-exec-save-made-while-change-still.json new file mode 100644 index 0000000000..9db761a12d --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-04-run-local-watch-mxcli-exec-save-made-while-change-still.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-04", "symptom": "`run --local --watch`: an `mxcli exec` (or save) made while a change is still building/applying is never built — no `Change detected` follows, the app keeps the previous model, and re-running the script writes nothing (byte-idempotent) so nothing re-triggers", "cause": "watchAndApply set `last = sourceMTime(...)` after every successful apply, under a comment claiming it kept mid-build edits; it did the opposite — the edit's mtime was folded into the baseline, so the next tick saw nothing newer", "file": "cmd/mxcli/docker/runlocal.go (watchAndApply)", "fix": "keep `last` at the settled mtime the build was taken from; the build writes nothing under the watched model/theme source (checked with find -newer during a live run), so this cannot self-trigger", "insight": "Found while reproducing #971 with a script that waited for the first output line of a change instead of its `applied` line — the next exec landed during a 2-minute restart-apply and vanished. Any test of a watch loop should include an edit made DURING a build, not only between builds", "test": "live only (11.13 scratch app, hsqldb): exec an entity add, exec a page change 15s into its build; fixed binary builds it as the next build, the binary with the refresh restored shows no further build after 45s"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-mxcli-playwright-check-login-run-page-check-screenshot-user.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-mxcli-playwright-check-login-run-page-check-screenshot-user.json new file mode 100644 index 0000000000..dd1944f14b --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-mxcli-playwright-check-login-run-page-check-screenshot-user.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-05", "symptom": "`mxcli playwright check` (and the login of `run --page-check`/`--screenshot-user`) failed with \"node not found; the page check needs the Node that Playwright uses\" on a machine with no system node whose newest cached mxbuild is 11.15, although mxbuild 11.15 ships tools/node//node", "cause": "resolveNodeForScript found mxbuild's bundled node through resolveNodeTooling, which also requires tools/node/rollup-runner.mjs; mxbuild 11.15 dropped the rollup runner (rspack only), so the lookup failed before it looked for the node binary", "file": "`cmd/mxcli/docker/screenshot_login.go` (resolveNodeForScript)", "fix": "look up the node binary directly with findNodeBinary(/tools/node) instead of going through the rollup-runner check", "insight": "a helper that validates a whole toolchain was reused to find one binary in it; when the platform dropped an unrelated piece of that toolchain the reuse broke a caller that never needed it. Found by a smoke test on a machine whose newest mxbuild was 11.15, not by unit tests, which put node on PATH", "test": "cmd/mxcli/docker/screenshot_login_node_test.go TestResolveNodeForScript_RspackOnlyMxBuild (PATH emptied, 11.15 tools/node layout; fails with the old lookup)"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-mxcli-run-local-page-check-screenshot-user-u-screenshot.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-mxcli-run-local-page-check-screenshot-user-u-screenshot.json new file mode 100644 index 0000000000..97c610c91e --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-mxcli-run-local-page-check-screenshot-user-u-screenshot.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-05", "symptom": "`mxcli run --local --page-check --screenshot-user U --screenshot-password P` reported every secured page as `title=\"Login\"` — the verdict described the sign-in form, not the page asked for; adding `--screenshot` made it sign in", "cause": "the boot step that signs in and saves the Playwright storage state was gated on `opts.Screenshot && opts.ScreenshotUser != \"\"`; `--page-check` (added later, ako/mxcli#614) reuses that storage but was never added to the gate, so it probed anonymously", "file": "`cmd/mxcli/docker/runlocal.go` (step 6b login gate, reportPageChecks)", "fix": "gate the sign-in on `(Screenshot || PageCheck) && ScreenshotUser != \"\"`; the page probe is now shared with `mxcli playwright check` (docker.ProbePages), which also treats a 401 or the sign-in form as a failure instead of a page", "insight": "A flag that consumes state produced by another flag's setup step silently degrades when used alone. The verdict looked healthy (title, text, console-errors=0) because the login page IS a healthy page; the tell is the title. `playwright check` now fails on `#usernameInput` or a 401 so the same mistake cannot pass", "test": "manual on a scratch 11.13.0 app (run --local --page-check --screenshot-user pc_user: `Logging in as \"pc_user\"` then the Items verdict with rows=2); unit coverage of the login-form failure in `cmd/mxcli/docker/pagecheck_check_test.go`"} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-mxcli-run-local-watch-model-change-made-while-app-still.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-mxcli-run-local-watch-model-change-made-while-app-still.json new file mode 100644 index 0000000000..27622e227e --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-mxcli-run-local-watch-model-change-made-while-app-still.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-05", "symptom": "`mxcli run --local --watch`: a model change made while the app is still booting is never built — the app keeps serving the model from before it, `--watch` logs no `Change detected`, and a waiter on the change times out. Re-running the exec does nothing (byte-idempotent: it writes nothing, so nothing re-triggers the watcher)", "cause": "watchAndApply took its baseline `last := sourceMTime(...)` after the boot finished, so an edit landing between the boot build's model read and the watch loop's start was already folded into the baseline", "file": "`cmd/mxcli/docker/runlocal.go` (`RunLocal` bootSource, `watchAndApply`)", "insight": "A watcher's baseline must be the source time the last build was MADE FROM, taken before that build — not 'now' when the watcher starts. Same class as the earlier fix that stopped moving the baseline to time.Now() after an apply. Measured with the run-lifecycle integration test: exec a page while the detached run's state says the boot build is in progress; with the old baseline `run wait` times out after 2m (no build #2), with the fix it reports `applied: build #2 via reload`. Exposed by making `run wait` race-free on source time: a generation-counting wait could not have told the change was lost.", "refs": ["cmd_run_lifecycle_integration_test.go"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-run-local-db-type-postgresql-hsqldb-works-but-neither-run.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-run-local-db-type-postgresql-hsqldb-works-but-neither-run.json new file mode 100644 index 0000000000..a082464cc3 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-05-run-local-db-type-postgresql-hsqldb-works-but-neither-run.json @@ -0,0 +1 @@ +{"area":"cmd/mxcli","date":"2026-10-05","symptom":"run --local --db-type postgresql|hsqldb works but is in neither the run-local skill's nor docs-site/src/tools/run-local.md's flag table; users without PostgreSQL hit 'no local PostgreSQL superuser available to create the role/database' instead of finding --db-type hsqldb","cause":"Flag shipped with help text and proposals only. Separately, the skill's --app-port and --db-host/--db-name/--db-user/--db-password rows had drifted below the OpenTelemetry section, detached from the table header, so they rendered as stray text — a grep for '--db-name' in SKILL.md matched and hid that the table did not contain it","file":"cmd/mxcli/cmd_run_dbflags_doc_test.go","insight":"Check flag-table coverage by parsing the table (rows under a '| Flag |' header), not by grepping the file: a stranded row or a prose mention passes a grep. The test reads the --db-* set from runCmd.Flags(), so the next database flag fails it without editing the test. Full-flag coverage was not asserted because several other run flags (--configuration, --constant, --hub-prefix…, --page-check) are also absent — a separate gap.","refs":["mendixlabs/mxcli#1292"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-06-mxcli-run-local-watch-mendix-10-24-11-6-dies-after-5-minutes.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-06-mxcli-run-local-watch-mendix-10-24-11-6-dies-after-5-minutes.json new file mode 100644 index 0000000000..df9ff6a842 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-06-mxcli-run-local-watch-mendix-10-24-11-6-dies-after-5-minutes.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-06", "symptom": "`mxcli run --local --watch` on Mendix 10.24 / 11.6 dies after 5 minutes with `starting web client bundler: web client watcher timed out after 5m0s`, while the bundler's own log says `Bundling finished in 8999 milliseconds` — the bundle was done in 9s", "cause": "`parseBundlerStatus` (`cmd/mxcli/docker/webclient_watch.go`) only understood the modern-web-bundler protocol (`{\"protocol\":\"mx-modern-web-bundler\",\"type\":\"status\",\"payload\":{\"kind\":…}}`). The rollup-runner.mjs shipped with 10.24 and 11.6 predates it and writes `{\"code\":\"START\"|\"SUCCESS\"|\"ERROR\",\"payload\":…}` (ERROR payload an object with `message`, or a bare string when the config fails to load). Every status line was dropped as plain logging, so the first-build wait never saw a success", "file": "cmd/mxcli/docker/webclient_watch.go", "insight": "--watch had never worked on these versions: the parser has accepted only the new protocol since it was written (#349). Nothing exercised the watcher below 11.12 until the run-lifecycle integration test reached the nightly matrix — and it reached it only because ResolveMxForVersion substitutes ANY cached mxbuild for the test's default 11.13.0, so each matrix leg ran it against its own version. The runner's protocol is part of the mxbuild version contract: read tools/node/rollup-runner.mjs of the oldest supported version before assuming a stdout shape. Control: the lifecycle test on 11.6.8 with the parser reverted fails with the nightly's exact timeout (MXCLI_WEB_CLIENT_TIMEOUT=60s makes it fail in a minute); with the fix it passes", "refs": ["nightly 2026-10-06 (ako/mxcli run 37437089065, mendixlabs/mxcli run 37437605368)"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-06-mxcli-syntax-rename-printed-unknown-topic-rename-mxcli.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-06-mxcli-syntax-rename-printed-unknown-topic-rename-mxcli.json new file mode 100644 index 0000000000..2253fdf332 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-06-mxcli-syntax-rename-printed-unknown-topic-rename-mxcli.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-06", "symptom": "`mxcli syntax rename` printed `Unknown topic: rename` and `mxcli syntax --json` had rename only for attributes, values and definitions, while `RENAME MICROFLOW TO ;` passed `mxcli check` and `mxcli help rename` documented it; an agent concluded MDL could not rename a microflow and copied, re-pointed and deleted it by hand", "cause": "The renameStatement rule (MDLParser.g4) and its executor (mdl/executor/cmd_rename.go) never got a SyntaxFeature in cmd/mxcli/syntax/; the only discovery path was the cobra `rename` subcommand's Long text, which `syntax` does not read. BySegmentMatch could not rescue it because no registered path has a `rename` segment", "file": "cmd/mxcli/syntax/features_misc.go", "insight": "Same class as the TABCONTAINER gap (widget_keywords_drift_test.go): an agent treats absence from `mxcli syntax` as absence from the language, so a statement the grammar accepts but the registry omits is effectively missing. Fixed with a `rename` topic plus a guard (rename_topic_test.go) that reads the renameTarget rule from the .g4 and requires every alternative in the topic, and see_also links from microflow/page/entity/move. The CLI subcommand and the MDL statement diverge (MDL also takes JAVA ACTION and WORKFLOW); the topic says so rather than implying parity. Control: with the topic reverted the built binary prints `Unknown topic: rename` verbatim and TestSyntaxRenameTopic fails; dropping the RENAME WORKFLOW line fails the grammar guard. A sweep for other top-level statements with no topic would be the cheap next step", "refs": ["mendixlabs/mxcli#1318"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-08-main-s-build-test-fails-intermittently-run-lifecycle-tests.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-08-main-s-build-test-fails-intermittently-run-lifecycle-tests.json new file mode 100644 index 0000000000..c0e589a292 --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-08-main-s-build-test-fails-intermittently-run-lifecycle-tests.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-08", "symptom": "Main's build-and-test fails intermittently in the run-lifecycle tests, several at once in one run and green on the next: TestRunStop_LeavesNoOrphanBehind (\"fake run has 0 session members, want the leader and its child\"), TestRunStop_ReapsTheLeftoversOfAKilledRun, TestSessionMembers_FindsOrphanedGrandchild (\"members … = [], want the leader and its sleep\"). Not reproducible locally (20/20 green). In real use the same path makes `run stop` / `run status` miss a live run's processes.", "cause": "docker.SessionMembers' pid-reuse guard rebuilt a process's start time as btime + starttime/USER_HZ and dropped it if that fell more than 1s before notBefore. btime in /proc/stat is whole seconds (up to 1s early) and starttime whole 10ms ticks (up to 10ms early), so the rebuild can be ~1.01s early: a process started a few ms after notBefore was dropped. frac(btime) is fixed for a machine's uptime, so a CI VM booted late in a second failed every such test in the run, and others never did; a process started a few ms later (the leader's sleep) survived.", "file": "`cmd/mxcli/docker/session_linux.go` (`startedBefore`, `startSlack` = 2s)", "insight": "A flake that fails a whole family of tests in one run and none in the next points at per-machine state, not timing within the test: here the fractional second of the VM's boot. Make the comparison a pure function and pin the worst case (boot at X.9999, start 9.9ms into a tick, 1ms after notBefore); the test then fails deterministically against the old slack. Rebuilding a wall-clock time from btime + ticks loses up to 1s + 1 tick; any slack must exceed both.", "refs": ["https://github.com/ako/mxcli/actions/runs/37733157296", "https://github.com/ako/mxcli/actions/runs/37688181946"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-08-mxcli-test-local-had-no-db-type-so-machine-without.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-08-mxcli-test-local-had-no-db-type-so-machine-without.json new file mode 100644 index 0000000000..f0d9d7252c --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-08-mxcli-test-local-had-no-db-type-so-machine-without.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-08", "symptom": "`mxcli test --local` had no --db-type, so on a machine without PostgreSQL it stopped with \"Error: ensuring database: no local PostgreSQL superuser available to create the role/database\" while `run --local --db-type hsqldb` served the same project. Passing the type through naively then failed with \"database not reachable at 127.0.0.1:5432\".", "cause": "The headless boot (docker.LocalAppOptions) never learned the file database: applyDefaults filled PostgreSQL's host/user/password into every config, StartLocalApp pinged that host, and testrunner hardcoded EnsureDB=true. run --local had the file-database branches in its own applyDatabaseDefaults/StartLocalRun, a second boot path that the test runner does not use. DBConfig.IsFileBased keys on the runtime spelling \"HSQLDB\", so the canonical \"hsqldb\" passed straight through also silently skipped every file-based branch.", "file": "`cmd/mxcli/docker/localapp.go` (`applyDefaults`, `prepareLocalAppDatabase`), `cmd/mxcli/testrunner/localapp_options.go` (`dbConfig`, `ResolveTestDBType`), `cmd/mxcli/cmd_test_run.go`", "insight": "Two boot paths (StartLocalRun for run --local, StartLocalApp for test --local) each own their database defaults, so a capability added to one is absent from the other until someone asks; grep both when adding a database option. Keep the _test scratch suffix for HSQLDB as well: the name selects the database files under deployment/data/database/hsqldb/, and the unsuffixed ones are the dev loop's (shared data + HSQLDB file lock). A test that asserts EnsureDB is refused must match the refusal's message: EnsureDatabase itself fails on a host without PostgreSQL, which passes an err != nil check for the wrong reason.", "refs": ["https://github.com/mendixlabs/mxcli/pull/1274", "https://github.com/mendixlabs/mxcli/issues/1292"]} diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-08-studio-pro-11-12-11-13-shows-conflicts-change-pane-does-not.json b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-08-studio-pro-11-12-11-13-shows-conflicts-change-pane-does-not.json new file mode 100644 index 0000000000..e4cff0edca --- /dev/null +++ b/.claude/skills/fix-issue/findings/cmd-mxcli/2026-10-08-studio-pro-11-12-11-13-shows-conflicts-change-pane-does-not.json @@ -0,0 +1 @@ +{"area": "cmd/mxcli", "date": "2026-10-08", "symptom": "Studio Pro (11.12/11.13) shows conflicts the change pane does not show, history entries with BranchName/ModelerVersion \"(unknown)\", and crashes on open (\"Unable to find 'system' property in 'system'\") after an agent committed and branched with plain git on a Team Server project", "cause": "Studio Pro keeps per-commit metadata in git notes under refs/notes/mx_metadata (written on every Studio Pro commit, pushed with it); git commits had none, so Studio Pro 11.13 back-fills a placeholder (HasModelerVersion:false) for each after every background fetch (~3 min), and that fetch force-replaces the local notes ref, discarding notes an agent wrote but did not push. Branches made with git checkout -b have no upstream (ako/mxcli#972).", "file": "cmd/mxcli/gitnote.go", "fix": "`mxcli git note` computes the note from the commit (the .mpr Unit index + mprcontents at commit and parent, canon.Equal to skip $ID-only churn, a deleted module listed once, Moved from a container change, the project root listed on a version change); the teamserver-git skill teaches server branches and pushing refs/notes/mx_metadata with the branch.", "insight": "The ground truth was in the user's own repository, not in any doc: `git log refs/notes/mx_metadata` separates Studio Pro's notes (libgit2 'git_note_create', or the git CLI in 11.13 within a second of a host-timezone commit) from agent ones, and the notes reflog shows the 3-minute fetch-and-backfill cycle. Comparing a generator against those notes commit by commit settled every rule; an ordinary commit must match exactly, and the only residual difference is upgrade commits, where Studio Pro also records names from its editing session (a rename before commit) that no diff can recover.", "refs": ["mendixlabs/mxcli#1337", "ako/mxcli#972"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend.jsonl b/.claude/skills/fix-issue/findings/mdl-backend.jsonl deleted file mode 100644 index fe0db9ae99..0000000000 --- a/.claude/skills/fix-issue/findings/mdl-backend.jsonl +++ /dev/null @@ -1,175 +0,0 @@ -{"area": "mdl/backend", "date": "2026-07-31", "symptom": "A microflow writes and `mxcli` reports success, but Studio Pro cannot open the project: `System.Collections.Generic.KeyNotFoundException: The given key '' was not present in the dictionary`. Triggered by a `break`/`continue` inside an if/case within a loop", "cause": "`microflowObjectToGen`'s `default:` branch returns nil for an unhandled object type, so the event object was dropped at serialization while the SequenceFlow pointing at it was written — a DestinationPointer to a GUID that exists nowhere. Exactly the shape already fixed once for `Microflows$ErrorEvent`", "file": "`mdl/backend/modelsdk/microflow_write.go` (`microflowObjectToGen`) — compare against `sdk/mpr/writer_microflow.go`, which handles the full set", "insight": "Add the missing `case *microflows.XxxEvent:` returning the gen element with ID, position and size. **Diagnose without Studio Pro**: dump the microflow (`mxcli bson dump --type microflow --object M.F`), collect every `$ID`, and check every key ending in `Pointer` resolves — a dangling pointer is this bug. When a check-time rule exists only as a stopgap for a write-path bug (MDL051 was), remove it with the real fix, and check whether it covered every affected keyword (MDL051 covered `break`, not `continue`, which is how #791 reached users). Repro `mdl-examples/bug-tests/791-loop-continue-dangling-flow.mdl`. Issue #791, ledger #52", "refs": ["#52", "#791"], "rules": ["MDL051"]} -{"area": "mdl/backend", "date": "2026-07-30", "symptom": "An **external (OData) entity** loses its remote settings on any read-modify-write — `describe external entity` says `is not an external entity (source: )`, and `alter entity … set allow_create_change_locally = true` reports success but the flag stays off. Works under `--engine legacy`", "cause": "`entityFromGen` recognised only `DomainModels$OqlViewEntitySource`, so the three `Rest$OData*` sources read back as no source at all: `Source` empty, every remote field zeroed. The write path was fine — it switches on `e.Source`, which the read never populated", "file": "`mdl/backend/modelsdk/domainmodel.go` (`entityFromGen`'s source switch, `odataKeyFromGen`)", "insight": "Mirror the legacy parser (`sdk/mpr/parser_domainmodel.go`) for all three flavours: RemoteEntitySource (capabilities + CreateChangeLocally + key), EntityTypeSource (type name + IsOpen + key), PrimitiveCollectionEntitySource (service only). `Updatable` has no gen accessor and the writer does not emit it — leaving it zero is symmetric. **Check the read side first when a write-path field \"does not stick\"**: a switch on a field the read never fills looks like a write bug. Repro `mdl-examples/bug-tests/782-external-entity-create-change-locally.mdl`. Issue #782", "refs": ["#782"]} -{"area": "mdl/backend", "date": "2026-07-30", "symptom": "A pluggable widget's datasource (or child widget, or client action) is **silently dropped at write time** — `exec` prints `Created page` but the widget lands with the piece missing, and only a `log` line mentions `not yet supported — rerun with MXCLI_ENGINE=legacy`", "cause": "The converter *does* return an error, but it travels through `widgetobj.ChildSerializer`, whose methods return BSON with no error channel (the `TODO(shared-types)` in `mdl/backend/widgetobj/builder.go`), so the caller logged it and returned nil", "file": "`mdl/backend/modelsdk/widget_pluggable_write.go` (`recordChildSerializeErr`, `takeChildSerializeErr`) + the drains in `page_write.go` / `snippet_write.go`", "insight": "Record the failure in a package-level accumulator and drain it at every page/snippet write entry point, so the statement fails instead of the write succeeding with data missing (ADR-0004: refuse, don't drop). The real fix is the deferred `ChildSerializer` interface change; until then, any **new** write entry point that builds pluggable widgets must drain too. Repro `mdl-examples/bug-tests/795b-flow-datasource-context-entity.mdl`"} -{"area": "mdl/backend", "date": "2026-07-07", "symptom": "MCP op reports `MCP error -32000: Request timed out` but the page/document/entity EXISTS in Studio Pro afterwards", "cause": "Studio Pro's ~30s server-side per-call limit fires while the op still applies — a client false failure, not a server rejection", "file": "`mdl/backend/mcp/timeout.go` (`isTimeoutErr`, `timeoutVerifyDelay`, `pedUpdateVerify`, `pedDocumentExists`)", "insight": "Verify-on-timeout, never blind-retry a non-idempotent op: idempotent root-replace (`pgWritePage`) retries once; creates confirm via `ped_find_document`; entity adds confirm via a shallow `/entities` read. Unverified → error with save-before-re-run guidance (a blind re-run from a fresh session duplicates elements)"} -{"area": "mdl/backend", "date": "2026-07-30", "symptom": "`ALTER PAGE INSERT`/`REPLACE` into a list bound `from association` **or** `datasource: microflow/nanoflow` produces a widget whose Attribute binds to the WRONG entity (the outer data view's) or nothing — `mxcli check` ✓ but `mx check` fails **CE1613** (\"attribute no longer exists\") or **CE0402** (\"No value specified\"); DESCRIBE masks it by printing only the short attribute name", "cause": "The ALTER mutator read the enclosing entity from `DataSource.EntityRef.Entity`, only set for a DIRECT ref (database). An `AssociationSource` stores its destination on the last `DomainModels$EntityRefStep` of an `IndirectEntityRef`; a `MicroflowSource`/`NanoflowSource` stores NO entity at all (its entity is the flow's RETURN type, in the flow document). Either way the list reported no entity, so the context stayed at the outer data view (or empty)", "file": "`mdl/backend/pagemutator/mutator.go` (`extractEntityFromDataSource`+`lastStepDestinationEntity` for association; `EnclosingDataSourceFlow`+`findNearestDataSourceDoc` for flows) + `mdl/executor/cmd_alter_page.go` (`resolveDataSourceFlowEntity`)", "insight": "Association: read the `IndirectEntityRef`'s last `EntityRefStep.DestinationEntity`. Microflow/nanoflow: the mutator returns the nearest-enclosing (or own, for INTO) flow QN — a nearer non-flow source shadows an outer flow — and the executor resolves its return entity via `getMicroflowReturnEntityName`/`getNanoflowReturnEntityName`. Guards `TestEnclosingEntity_AssociationSource`, `TestEnclosingDataSourceFlow`; repro `mdl-examples/bug-tests/55-alter-page-insert-assoc-binding.mdl`. FINDINGS #55", "refs": ["#55"], "ce": ["CE0402", "CE1613"]} -{"area": "mdl/backend", "date": "2026-07-29", "symptom": "Runtime `Failed to load model: ... Class 'Workflows$CallMicroflowTask' could not be found` — the WHOLE app won't boot, yet `mxcli check` ✓ and `mx check` → 0 errors", "cause": "Mendix 11.9 (WOR-2802) split MicroflowBasedActivity into CallMicroflowActivity + AIAgentTaskActivity, renaming the workflow call-microflow on-disk `$Type` from the pre-11.9 `CallMicroflowTask` to `CallMicroflowActivity`. Writing the old name to an 11.9+ project is fatal at boot only. Evidence: 11.6.3 modeler = only Task; 11.10 modeler = both (Task marked \"Removed ... WOR-2802\" + a conversion routine); 11.10+ runtime jars = only Activity", "file": "`mdl/backend/modelsdk/workflow_write.go` (`applyCallMicroflowStorageName`, `useCallMicroflowActivityName`) + legacy `sdk/mpr/writer_workflow.go` (`renameCallMicroflowTypeBSON`)", "insight": "Version-gate the emitted `$Type` at 11.9 (same boundary as the `HasOwner`→`HasOwnerAttr` gate): build with the legacy name, rewrite the tree to `CallMicroflowActivity` for `pv.IsAtLeast(11,9)`; register codec TypeDefaults + list-marker under BOTH names; read path already folds both into one semantic type. Repro `mdl-examples/bug-tests/263-workflow-callmicroflow-storage-name.mdl`. FINDINGS #39", "refs": ["#39"]} -{"area": "mdl/backend", "date": "2026-07-30", "symptom": "`describe microflow` prints `-- Empty action` for a `set task outcome` / `open user task` / `notify workflow` statement (default engine); a describe→drop→exec round-trip silently drops it. Legacy engine (`MXCLI_ENGINE=legacy`) describes it fine", "cause": "The modelsdk read path (`actionFromGen`) had no case for the workflow microflow actions, so they read back as nil → \"Empty action\". The write path + DESCRIBE formatter already handled them; only the modelsdk read case was missing", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`)", "insight": "Add cases for `genMf.SetTaskOutcomeAction` / `OpenUserTaskAction` / `NotifyWorkflowAction`, mirroring the legacy parsers. Test `TestActionFromGen_WorkflowActions`; repro `mdl-examples/bug-tests/54-describe-set-task-outcome.mdl`. FINDINGS #54", "refs": ["#54"]} -{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders a rule-based split as `if true then …` instead of the real `Module.Rule_X(args)` condition (body renders, only the condition text is wrong)", "cause": "`splitConditionFromGen` only handled `*ExpressionSplitCondition`; a `RuleSplitCondition` returned nil → renderer fell back to `true`. Even when handled, the rule name lives under the `RuleCall.Microflow` storage key, but gen decodes the property as `Rule` (and `ByNameRef` decode bypasses `fieldAliases`), so `RuleQualifiedName()` is empty", "file": "`mdl/backend/modelsdk/microflow.go` (`splitConditionFromGen`)", "insight": "Reconstruct `*microflows.RuleSplitCondition` from the gen `RuleCall`; read the rule name from `rc.Raw().Lookup(\"Microflow\")` when `RuleQualifiedName()` is empty (same raw-storage-key pattern the action readers use). The existing `formatSplitCondition` renders it"} -{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow`/`nanoflow` on the **modelsdk** engine omits the `grant execute on microflow … to ;` line that legacy emits", "cause": "`microflowFromGen`/`nanoflowFromGen` never read the allowed module roles, so `Microflow.AllowedModuleRoles` was empty and the describer skipped the grant block", "file": "`mdl/backend/modelsdk/microflow.go` (`microflowFromGen`/`nanoflowFromGen`)", "insight": "Populate `out.AllowedModuleRoles` from the gen `AllowedModuleRolesQualifiedNames()` (BY_NAME role refs, stored as qualified-name strings)"} -{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders a loop `break;`/`continue;` as `-- Empty action`", "cause": "`flowObjectFromGen` had no cases for `Microflows$BreakEvent` / `Microflows$ContinueEvent`, so they fell through to the ActionActivity default with a nil action", "file": "`mdl/backend/modelsdk/microflow.go` (`flowObjectFromGen`)", "insight": "Add both object cases (renderer already maps them to `break;`/`continue;`)"} -{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders `export to mapping …` / `import from mapping …` as `-- Empty action`; and single-object mapping results render `as list of Entity` instead of `as Entity`", "cause": "`actionFromGen` lacked `ExportXmlAction`/`ImportXmlAction` cases; `restResultHandlingFromRaw` set `SingleObject` only from `Range.SingleObject`, ignoring an object var type (REST) and `ForceSingleOccurrence` (XML import)", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`, `restResultHandlingFromRaw`, `readMappingCall`)", "insight": "Add both XML-action cases reading mapping/arg/output from raw `ResultHandling`/`OutputMethod`/`ImportMappingCall`; extract `readMappingCall`; apply the object-type rule (REST) and force fallback (XML import) — mirrors legacy `parseExportXmlAction`/`parseImportXmlAction`/`parseResultHandling`"} -{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine drops a database retrieve's `sort by …` clause", "cause": "`retrieveSourceFromGen` read Entity/XPath/Range but not the sort columns, which live in the `DatabaseRetrieveSource`'s `NewSortings` child", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`retrieveSourceFromGen`, `sortItemsFromRaw`)", "insight": "Generalize `sortItemsFromRaw` to accept the `NewSortings` wrapper key (it already handled the Sort list-op `Sortings` key); set `s.Sorting = sortItemsFromRaw(g.Raw())`"} -{"area": "mdl/backend", "date": "2026-07-02", "symptom": "A database retrieve's `sort by …` is **completely missing** from DESCRIBE after any modelsdk **write** (regression on switching to the modelsdk engine; \"no more sort\") — reproduces on a fresh `create microflow … retrieve … sort by …` round-trip", "cause": "The WRITE counterpart of the row above: `retrieveSourceToGen` set an **empty** `genMf.NewSortItemList()` and never serialized `s.Sorting`, so nothing was stored for the read to render. The generated `SortItemList` type also stores items under `\"Items\"`, but Mendix stores retrieve sort columns under `\"Sortings\"` (like the Sort list-op), so the gen convenience type is the wrong tool", "file": "`mdl/backend/modelsdk/microflow_write.go` (`retrieveSourceToGen`)", "insight": "Build the envelope manually, mirroring the `SortOperation` branch: `newElem(\"Microflows$SortingsList\",\"\")` + `addPartList(sl,\"Sortings\", sortItemToGen(each))` + `g.SetSortItemList(sl)`. Guard with a toGen→encode→decode→fromGen round-trip test asserting the sort items survive. Note \"completely missing\" ⇒ write dropped it (nothing stored); a *blank-attribute* sort line ⇒ a read/name-resolution gap instead. Issue #727", "refs": ["#727"]} -{"area": "mdl/backend", "date": "2026-07-02", "symptom": "Consumed OData service **Configuration microflow** has no effect in Studio Pro ≥ 11.10 — the \"Configuration source\" dropdown stays on \"Constants only\" even though `describe odata client` shows the microflow", "cause": "Mendix **renamed the BSON storage field** across versions (from the `Rest$ConsumedODataService` reflection metadata): `ConfigurationMicroflow` was introduced 10.12.0 and **DELETED 11.10.0**; `ConfigurationEntityMicroflow` was introduced 11.10.0. mxcli wrote the pre-11.10 key on all versions, so ≥ 11.10 Studio Pro ignored the unknown field. Both engines were affected (identical BSON). The codebase had flip-flopped this key (573→`ConfigurationEntityMicroflow`, then reverted) — the reflection `VersionInfos` Introduced/Deleted is the authority", "file": "`model.ODataConfigMicroflowBSONKey` (new, version-gated), wired into `mdl/backend/modelsdk/odata_write.go` (`consumedODataServiceToGen` via `Backend.configMicroflowKey()`) **and** `sdk/mpr/writer_odata.go` (`serializeConsumedODataService` via `w.reader.ProjectVersion()`)", "insight": "Gate the write key on project major/minor (≥ 11.10 → `ConfigurationEntityMicroflow`). On read, coalesce **all** historical keys (`model.ODataConfigMicroflowBSONKeys()` in the legacy parser; `firstNonEmpty(ConfigurationEntity…, Configuration…, Headers…)` of the gen getters in `integration_read.go`) so a service authored by any version round-trips. When a storage name looks version-dependent, check `modelsdk/gen/*/version.go` `VersionInfos` before guessing. Issue #728", "refs": ["#728"]} -{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders an inheritance split header as `split type $` (empty variable) and drops its `@caption`", "cause": "`flowObjectFromGen` created a bare `InheritanceSplit` without reading `SplitVariableName`/`Caption`/`Documentation` off the gen element", "file": "`mdl/backend/modelsdk/microflow.go` (`flowObjectFromGen`)", "insight": "Populate `VariableName`/`Caption`/`Documentation` from the gen `InheritanceSplit` accessors"} -{"area": "mdl/backend", "date": "2026-07-07", "symptom": "`mx check` **CE4899** \"concurrent execution: error message or microflow required\" after a microflow round-trip (`GetMicroflow` → `UpdateMicroflow`), even though the microflow allowed concurrent execution", "cause": "`microflowFromGen` never read `AllowConcurrentExecution`/`MarkAsUsed` back (the write path set them), so the round-trip reset them to `false` → \"disallow concurrent execution\" with no error message configured", "file": "`mdl/backend/modelsdk/microflow.go` (`microflowFromGen`)", "insight": "Read both flags back from the gen getters (`mf.AllowConcurrentExecution()`, `mf.MarkAsUsed()`) — keep the FromGen converter lossless. Guarded by a `toGen→encode→decode→fromGen` Go test, not MDL (create-or-modify rebuilds the microflow from MDL, so it can't reproduce a read-back bug). Issue #723 A1", "refs": ["#723"], "ce": ["CE4899"]} -{"area": "mdl/backend", "date": "2026-07-07", "symptom": "Studio Pro renders **every activity/decision as a 1-px sliver** (caption wraps one letter per line) after a modelsdk round-trip; `mx check` reports **NO** error (it ignores box size, so the corruption is silent)", "cause": "`flowObjectFromGen` carried each object's `Position` but not its `Size`, so the round-trip rewrote every node with size `0;0`", "file": "`mdl/backend/modelsdk/microflow.go` (`sizeFromGen`, `splitFlowObjects`)", "insight": "Add `sizeFromGen` (mirrors `pointFromGen`) and apply it at the `splitFlowObjects` call site — covers nested loop bodies via recursion; add `GetSize`/`SetSize` to `BaseMicroflowObject`. Go round-trip test (not MDL, same reason as A1). Issue #723 A2", "refs": ["#723"]} -{"area": "mdl/backend", "date": "2026-07-07", "raw": "| `mx check` **CE0117** \"Error in expression\" when creating a rule-based decision (`if Module.SomeRule(...)`) via MDL on the **modelsdk** engine; the decision's subtype is demoted Rule→Expression on every round-trip | modelsdk `*Backend` never implemented `IsRule` → fell back to the embedded `unimplemented` stub (which errors); the builder's `isRule, err := backend.IsRule(...); if err != nil || !isRule` guard treated the error as \"not a rule\" and emitted an invalid `ExpressionSplitCondition` | `mdl/backend/modelsdk/microflow.go` (`IsRule`) | Implement `IsRule` on `*Backend` (list `Microflows$Rule` units, match qualified name via `moduleNameFor`), mirroring the legacy reader; it shadows the generated stub. With the existing builder mock-test this guards the full chain modelsdk.IsRule→RuleSplitCondition. Issue #723 A4 |", "refs": ["#723"], "ce": ["CE0117"]} -{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders `download file $X …;` as `-- Empty action`", "cause": "`actionFromGen` lacked a `DownloadFileAction` case", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`)", "insight": "Add the case reading `FileDocumentVariableName()`/`ShowFileInBrowser()`, defaulting an empty `ErrorHandlingType` to Rollback. Note: the storage key is `ShowFileInBrowser` (legacy's `parseDownloadFileAction` reads the wrong `ShowInBrowser` key — a latent legacy bug; the gen reads it correctly)"} -{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders a legacy SOAP `call web service …` as `-- Empty action`", "cause": "`actionFromGen` lacked a `WebServiceCallAction` case", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`, `webServiceActionRequiresRawBSON`)", "insight": "Add the case: read the structured fields (ImportedService / OperationName / NewResultHandling / RequestHandling) and, when the action carries any field the structured form can't represent, set `RawBSON = a.Raw()` so the renderer emits `call web service raw ''`. Mirror legacy's supported-key set exactly; `canonicalRawBSON` makes both engines' base64 byte-identical"} -{"area": "mdl/backend", "date": "2026-07-01", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine emits a REST `body mapping X` line with no `from $var`, which fails to re-parse (`mismatched input … expecting FROM`) — breaks the DESCRIBE roundtrip", "cause": "`restRequestHandlingFromRaw` read the export-mapping source variable from the wrong key `ParameterVariable`; the real storage key is `MappingVariableName` (same key ExportXmlAction uses), so it came back empty and the renderer dropped the grammar-mandatory `from $var`", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`restRequestHandlingFromRaw`)", "insight": "Read `ParameterVariable` from `MappingVariableName` (fallback to `ParameterVariable`). Note this makes modelsdk *more* faithful than legacy, whose `parseRequestHandling` drops the REST request body mapping entirely (\"would be parsed here if needed\")"} -{"area": "mdl/backend", "date": "2026-07-01", "symptom": "`SHOW ACCESS ON PAGE` / Page section of `SHOW SECURITY MATRIX` reports \"no roles\" for a restricted page on the **modelsdk** engine (legacy is correct) — under-reports page access, a security-audit hazard", "cause": "`pageFromGen` never read the page's allowed module roles into `Page.AllowedRoles`, so it defaulted to empty. (The gen `Page` decode is correct — it has the storage-name override `allowedRoles`↔`AllowedModuleRoles`.) The microflow/nanoflow equivalent was fixed separately", "file": "`mdl/backend/modelsdk/page.go` (`pageFromGen`)", "insight": "Populate `out.AllowedRoles` from the gen `AllowedRolesQualifiedNames()` (mirrors `microflowFromGen`). Fixes both `SHOW ACCESS ON PAGE` and the matrix Page section. Issue #722", "refs": ["#722"]} -{"area": "mdl/backend", "date": "2026-05-16", "symptom": "Pluggable widget `Selection` BSON value is lowercase (`single` / `multi` / `none`) but Studio Pro stores PascalCase — contributes to CE0463 drift on stricter widgets and looks wrong in diffs", "cause": "MDL passes the user's typed value verbatim to `SetSelection`; the builder didn't normalise", "file": "`mdl/backend/mpr/widget_builder.go` `SetSelection`", "insight": "Canonicalise via `canonicalSelectionValue` helper (lowercase-keyed switch → `Single` / `Multi` / `None`); unknown values pass through", "ce": ["CE0463"]} -{"area": "mdl/backend", "date": "2026-06-19", "symptom": "A page-level property can't be set — `ALTER PAGE X { SET PopupWidth = 800; }` (or any page-level prop other than Title/Url) fails with `unsupported page-level property: …`", "cause": "The page-level SET handler only special-cased a couple of properties; everything else fell through to the default error", "file": "`mdl/backend/pagemutator/mutator.go` → `applyPageLevelSetMut` (shared by both engines)", "insight": "Add a `case` writing the field at the top level of the Forms$Page doc via `dSetOrAppend` with the on-disk BSON type (int64 for PopupWidth/PopupHeight, bool for PopupResizable — verify against a Studio Pro page with `mxcli bson dump --format bson`). Page-level prop names are **case-sensitive**. For DESCRIBE roundtrip, emit the values back in the CREATE PAGE header. CREATE-time support: add a generic `IDENTIFIER COLON propertyValueV3` to `pageHeaderPropertyV3` (regen grammar), recognize the keys in `parsePageHeaderV3` (`applyGenericPageHeaderProp`, error on unknown), carry `*int`/`*bool` on `CreatePageStmtV3`, default to 600/600/false in `buildPageV3`, and have both writers honour `page.Popup*` (legacy `sdk/mpr/writer_pages.go` int64; codec `mdl/backend/modelsdk/page_write.go` int32 via gen — tolerated by mx check). The MCP backend has its **own** `mcpPageMutator` (pg content tree, not raw BSON) — page-level SET there reaches `SetWidgetProperty(\"\")`; map it or reject honestly (it rejects, pending a `pg_read_page` probe of the pop-up keys). Issue #661", "refs": ["#661"]} -{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`CREATE EXTERNAL ENTITIES FROM Module.Service` imports external entities as plain **PERSISTED** entities and NPEs **without the \"from Service\" link** (regression on the default modelsdk engine; legacy `--engine legacy` was correct)", "cause": "The modelsdk write adapters dropped all external-entity serialization: `entityToGen` only handled `DomainModels$OqlViewEntitySource`, `attributeToGen` always emitted `StoredValue`, and `assocToGen` forced `Source` null. So `e.Source = \"Rest$ODataRemoteEntitySource\"` (+ RemoteServiceName/EntitySet/KeyParts) was ignored and the entity fell to the plain NoGeneralization path", "file": "`mdl/backend/modelsdk/domainmodel_write.go` (`entityToGen`, `attributeToGen`, `assocToGen`) — the executor (`applyExternalEntityFields` in `mdl/executor/cmd_contract.go`) already stamps the right `domainmodel` fields; only the write adapter was lossy. The legacy serializer `sdk/mpr/writer_domainmodel.go` is the parity reference", "insight": "Port the legacy logic into the codec adapters using the gen `Rest$OData*` types (all present in `modelsdk/gen/rest`): `externalEntitySourceToGen` builds `Rest$ODataRemoteEntitySource`/`…EntityTypeSource`/`…PrimitiveCollectionEntitySource` (+ `ODataKey`/`ODataKeyPart`, with fresh IDs since `assignEntityIDs` only stamps the top source); `attributeToGen` takes an `isExternal` flag and emits `Rest$ODataMappedValue` / `…MappedPrimitiveCollectionValue`; `externalAssociationSourceToGen` emits `Rest$ODataRemoteAssociationSource` / `…PrimitiveCollectionAssociationSource` (the `DomainModels$Association` NullFields:[\"Source\"] default still nulls Source for plain associations because SetSource only emits when set). Tests in `external_entity_write_test.go`. Issue #718", "refs": ["#718"]} -{"area": "mdl/backend", "date": "2026-07-01", "symptom": "`CreatePage: ListView source *pages.DatabaseSource not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy` — a native `listview` with a `database`/`database from … where …` datasource fails on the default engine", "cause": "`listViewSourceToGen` only handled `*pages.MicroflowSource`; database (and context/listen/association) sources fell through to the \"not supported\" error. DataView, DataGrid2, and Gallery database sources already worked — only native ListView was missing", "file": "`mdl/backend/modelsdk/widget_write.go` (`listViewSourceToGen`); mirror the `customWidgetDataSourceToGen` database branch and the legacy `serializeListViewDataSource`", "insight": "Add a `*pages.DatabaseSource` case building `genPg.NewListViewXPathSource()` with EntityRef (`DirectEntityRef`), XPathConstraint, a `NewGridSortBar()` (GridSortItem per `d.Sorting`, note gen uses **SortBar** not the legacy `Sort`/`ListViewSort`), and a `NewListViewSearch()` — the codec already has all types. Verify with `mx check` = 0 errors. Tests in `listview_source_write_test.go`"} -{"area": "mdl/backend", "date": "2026-06-29", "symptom": "CE0488 \"No entity configured for the data source of this data view\" on a context (page-parameter-bound) data view authored over **MCP** — DESCRIBE looks correct (`dataview dvX (DataSource: $Param)`), but the model has no entity on the source. DataGrids with database sources are fine", "cause": "`mapDataViewSource`'s `*pages.DataViewSource` case used a mutually-exclusive `switch`: when `ParameterName != \"\"` it wrote only `sourceVariable` and never `entityRef`. Studio Pro/pg write **both** entityRef AND sourceVariable for a context source (see `testdata/pg-page-contact-newedit.json`)", "file": "`mdl/backend/mcp/page_widgets.go` → `mapDataViewSource`", "insight": "Drop the exclusive switch: set `entityRef` (DomainModels$DirectEntityRef) whenever `EntityName != \"\"` AND `sourceVariable` (Pages$PageVariable) whenever `ParameterName != \"\"`. The executor already resolves the parameter's entity into `EntityName` (`cmd_pages_builder_v3.go` parameter case), so both are available. MCP-only — the MPR engines were never affected", "ce": ["CE0488"]} -{"area": "mdl/backend", "date": "2026-06-19", "raw": "| Under the default modelsdk engine a SHOW/DESCRIBE errors `… not implemented yet — rerun with MXCLI_ENGINE=legacy`, OR the read \"works\" but DESCRIBE drops nested config (return type, parameters, exposed-as info) into a half-shell that's fine under `--engine legacy` | The modelsdk `Backend` never overrode the read method (falls through to the generated `unimplemented` embed), OR a gen-accessor converter read children under the codegen property names while real Studio-Pro BSON stores them under the original storage names — the gen codec decodes those keys into empty lists/parts | `mdl/backend/modelsdk/*_read.go` (add the override) + `mdl/backend/modelsdk/unimplemented_gen.go` (confirm the stub) | First confirm the gap: `comm -23 <(grep -hoP 'errUnimplemented\\(\"\\K[^\"]+' mdl/backend/modelsdk/unimplemented_gen.go \\| sort -u) <(grep -rhoP 'func \\([a-z]* \\*Backend\\) \\K[A-Za-z0-9_]+' mdl/backend/modelsdk/*.go \\| grep -v _test \\| sort -u)`. Then **dump the real raw keys before writing the converter** (a gen-accessor port silently half-shells): iterate `b.ListRawUnitsByType(typePrefix)` + `b.GetRawUnit(id)` and read the fidelity-sensitive children from the empirical keys, not the gen accessors. JS actions store `JavaReturnType`/`Parameters`/`TypeParameters`/`MicroflowActionInfo` (gen wants `ActionReturnType`/`ActionParameters`/`ActionTypeParameters`/`ModelerActionInfo`) — see `javascript_read.go`. Validate with describe-parity vs `--engine legacy` (modelsdk may be *more* correct, e.g. recovers `Enum X` where legacy drops to `Object`). Issue 7 |"} -{"area": "mdl/backend", "date": "2026-06-19", "symptom": "`ALTER ENTITY ... ADD ATTRIBUTE` (or any ALTER) succeeds and `DESCRIBE ENTITY` is byte-identical before/after, but `mx check` then reports CE1613 \"attribute … no longer exists\" — often on a *different* entity's inherited system member (`createdDate`) referenced by grids/sort-bars in another module", "cause": "The ALTER target is rebuilt from the semantic `domainmodel.Entity` (`entityToGen` → fresh `NewEntity`, `raw==nil`), so the codec's `EmitGUID` default writes `GUID = $ID`, discarding the entity's real on-disk GUID. The GUID is the stable cross-reference identity inheriting entities/pages resolve members through, so changing it dangles those refs. The forgiving reader re-derives DESCRIBE from flags and never shows GUID, so mxcli can't see its own damage. (Legacy `sdk/mpr` had the broader form: it re-serializes the *whole unit* and forces `GUID = $ID` on every sibling too)", "file": "`mdl/backend/modelsdk/domainmodel_alter.go` → `UpdateEntity`", "insight": "Transplant the original element's raw bytes onto the rebuilt target (`ge.SetRaw(orig.Raw())`) so the codec treats it as an EXISTING element: dirty properties re-encode from `ge`, unmodeled fields (GUID, Capabilities, …) pass through verbatim. Siblings already survive via the list-rebuild raw passthrough. When a write \"succeeds\" but `DESCRIBE` is suspiciously byte-identical, suspect an identity (`$ID`/`GUID`) the reader doesn't surface — assert on raw BSON, not DESCRIBE. Issue #657", "refs": ["#657"], "ce": ["CE1613"]} -{"area": "mdl/backend", "date": "2026-07-01", "symptom": "`mx check` reports CE6621 \"Attribute 'X' has max length '200' which is not the same as max length in the OData service which is 'unlimited'\" on every unlimited-length string attribute of a `create external entities from …` import — **only under the default modelsdk engine**; `--engine legacy` is clean", "cause": "The codec's encoder only emits *dirty* properties for a new element. `attributeTypeToGen` skipped `SetLength` when `Length == 0` (unlimited), so the `Length` property stayed non-dirty and the field was omitted from the BSON entirely. Studio Pro then applies its own UI default of 200, which contradicts the OData service's \"unlimited\" type. Legacy's `writer_domainmodel.go` always writes `Length: t.Length` (including 0), so it never regressed", "file": "`mdl/backend/modelsdk/domainmodel_write.go` → `attributeTypeToGen` (`*domainmodel.StringAttributeType` case)", "insight": "Always call `g.SetLength(int32(at.Length))` — drop the `if at.Length > 0` guard — so `Length: 0` is emitted explicitly, matching the legacy serializer. General rule for the codec write path: any field whose *zero value is meaningful* (0 = unlimited here) must be Set unconditionally, because an omitted field falls back to Studio Pro's own default, not to zero. Assert on the serialized `NewType` map (not DESCRIBE) — the reader treats a missing Length as unlimited so it hides the damage. Test `TestEntityToGen_ODataRemoteEntitySource` in `external_entity_write_test.go`. Issue #718", "refs": ["#718"], "ce": ["CE6621"]} -{"area": "mdl/backend", "date": "2026-07-11", "symptom": "`CREATE PAGE` with a widget `Action: nanoflow …` fails on the **default (modelsdk)** engine: `client action *pages.NanoflowClientAction not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy` (legacy works). Blocks any nanoflow-triggered button/link written via MDL", "cause": "`clientActionToGen` had cases for save/cancel/close/delete/page/microflow/create-object client actions but **no `*pages.NanoflowClientAction` case** → hit the `default` refuse-loudly branch. (The sibling microflow-arg drop was the separate Bug 1, already fixed on `microflowSettingsToGen`.)", "file": "`mdl/backend/modelsdk/widget_write.go` → `clientActionToGen`", "insight": "Add a `*pages.NanoflowClientAction` case building a gen `CallNanoflowClientAction`. Unlike the microflow action (which nests a `Forms$MicroflowSettings`) it carries the Nanoflow name + `ParameterMappings` **directly**: `SetNanoflowQualifiedName`, `SetProgressBar(\"None\")`, `SetDisabledDuringExecution(true)`, and one `NanoflowParameterMapping` per arg (`SetParameterQualifiedName(nanoflow+\".\"+param)` BY_NAME + `SetExpression($var)`); register `Forms$CallNanoflowClientAction` TypeDefaults (`MandatoryLists:[ParameterMappings]`, `NullFields:[ProgressMessage,ConfirmationInfo]`). Mirror the legacy `writer_widgets_action.go`. The modelsdk **read** already round-trips it (describe shows `nanoflow M.NF(Form: $Form)`). Verify both engines with `mx check`=0. Bug-test `bug2-modelsdk-nanoflow-widget-action.mdl`; tests `TestNanoflowClientAction_Serialized` (+ `TestMicroflowClientAction_PersistsParameters` guards Bug 1's mapping stays wired through the shared helper). Bug 2"} -{"area": "mdl/backend", "date": "2026-07-03", "raw": "| `GRANT VIEW ON PAGE Module.Page TO Module.Role` prints success but the page's allowed roles appear to stay empty: `mxcli lint` still reports `CE0557`/`MPR007` (\"used as home page … but has no allowed roles\") across a freshly rebuilt catalog, while entity-access grants on the same roles verify fine (`describe entity` shows them). Reported on **modelsdk** engine (default), Mendix 11.11 | NOT a write bug — the grant **does** persist (`AllowedModuleRoles: [3, \"Module.Role\"]` on disk; `mx check` = 0 errors; Studio Pro would load clean). The regression was on the **read** path: modelsdk `pageFromGen` didn't populate `Page.AllowedRoles`, and `mxcli lint`'s CE0557 rule counts `len(pg.AllowedRoles)` from `ListPages()` — so it read every page as having zero roles and false-fired. `SHOW ACCESS ON PAGE` / `SHOW SECURITY MATRIX` under-reported the same way. The entity-vs-page asymmetry the reporter saw = different read paths (entity `describe` worked; page `lint` didn't), not different writes | `mdl/backend/modelsdk/page.go` → `pageFromGen` | Already fixed by `88a2d50b1` (issue #722): populate `out.AllowedRoles` from the gen `AllowedRolesQualifiedNames()`, mirroring `microflowFromGen`. Guard for the reporter's exact end-to-end flow: `TestUpdateAllowedRoles_PagePersistsForLintRead` (`mdl/backend/modelsdk/page_roles_test.go`) writes a page role then reopens fresh and asserts the lint read path (`ListPages`→`AllowedRoles`) sees it. **Diagnosis tip**: when a write \"reports success but the value stays empty\" *only in a read/lint/show command*, dump the raw unit (`mxcli bson dump`) — if the bytes are there, it's a read-adapter (`*FromGen`) gap, not a write bug; confirm by reverting the suspected read commit and re-running | ", "refs": ["#722"], "ce": ["CE0557"]} -{"area": "mdl/backend", "date": "2026-07-06", "symptom": "`MOVE ENTITY X to OtherModule` where a **view entity's OQL** joins through `X`: passes `mxcli check`, but `mx check` on **Mendix 10.x** reports `CE0174 \"…path '…/DmTest.Customer' does not resolve to an entity\"` at the view entity — **only on the modelsdk engine**, only on 10.x (11.x is clean; legacy is clean)", "cause": "Mendix <11 stores a view entity's OQL in **two** places: the top-level `DomainModels$ViewEntitySourceDocument` unit AND inline on the entity's `DomainModels$OqlViewEntitySource` (see `entityToGen`, gated `major < 11`). modelsdk's `UpdateOqlQueriesForMovedEntity` rewrote only the source document, leaving the inline copy pointing at the old module. 11.x doesn't write the inline copy (OQL lives only on the source doc), so it was never affected — hence 10.x-only. Legacy updates the inline copy incidentally by re-serializing the domain model", "file": "`mdl/backend/modelsdk/move_view_write.go` → `UpdateOqlQueriesForMovedEntity`", "insight": "After the ViewEntitySourceDocument sweep, also iterate every domain model (`ListDomainModels` → `loadDomainModelGen`), find each entity whose `Source()` is `*genDm.OqlViewEntitySource` with an `Oql()` containing the old qualified name, `SetOql(strings.ReplaceAll(...))`, and `persistDM`. Mirrors the `UpdateEnumerationRefsInAllDomainModels` walk. **Diagnosis tip**: when a value updates in one storage location but a stale copy elsewhere still breaks `mx check`, dump raw `mprcontents` and grep for the value across `$Type`s — Mendix often duplicates a field (OQL, captions) and a version gate (`major < 11`) can make one copy version-conditional. Repro: `mdl-examples/bug-tests/move-entity-oql-view-inline-ref.mdl`", "ce": ["CE0174"]} -{"area": "mdl/backend", "date": "2026-07-06", "symptom": "`MOVE ENTITY` / `MOVE ENUMERATION` cross-module prints `Warning: Could not update OQL queries` / `Could not update enumeration references: load domain model 00000000-0000-0000-0000-000000000002: open …/mprcontents/00/00/00000000-…002.mxunit: no such file or directory` — **modelsdk engine only**. The move itself succeeds and (today) user modules are still updated, because `ListDomainModels` appends the failing System DM **last** — but the warning is spurious and the ordering dependence is fragile", "cause": "Both reference-update sweeps iterate `ListDomainModels()`, which **injects the virtual System-module domain model** (ID `…002`, `buildSystemDomainModel`) so platform entities resolve. Each sweep then re-loads every listed DM from disk via `loadDomainModelGen(info.ID)` — but the System module isn't stored in `mprcontents`, so its `.mxunit` read fails. Legacy never hit this: it walks in-memory `*domainmodel.DomainModel` objects and never re-reads from disk", "file": "`mdl/backend/modelsdk/association_move_write.go` → `UpdateEnumerationRefsInAllDomainModels`; `mdl/backend/modelsdk/move_view_write.go` → `UpdateOqlQueriesForMovedEntity`", "insight": "Skip the System DM at the top of each DM loop: `if string(info.ID) == meta.SystemDomainModelID { continue }` (import `modelsdk/meta`). It never holds user enum refs or user view-entity OQL, so skipping is correct and removes the disk-read failure regardless of iteration order. **Diagnosis tip**: a \"no such file / not found\" on the fixed ID `…002` (or module `…001`) means code is trying to load the *virtual* System unit from disk — filter it out, don't try to read it. Regression: `TestUpdateRefsForMovedEntity_SkipsSystemDomainModel` (`move_refs_system_test.go`)"} -{"area": "mdl/backend", "date": "2026-07-11", "symptom": "Under the **default (modelsdk)** engine, any page widget with an **association datasource** (`datasource: $currentObject/Module.Assoc`, the master-detail idiom) fails at exec: `CreatePage: DataView source *pages.AssociationSource not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy`. Same for a nested `listview`", "cause": "`customWidgetDataSourceToGen` (pluggable widgets) already handled `*pages.AssociationSource`, but `dataViewSourceToGen`/`listViewSourceToGen` had no case → hit the \"not yet supported\" default. (Legacy doesn't error but writes a **broken** `Forms$AssociationSource{EntityRef: nil}` in `serializeListViewDataSource`, so legacy is not a valid reference — tracked as **L1** in [`docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md`](../../docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md); do **not** fix legacy, it will be removed when modelsdk matures)", "file": "`mdl/backend/modelsdk/widget_write.go` (`associationSourceToGen`, wired into `dataViewSourceToGen`+`listViewSourceToGen`) + `mdl/executor/cmd_pages_describe.go` (`associationSourcePath`) + `cmd_pages_describe_parse.go` (DataView/ListView datasource cases) + `cmd_pages_describe_output.go` (`associationDataSourceExpr`)", "insight": "**modelsdk engine only.** Extract the proven `AssociationSource` block from `customWidgetDataSourceToGen` into a shared `associationSourceToGen` (Forms$AssociationSource → `IndirectEntityRef` of `EntityRefStep{Association, DestinationEntity}` + optional page-param `SourceVariable`; `$currentObject` → nil source var) and add a `case *pages.AssociationSource` to the DataView and ListView source builders. Matches the canonical legacy `serializeAssociationSource` (writer_widgets.go) and the Studio-Pro-verified `EntityRefStep` structure — NOT the broken `serializeListViewDataSource` stub. DESCRIBE reconstructs `$currentObject/Module.Assoc` from `EntityRef.Steps`. **An association source is valid only on LIST-producing widgets (listview/datagrid/gallery/templategrid), NOT a plain DataView** — Studio Pro rejects a DataView association source (\"cannot have a data source of type association\"); the widget still exec-creates (valid BSON), so `check` flags it with **MDL-WIDGET08** (`validateStaticWidget`). Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go`, `mdl/executor/cmd_pages_describe_assoc_source_test.go`, `validate_widgets_test.go`; bug-tests `mdl-examples/bug-tests/assoc-datasource-modelsdk.mdl` (valid listview) + `dataview-association-source-rejected.fail.mdl` (MDL-WIDGET08)"} -{"area": "mdl/backend", "date": "2026-07-09", "symptom": "Under the **default (modelsdk)** engine, any page widget with an **association datasource** (`datasource: $currentObject/Module.Assoc`, the master-detail idiom) fails at exec: `CreatePage: DataView source *pages.AssociationSource not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy`. Same for a nested `listview`", "cause": "`customWidgetDataSourceToGen` (pluggable widgets) already handled `*pages.AssociationSource`, but `dataViewSourceToGen`/`listViewSourceToGen` had no case → hit the \"not yet supported\" default. (Legacy doesn't error but writes a **broken** `Forms$AssociationSource{EntityRef: nil}` in `serializeListViewDataSource`, so legacy is not a valid reference — tracked as **L1** in [`docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md`](../../docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md); do **not** fix legacy, it will be removed when modelsdk matures)", "file": "`mdl/backend/modelsdk/widget_write.go` (`associationSourceToGen`, wired into `dataViewSourceToGen`+`listViewSourceToGen`) + `mdl/executor/cmd_pages_describe.go` (`associationSourcePath`) + `cmd_pages_describe_parse.go` (DataView/ListView datasource cases) + `cmd_pages_describe_output.go` (`associationDataSourceExpr`)", "insight": "**modelsdk engine only.** Extract the proven `AssociationSource` block from `customWidgetDataSourceToGen` into a shared `associationSourceToGen` (Forms$AssociationSource → `IndirectEntityRef` of `EntityRefStep{Association, DestinationEntity}` + optional page-param `SourceVariable`; `$currentObject` → nil source var) and add a `case *pages.AssociationSource` to the DataView and ListView source builders. Matches the canonical legacy `serializeAssociationSource` (writer_widgets.go) and the Studio-Pro-verified `EntityRefStep` structure — NOT the broken `serializeListViewDataSource` stub. DESCRIBE reconstructs `$currentObject/Module.Assoc` from `EntityRef.Steps`. A `Forms$AssociationSource` is valid on **LIST-producing** widgets (listview/datagrid/gallery/templategrid). A plain **DataView** cannot use `Forms$AssociationSource` (MxBuild **CE6705**) — but the DataView *association* case is still valid via a **different** source type; see the \"data from context over association\" row below (an earlier fix wrongly rejected it with MDL-WIDGET08, now removed). Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go`, `mdl/executor/cmd_pages_describe_assoc_source_test.go`; bug-test `mdl-examples/bug-tests/assoc-datasource-modelsdk.mdl` (valid listview)", "ce": ["CE6705"]} -{"area": "mdl/backend", "date": "2026-07-09", "symptom": "A **DataView bound to a to-one referenced object over an association** (\"data from context\", e.g. an inner DataView showing the referenced Employee's `Name`/`Email` inside an `Expense` DataView) can't be authored/round-tripped: `dataview dvEmp (datasource: $currentObject/Module.Assoc)` was rejected (MDL-WIDGET08), a bare nested `dataview { textbox (attribute: Name) }` bound children to the **parent** entity (CE1613), and `describe` dropped the source → describe/exec **silently strips** the association DataView (round-trip corruption)", "cause": "A DataView over an association is **not** a `Forms$AssociationSource` (that's list-widgets-only; CE6705 on a DataView). It is a `Forms$DataViewSource` whose `EntityRef` is a `DomainModels$IndirectEntityRef` navigating the association (a DataViewSource *is* an `EntityPathSource`, so its EntityRef may be indirect — `generated/metamodel/types.go:558`). mxcli generated the wrong source type, then (Bug 5 v1) wrongly rejected the whole case", "file": "`mdl/backend/modelsdk/widget_write.go` (`dataViewContextAssociationSourceToGen`, wired into `dataViewSourceToGen`'s `*pages.AssociationSource` case) + `mdl/executor/cmd_pages_builder_v3_widgets.go` (removed the `buildDataViewV3` association guard) + `mdl/executor/validate_widgets.go` (removed MDL-WIDGET08) + `mdl/executor/cmd_pages_describe_parse.go` (`Forms$DataViewSource` reads `EntityRef.Steps` via `associationSourcePath`)", "insight": "**modelsdk engine only.** For a DataView association datasource, emit `Forms$DataViewSource` with `EntityRef = IndirectEntityRef{EntityRefStep{Association, DestinationEntity}}` (+ optional page-param `SourceVariable`; `$currentObject` → none) — mirrors `associationSourceToGen` but wraps the IndirectEntityRef in a DataViewSource. Children then bind to the destination entity. DESCRIBE reconstructs `$currentObject/Module.Assoc`. Removes the earlier MDL-WIDGET08 over-rejection. **When two widget kinds share a navigation but MxBuild accepts it on only one, the distinction is the source `$Type` wrapper, not the EntityRef inside it** — check `generated/metamodel/types.go` for the widget's allowed `DataSource` subtypes. mxbuild-validated (`mxcli docker check --no-update-widgets` = 0 errors) + full describe→re-exec round-trip. Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go` (`TestDataViewAssociationSource_Serialized`); bug-test `mdl-examples/bug-tests/dataview-context-association-source.mdl`. Bug 5 (reclassified)", "ce": ["CE1613", "CE6705"]} -{"area": "mdl/backend", "date": "2026-07-10", "symptom": "`DESCRIBE` of a view entity on the **default (modelsdk)** engine omits the `as (… OQL …)` clause entirely (legacy renders it) — the describe→exec round-trip silently drops the query, leaving a queryless view. Reproduces on any Mendix **11.x** project", "cause": "On Mendix 11.0+ the OQL is stored **only** in the separate `DomainModels$ViewEntitySourceDocument`; the inline `OqlViewEntitySource.Oql` field was removed (see the writer's `if !pv.IsAtLeast(11,0)` gate in `serializeOqlViewEntitySource`). The modelsdk read (`entityFromGen`: `out.OqlQuery = src.Oql()`) only read the now-empty inline field and never followed the `SourceDocumentRef`. Legacy was correct — `loadViewEntityOqlQueries` in `sdk/mpr/reader_documents.go` already joins the source docs", "file": "`mdl/backend/modelsdk/domainmodel.go` (`populateViewEntityOql`, wired into `ListDomainModels` + `GetDomainModel`)", "insight": "After building the domain models, list `ViewEntitySourceDocument` units (`mprread.ListUnitsWithContainer[*genDm.ViewEntitySourceDocument]`), map `moduleName.docName → Oql()`, and fill `OqlQuery` for any view entity with an empty `OqlQuery` + a `SourceDocumentRef` (mirrors the legacy join; guarded by a cheap pre-check so non-view projects pay nothing). When a view-entity read differs between engines on 11.x, suspect a field the platform moved out-of-line into a source document. Test: `TestGetDomainModel_ViewEntityOqlFromSourceDocument` (round-trips via both read paths)"} -{"area": "mdl/backend", "date": "2026-07-10", "symptom": "`DESCRIBE WORKFLOW` on the **default (modelsdk)** engine renders some activities as non-round-trippable comments — `-- [Workflows$StartWorkflowActivity]`, `-- [Workflows$JumpToActivity]`, `-- [Workflows$WaitForTimerActivity]`, `-- [Workflows$WaitForNotificationActivity]` — so `describe → exec` drops those activities and you can't learn their syntax from an existing workflow. User tasks, decisions, splits, call-microflow/workflow round-trip fine (legacy round-trips all of them)", "cause": "The describe **formatter** (`formatWorkflowActivities`) already has typed cases for jump/wait/start/end, but the modelsdk **reader** never produced them: these activities have no `genWf` struct (written via generic `newElem`), so `workflowActivityFromGen`'s type switch fell through to `GenericWorkflowActivity`, which the formatter renders as `-- [$Type]`. Legacy's `parser_workflow.go` had dedicated cases, so legacy was correct — modelsdk-only gap", "file": "`mdl/backend/modelsdk/workflow_read.go` (`workflowActivityFromGen` default → new `workflowSimpleActivityFromGen`)", "insight": "Recognise the untyped simple activities by `el.TypeName()` and rebuild the semantic struct, reading fields from raw BSON via `genWf.RawFieldString` (`TargetActivity` for jump, `Delay` for the timer) — mirrors the legacy parser. Start/end become typed so the formatter **skips** them (implicit in MDL); jump/wait become executable statements. When an engine emits `-- [$Type]` in describe, the *formatter* usually already handles the type — the gap is the *reader* decoding it to `GenericWorkflowActivity`. Test: `TestWorkflowSimpleActivities_ReconstructedTyped`; round-trip validated describe→drop→exec→`docker check` = 0 workflow errors. Bug 11b (11a = the missing `write-workflows.md` authoring skill)"} -{"area": "mdl/backend", "date": "2026-07-11", "symptom": "A built-in widget action (`actionbutton`/`container`/`linkbutton` `Action: microflow M.Flow(Param: $x)`) **silently drops the argument** on the **default (modelsdk)** engine — `describe` shows `Action: microflow M.Flow` (no args), the flow runs with no parameter, and a required-param flow never fires so the button/row no-ops at runtime. `mxcli check`/`docker check`/`docker build` all pass (0 errors) — silent break. Legacy persists it (the workaround)", "cause": "`clientActionToGen`'s `MicroflowClientAction` case built the `Forms$MicroflowSettings` from the microflow **name only** (`microflowSettingsToGen(x.MicroflowName)`), never serializing `x.ParameterMappings` — the executor builds them correctly into the model, the writer threw them away. (NOT the same as `custom-widgets.md`'s \"action mapping too complex for auto-mapping\" — that's about *pluggable-widget action-typed properties* in def.json generation, a different path.)", "file": "`mdl/backend/modelsdk/widget_write.go` (`microflowSettingsToGen`)", "insight": "Give `microflowSettingsToGen` the mappings param; emit one `genPg.MicroflowParameterMapping` each — `SetParameterQualifiedName(\".\")` (BY_NAME) + `SetExpression(variable-or-expression)` — mirroring the legacy `writer_widgets_action.go` BSON. The client-action caller passes `x.ParameterMappings`; the microflow-datasource callers pass `nil`. Verified: default-engine describe now keeps `(Param: $x)`; a type-correct page = 0 mxbuild errors (both engines give the *same* CE0117 on a type-mismatched arg — that's a model error, not the writer). Tests: `TestClientActionToGen_MicroflowParameterMappings`; bug-test `mdl-examples/bug-tests/bug1-widget-action-param-mapping.mdl`. Nanoflow widget actions were a separate gap (the whole `NanoflowClientAction` case was missing) — now fixed in Bug 2 below. Bug 1", "ce": ["CE0117"]} -{"area": "mdl/backend", "date": "2026-07-22", "symptom": "A page with a native **LISTVIEW** over a `database from` (XPath) datasource passes `mx check` but **crashes the browser client at runtime** and redirects to login: `TypeError: Cannot read properties of undefined (reading 'length')` at `processResult` → `retrieveByXPath`. Pluggable Gallery/DataGrid2 database sources are fine", "cause": "The serialized `Forms$ListViewXPathSource` omitted the arrays the client reads `.length` of. **Codec (default):** `Forms$ListViewSearch` emitted without its `SearchRefs` list — the encoder drops an empty, never-`Set` PartList unless the `$Type` is in `RegisterTypeDefaults` (GridSortBar.SortItems was registered; ListViewSearch was not). **Legacy:** wrote a bogus `Forms$ListViewSort` + a `Paths` key (renamed `SearchRefs` in 7.11.0) and no `Forms$GridSortBar`. NOT a `SortItems` marker issue — an empty `[2]` compiles fine when search is off; the crash is the absent `SearchRefs`. Diagnose by building a Deploy target and reading the compiled `deployment/web/pages/.js` (the client model) + `mxcli bson dump --type page` on the source", "file": "`mdl/backend/modelsdk/widget_write.go` (`init` RegisterTypeDefaults) + `sdk/mpr/writer_widgets_display.go` (`serializeListViewDataSource`, `emptyListViewXPathSource`)", "insight": "Codec: `RegisterTypeDefaults(\"Forms$ListViewSearch\", {MandatoryLists: []string{\"SearchRefs\"}})` (emits empty marker-3 list). Legacy: emit `Forms$GridSortBar`/`SortItems` (mirror `SerializeCustomWidgetDataSource`) + `Forms$ListViewSearch`/`SearchRefs` + `ForceFullObjects`; drop the bogus `Sort`/`Paths`. Tests: `TestListViewSourceToGen_SearchRefsEmitted` (codec, encode-level), `TestSerializeListViewDataSource_Database` + `TestEmptyListViewXPathSource_Shape` (legacy). Repro `mdl-examples/bug-tests/listview-database-source-searchrefs.mdl`"} -{"area": "mdl/backend", "date": "2026-07-27", "symptom": "`alter page P { set class = 'x' on }` (lowercase property) hard-errors on EVERY built-in widget: `property \"class\" not found (widget has no pluggable Object)` — but `create page` accepts the same lowercase prop, and these aren't pluggable widgets", "cause": "The MPR page mutator dispatched first-class props via a case-SENSITIVE switch on canonical casing (\"Class\"/\"Caption\"/\"DynamicClasses\"); a lowercase name missed every case and fell through to the pluggable-Object path. `create page` was fine (WidgetV3.GetStringProp is case-insensitive); the sibling MCP mutator already lowercased", "file": "`mdl/backend/pagemutator/mutator.go` (`setRawWidgetPropertyMut`)", "insight": "`switch strings.ToLower(propName)` with lowercase case labels; keep the pluggable fallback (default) on the original casing (template keys are case-sensitive). Test `TestSetWidgetProperty_LowercaseFirstClassProps`; repro `mdl-examples/bug-tests/alter-page-lowercase-set-on-builtin.mdl`. Findings #1", "refs": ["#1"]} -{"area": "mdl/backend", "date": "2026-07-27", "symptom": "`call javascript action` (e.g. NanoflowCommons.OpenURL) passes `check --references` + `exec` but persists as `-- Empty action`; `mx check` fails CE0008 \"No action defined.\" + CE0109 on the output variable. Affects microflows AND nanoflows", "cause": "The default `modelsdk` engine's `microflowActionToGen` had no case for `*microflows.JavaScriptActionCallAction` → returned nil → ActionActivity written with no Action. Read path + legacy engine both handled it (hence check/exec looked green); nanoflows reuse the same converter", "file": "`mdl/backend/modelsdk/microflow_write.go` (`microflowActionToGen`)", "insight": "Add the `JavaScriptActionCallAction` case, built directly to mirror the legacy serializer (storage keys JavaScriptAction/OutputVariableName; JS parameter mappings use the `ParameterValue` key, not the Java-style `Value`). Test `TestMicroflowActionToGen_JavaScriptActionCall`; repro `mdl-examples/bug-tests/javascript-action-call-persist.mdl`. Findings #11", "refs": ["#11"], "ce": ["CE0008", "CE0109"]} -{"area": "mdl/backend", "date": "2026-08-06", "symptom": "A **`CREATE JAVASCRIPT ACTION`** succeeds, `mxcli check` passes and the build is clean, but calling the action in the running app throws **`JavaScript action was not implemented`** and the nanoflow aborts", "cause": "mxcli wrote the source to `javascriptsource//actions/` using the module's own casing. Mendix reads a **lowercased** directory — a blank Mendix 11 app ships `javascriptsource/nanoflowcommons/`, `/datawidgets/`, `/webactions/` for modules named `NanoflowCommons`, `DataWidgets`, `WebActions`. Finding no source at the path it reads, mxbuild generates a stub whose body is `throw new Error(\"JavaScript action was not implemented\")` and bundles that. Only reproduces on a **case-sensitive filesystem**, which is why it survived: on macOS and Windows the two spellings are the same directory", "file": "`mdl/backend/modelsdk/javascript_write.go` and `sdk/mpr/writer_javascriptactions.go` (`jsActionSourceDir`)", "insight": "`strings.ToLower(moduleName)` in both writers — the comment in each previously asserted the opposite (\"unlike javasource, which is lowercased\"), so the belief was documented, not tested. **Generalisable — the shape to look for**: when generated *source files* pair with model units, the model unit is not evidence the file is found; the filesystem path is a separate contract, and a case-only mismatch is invisible on the developer's own machine. Check against the directories a blank project already ships rather than against what the code says. Nothing short of running the app catches it: parse, check and build all pass. Test `mdl/backend/modelsdk/javascript_write_dir_test.go`; verified end-to-end (button click flips the theme instead of throwing)"} -{"area": "mdl/backend", "date": "2026-08-05", "symptom": "`ALTER PAGE … ON ` targeting a DataGrid2 column: (a) the authored MDL column name (`column colFoo`) fails with a bare `widget \"colFoo\" not found`, and (b) a name shared by two columns (duplicate captions) **silently mutates the first** and reports `Altered page`, leaving the second unreachable", "cause": "DataGrid2 columns carry **no stored name** in the Mendix model (their WidgetObject has no `Name` property), so the authored name is dropped on write and mxcli addresses a column by a *derived* name (attribute leaf, else caption, else `col{N}`). The bare-name resolver `findInWidgetArray` returned on the **first** derived-name match, so duplicates collided silently; the miss path emitted a generic \"widget not found\" with no hint that columns use a derived name", "file": "`mdl/backend/pagemutator/mutator.go` — the column loop in `findInWidgetArray` (now counts matches → `bsonWidgetResult.matchCount`), `findBsonColumn` (returns `(result, error)`), and the mutation entry points `SetWidgetProperty`/`DropWidget`/`InsertWidget`/`ReplaceWidget` (reject `matchCount > 1`, use `widgetNotFoundError` which lists derived names)", "insight": "Persisting the authored name is **not possible** — columns have no name slot, and inventing one is the Studio-Pro-won't-open hazard (ADR-0005 guard-don't-drop). So implement the finding's two fallbacks: reject an ambiguous `ON ` with an actionable error instead of silently taking the first (a *data hazard*, not just a wart), and on a miss list the addressable derived names + explain the derived-name model (run DESCRIBE PAGE). **Generalisable**: a resolver that returns the first of N matches hides ambiguity — count matches and reject >1 at every *mutating* entry point, not just the read path. Repro `mdl-examples/bug-tests/ledger-78-datagrid-column-addressing.mdl`; A/B: pre-fix binary reports `Altered page` for the duplicate-caption `ON \"Amount\"`, fixed errors with \"ambiguous\". Ledger #78", "refs": ["#78"]} -{"area": "mdl/backend", "date": "2026-08-07", "symptom": "`alter page … set Editable = [expr]` (or `set Visible`) writes a project Studio Pro refuses to open: `StorageLoadException: Conditional editability settings has an invalid value '' for property Attribute`. `mxcli check` ✓ and `mx check` ✓ — neither inspects the stored value. The identical settings written by `create page` load fine", "cause": "The ALTER path builds the `Forms$Conditional{Visibility,Editability}Settings` node by hand and wrote `Attribute: null`. `Attribute` is a **BY_NAME** `AttributeIdentifier`, so its unset value is the empty string, not null — exactly what the CREATE path already encodes via `codec.RegisterTypeDefaults(..., EmptyStringFields: []string{\"Attribute\"})`, whose comment records this same StorageLoadException from #627. Only the hand-built ALTER node missed it", "file": "`mdl/backend/pagemutator/mutator.go` (`setWidgetConditionalSettingMut`)", "insight": "Write `{Key: \"Attribute\", Value: \"\"}`, not `nil`. **General rule: when one path hand-builds BSON that another path builds through the codec, diff the two encodings rather than eyeballing the hand-built one** — `mxcli bson dump --type page --object M.P` on a CREATE-authored and an ALTER-authored widget makes the divergence a one-line diff (key sets and values were otherwise identical). `SourceVariable` stays `nil`: it is BY_ID, where null *is* the absent value, so \"null is wrong\" is per-field, not a blanket rule. Test `TestSetWidgetConditionalSetting_AttributeIsEmptyString`; repro `mdl-examples/bug-tests/851-alter-page-conditional-attribute.mdl`. Issue #851", "refs": ["#627", "#851"]} -{"area": "mdl/backend", "date": "2026-08-07", "symptom": "`download file $Doc;` is accepted by `mxcli check` and `mxcli exec` (\"Created microflow\") but the activity lands with **no action at all** — `describe` renders `-- Empty action` and `mx check` fails `[CE0008] \"No action defined.\"`. Same for `download file $Doc show in browser;`", "cause": "`microflowActionToGen` (the modelsdk write path) had no `*microflows.DownloadFileAction` case, so it hit `default: return nil` and the enclosing ActionActivity was serialized with a nil Action. Grammar, visitor, flow builder, read path and DESCRIBE formatter were all already in place, so the statement passed every stage that reports anything and vanished at the one that does not", "file": "`mdl/backend/modelsdk/microflow_write.go` (`microflowActionToGen`)", "insight": "Add the case, setting `FileDocumentVariableName`, `ShowFileInBrowser` and `ErrorHandlingType` (Rollback default). **The storage key is `ShowFileInBrowser`, not `ShowInBrowser`** — the gen setter binds the right one; legacy's `parseDownloadFileAction` reads the wrong key. **Test at the round trip, not the reader**: a reader-only test starts from BSON the writer never had to produce, so `TestActionFromGen_DownloadFile` was green throughout. `roundTripMicroflow` (model→gen→codec→model) is the harness; assert the ActionActivity's `Action` is non-nil, which is the CE0008 shape itself. This is the same silent-drop mechanism as the `microflowObjectToGen` default branch (#791) — when auditing, diff the write switch's cases against `sdk/mpr/writer_microflow_actions.go`. Test `TestMicroflowRoundTrip_DownloadFile`; repro `mdl-examples/bug-tests/850-download-file-action.mdl`. Issue #850", "refs": ["#791", "#850"], "ce": ["CE0008"]} -{"area": "mdl/backend", "date": "2026-08-08", "symptom": "A microflow using `split type` writes a project mxbuild cannot **load**: `KeyNotFoundException: The given key '' was not present in the dictionary` at `StreamingBsonUnitReader.ResolvePostponedProperties`. `mxcli check` ✓ and `mxcli exec` ✓; reproduced on 11.6.6 and 11.13.0", "cause": "Two gaps in the modelsdk writer, both the #791 shape. (1) `microflowObjectToGen` had no `*microflows.InheritanceSplit` case → `default: return nil`, so the split was dropped while three sequence flows kept pointing at its `$ID`. (2) `caseValueToGen` had no `InheritanceCase` case → every branch degraded to a bare `Microflows$NoCase`, losing the entity it selects on. Its value-receiver normalisation also omitted the type, so a pointer-only fix would still miss half the calls", "file": "`mdl/backend/modelsdk/microflow_write.go` (`microflowObjectToGen`, `caseValueToGen`) — mirror `sdk/mpr/writer_microflow.go`", "insight": "Add both cases. **Diagnose with the #791 recipe**: `mxcli bson dump --type microflow`, collect every `$ID`, check each key ending in `Pointer` resolves (before: 27 objects / 3 dangling; after: 28 / 0). **Take field lists from the GENERATED type, not from legacy** — legacy writes `ErrorHandlingType` on the split but `initInheritanceSplit` has no such property, i.e. legacy writes a field Mendix does not define. **When adding a case-value type, update the value-receiver normalisation too.** Modelling rules confirmed on both versions while verifying: a type split needs an outgoing flow for every type INCLUDING the base (CE0090), and an `else` does NOT substitute for the base-type case. Tests `TestMicroflowRoundTrip_InheritanceSplit`, `TestCaseValueToGen_InheritanceCase{,ValueReceiver}`; repro `mdl-examples/bug-tests/split-type-dangling-pointer.mdl`", "refs": ["#791"], "ce": ["CE0090"]} -{"area": "mdl/backend", "date": "2026-08-08", "symptom": "`alter page … set on ` reports `widget \"X\" not found` when the widget sits inside a datagrid column rendered as `customContent`; only CREATE OR REPLACE PAGE can touch it", "cause": "`findInWidgetChildren`'s pluggable branch searched the grid's own `Object.Properties[].Value.Widgets` and matched columns by derived name, but never descended into a COLUMN's own content. Columns live at `Object.Properties[columns].Value.Objects[]`; their widgets are one level deeper at `Properties[content].Value.Widgets[]`", "file": "`mdl/backend/pagemutator/mutator.go` (`findInWidgetChildren`, the columns loop)", "insight": "Descend into each column's `Properties[].Value` and reuse `findInWidgetArray(…, \"Widgets\", name)`. **Address by the nested widget's OWN name, not a `grid.column.widget` path**: DataGrid2 columns carry no stored name in the MPR (see `findBsonColumn`), so a column segment could only be a derived name that changes when the caption is edited — a path that goes silently stale. Keep the existing column-by-derived-name lookup working (a test pins it, or the descent could shadow it). Test `TestFindBsonWidget_InsideCustomContentColumn`; repro `mdl-examples/bug-tests/834-alter-customcontent-column-widget.mdl`. Issue #834", "refs": ["#834"]} -{"area": "mdl/backend", "date": "2026-08-08", "symptom": "`alter page … set Caption = '…' on ` fails with `widget has no Caption property` — for EVERY action button, nested or top-level", "cause": "An ActionButton has no `Caption` document: its caption is a `Forms$ClientTemplate` under **`CaptionTemplate`** (Template → Items[] → Translation.Text), the same shape `setWidgetContentMut` already walked for `Content`. `setWidgetCaptionMut` only looked for `Caption`", "file": "`mdl/backend/pagemutator/mutator.go` (`setWidgetCaptionMut`, `setClientTemplateText`)", "insight": "Fall back to `CaptionTemplate` via a shared `setClientTemplateText` helper, which `setWidgetContentMut` now uses too. **Found while fixing #834 — check whether a symptom reproduces OUTSIDE the reported context before attributing it**: this failed on a top-level button as well, so it was a second, independent defect and the #834 finder fix alone would not have made the reporter's command work. When a widget property will not set, dump the widget's key list (`mxcli bson dump --type page`) before assuming the setter is wired — the field is often stored under a different, template-shaped key", "refs": ["#834"]} -{"area": "mdl/backend", "date": "2026-08-08", "symptom": "`create or modify external entity Mod.E (… Countable: false)` — touching only an entity-level property — detonates every attribute: `[CE6612] \"Attribute 'circuitId' of external entity 'Stg_Circuit' is not supported.\"`, one per attribute, leaving a project that cannot build", "cause": "Not the executor: it already preserves attributes it was not asked to change (`if len(attrs) > 0`). One layer down, `attributeFromGen` handled `StoredValue` and `OqlViewValue` but **not** `Rest$ODataMappedValue`, so every attribute of an external entity read back with no `RemoteName`, and the writer's `isExternal && a.RemoteName != \"\"` arm then fell through to a plain StoredValue on the next read-modify-write", "file": "`mdl/backend/modelsdk/domainmodel.go` (`attributeFromGen` gains the `ODataMappedValue` / `ODataMappedPrimitiveCollectionValue` arms)", "insight": "**The attribute-level half of #782**, which fixed the entity level and stopped there — when a read-modify-write loses data, check every *nesting level* of the read, not just the one named in the report. A polymorphic `Value` switch that silently ignores a variant is the shape to look for: it compiles, it reads, and it drops. Reproduce with a **local metadata file** (`MetadataUrl: './contract.xml'`) — no server needed, and the import is the same code path. Also learned here: the reported attribute *rename* (`name` → `Stg_Circuitname`) is a different thing entirely — it happens at import, from `reservedEntityAttrNames`, and `name` is **not** actually reserved (verified: Mendix builds an external entity with an attribute literally named `name`). Tests `external_entity_read_test.go`. mxcli-formula1 #25", "refs": ["#25", "#782"], "ce": ["CE6612"]} -{"area": "mdl/backend", "date": "2026-08-09", "symptom": "Re-running `create external entities from` adds two more associations every time — `season_2`, `season_3`, then `season_4`, `season_5`, … unbounded. `mx check` is clean, every test passes, and the only symptom is duplicate links in Studio Pro's domain model", "cause": "The re-import dedup matches an existing association by the OData **nav-property** name, but association names are unique per MODULE, so the second entity with a `season` nav property is stored as `season_2` — which can never match `season`. The nav-property index that would have matched it is keyed on `RemoteParentNavigationProperty`, and the **modelsdk reader never read the OData source back**, so that index was always empty under the default engine. The legacy reader did read it, which is why this only bit the default path", "file": "`mdl/backend/modelsdk/domainmodel.go` (`assocFromGen` now reads `*genRest.ODataRemoteAssociationSource`), `mdl/executor/cmd_contract.go` (`indexExistingAssociations`)", "insight": "**A field that the write path sets and the read path drops survives exactly one save.** Grep both directions when a round-trip matters — `RemoteParentNavigationProperty` appeared in `domainmodel_write.go`, in gen, and in the legacy parser, and was missing only from the modelsdk read, which is invisible if you only grep for the identifier. **Do not \"fix\" this by stripping a trailing `_` from names**: that heuristic also swallows a user's genuine `season_2`, and the model already records the true origin. Damage is cumulative and pre-existing: a project that ran the import N times carries 2N spurious associations, and they cannot simply be deleted — external-entity access rules reference them, so removing one leaves CE1613 \"The selected association no longer exists\". Tests `mdl/backend/modelsdk/association_odata_source_test.go` (round-trip, plus a control that a plain association is not stamped as external) and `mdl/executor/cmd_contract_reimport_test.go` (the index, incl. the assertion that the suffixed ones match via the nav index and NOT the name index — otherwise the guard passes for the wrong reason). mxcli-formula1 §50", "ce": ["CE1613"]} -{"area": "mdl/backend", "date": "2026-08-10", "symptom": "DESCRIBE renders an activity as `-- Empty action` and its entire error branch is missing from the output — the MDL stays syntactically valid, nothing is printed to stderr, and a describe→edit→exec cycle deletes both", "cause": "Two unrelated defects stacked. (a) The modelsdk reader — the DEFAULT engine — had no case for the action's `$Type`, so `ActionActivity.Action` was nil and `formatAction(nil)` returned the anonymous comment; 27 of 58 `MicroflowAction` types were in that state, 12 of them ones the legacy parser handles. (b) `emitActivityStatement` returned as soon as the statement started with `--`, *before* consulting `errorHandlerFlow`, so the branch was never traversed — for **every** unmapped action, not just the reported one", "file": "`mdl/backend/modelsdk/microflow.go` (nil → `UnsupportedAction` stand-in carrying `$Type` + `ErrorHandlingType`), `mdl/backend/modelsdk/microflow_read_actions.go` (`errorHandlingTypeOf` reads the field by property name), `mdl/executor/cmd_microflows_show_helpers.go` (`emitCommentedErrorHandler`), `mdl/executor/cmd_microflows_format_action.go`", "insight": "**Count the gap before fixing the instance** — decoding each `$Type` through the real reader turned \"SynchronizeAction is unsupported\" into \"27 types are, and `ExecuteDatabaseQueryAction` is a shipped feature you cannot describe back\". **The early return is load-bearing**: annotations before a line comment make exec fail `no viable alternative at input '@position...'`, so keep it and emit the branch **commented-out** — emitting it live is worse than dropping it, since the next exec runs those activities unconditionally in the main flow instead of on failure. **Never take an enum from the UI wording**: Studio Pro says \"Selected object(s)\", the model stores `Specific` (Model SDK `SynchronizationType`, fetched via `npm pack mendixmodelsdk` — authoritative when `reference/` is not vendored). **A Mendix array's element 0 is a version marker, not data**: `property.EnumList.BSONValue()` returns a bare `[]string`, so a one-element list read back empty and mxbuild said CE2004 \"No variables to synchronize have been selected.\" with the value plainly in the dump. Also CE0011 if a custom error handler has no outgoing error flow — call `finishCustomErrorHandler`. Tests `cmd_microflows_unsupported_action_test.go`, `microflow_synchronize_test.go`; example `mdl-examples/bug-tests/863-synchronize-and-unsupported-actions.mdl`. upstream #863", "refs": ["#863"], "ce": ["CE0011", "CE2004"]} -{"area": "mdl/backend", "date": "2026-08-10", "symptom": "`execute database query` is written correctly and builds, but DESCRIBE renders it as `-- Empty action` (or, after #863, `-- Unsupported action: DatabaseConnector$ExecuteDatabaseQueryAction`) — a shipped feature you can author and never get back", "cause": "The modelsdk reader (the DEFAULT engine) had no case for it. Both the MDL grammar and the DESCRIBE formatter were already there; only `actionFromGen` was missing, so `ActionActivity.Action` came back nil", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`case *genDb.ExecuteDatabaseQueryAction`, plus `rawDocElements` for versioned arrays)", "insight": "**The `$Type` is `DatabaseConnector$ExecuteDatabaseQueryAction`, not `Microflows$`** — several actions live in their own sub-metamodel, so a probe under the wrong prefix proves nothing either way and a `case \\*genMf\\.` grep under-counts what is mapped. **Read from raw against the keys the WRITER builds** (`Query` / `DynamicQuery` / `OutputVariableName` / the two mapping lists): the writer constructs the element directly rather than through gen setters, and gen exposes the named query as `QueryQualifiedName`, so pinning reader and writer to one key set is what makes the round trip mean anything. **Element 0 of a versioned array is the marker, not a mapping** — skip non-documents rather than slicing `[1:]`. **A text-stable round trip is not a valid model**: `describe→exec→describe` diffed clean while `mx check` died at load, because the fixture's `create database connection … username 'app'` wrote a literal into a ConstantIdentifier property (`StorageLoadException`, still-open defect, same class as #854) — always run the build, not just the diff. Tests `microflow_dbquery_test.go`, example `mdl-examples/bug-tests/863-execute-database-query-describe.mdl`. upstream #863 follow-up", "refs": ["#854", "#863"]} -{"area": "mdl/backend", "date": "2026-08-11", "symptom": "`call workflow`, `get workflow data/workflows/activity records`, `open`/`lock`/`unlock workflow` and `workflow operation` all DESCRIBE as a placeholder, so a describe→edit→exec cycle deletes them — while authoring and building them works fine", "cause": "The modelsdk reader (the DEFAULT engine) had no case for any of the eight; each read back with a nil Action. The formatters and grammar were already there, so only `actionFromGen` was missing", "file": "New `mdl/backend/modelsdk/microflow_workflow_read.go` (+ dispatch in `microflow_read_actions.go`), `mdl/grammar/domains/MDLMicroflow.g4` + `mdl/visitor/visitor_microflow_workflow.go` (positional `call workflow` form), `mdl/executor/cmd_microflows_builder_workflow.go` (abort reason)", "insight": "**Read against the WRITER's keys, not gen's** — gen binds every \"get\" action's result as `VariableName` while the model stores `OutputVariableName`, so an accessor-based reader silently drops the variable; round-trip tests (not reader-only tests on hand-written BSON) are what catch it. **`WorkflowSelection` has two variants and the object form uses `WorkflowDefinitionVariable`**, not `WorkflowVariable`; its ABSENCE is meaningful (the all-workflows case), so do not synthesise one. **Dispatch the operation on `$Type` before reading fields** — only Abort carries a Reason. **Finishing the reader exposes what could not be seen while nothing rendered**: `call workflow` described to a positional form the grammar rejected (the model never stores the parameter NAME, so DESCRIBE cannot emit the named form), and the abort reason stored the expression *including its quotes* into a StringTemplate Text, so Mendix rendered the quotes at runtime and every round trip doubled them. **Still open**: `lock/unlock workflow all` writes an activity mxbuild rejects with CE1825 — a lock always needs a specific definition, and supplying an empty selection does not satisfy it. Tests `microflow_workflow_read_test.go`, example `mdl-examples/bug-tests/workflow-actions-describe.mdl`", "ce": ["CE1825"]} -{"area": "mdl/backend", "date": "2026-08-11", "symptom": "`transform $In with Module.Transformer` reports \"Created microflow\" and the build then fails `[CE0008] \"No action defined.\"` — the activity is in the model with no action inside it", "cause": "The modelsdk writer (the DEFAULT engine) had no `TransformJsonAction` case, so it fell through to `default: return nil`. The grammar, builder, DESCRIBE formatter and the LEGACY writer all handled it, which is why it looked supported. The reader was missing too, so even a correctly written action described as a placeholder", "file": "`mdl/backend/modelsdk/microflow_write.go` (writer case), `mdl/backend/modelsdk/microflow_read_actions.go` (reader cases for `TransformJsonAction`, `CallExternalAction`, `RestOperationCallAction`)", "insight": "**A missing WRITER case and a missing READER case present identically in a coverage audit** — grepping for reader cases found `transform` \"authorable but unreadable\" when it was actually unwritable, a strictly worse bug. Check both directions before sizing the work. **Mirror the legacy serializer for keys** (`InputVariableName`/`OutputVariableName`/`Transformation`); for the other two, note `CallExternalAction` stores its result under `VariableName` (not `ResultVariableName`) and REST's two mapping lists are NOT symmetric — the query list keys its name as `QueryParameter`. **Do not reconstruct `CallExternalAction.ResultDataType`**: it is resolved from the consumed service's cached `$metadata` at write time, so reading it back would let a stale value round-trip as if authored. **CE0008 turning into CE1613 is progress, not a new bug** — the action now exists and the build has moved on to validating its reference. Tests `microflow_integration_actions_test.go`, example `mdl-examples/bug-tests/transform-json-write-and-describe.mdl`", "ce": ["CE0008", "CE1613"]} -{"area": "mdl/backend", "date": "2026-08-11", "symptom": "`describe import mapping M.X` / `describe export mapping M.X` reports \"not found\" for a mapping that exists and that `show` lists. `create or modify json structure` silently creates a duplicate instead of modifying the existing one. All of them work if the document happens to sit directly under the module rather than in a folder", "cause": "`Backend.moduleNameFor` resolved a unit's module by reading its **direct** container (`GetModule(u.ContainerID)`). Studio Pro nests documents in folders — and folders in folders — so for any foldered document the container is a `Projects$Folder`, `GetModule` returns nil, the name resolves to `\"\"`, and every `…ByQualifiedName` lookup built on it misses. In a stock marketplace project this is the *normal* case, not an edge case: 15 of 16 microflows, both JSON structures and both mappings were foldered, while all 8 domain models sat directly under their module — which is exactly why the three domain-model callers worked and hid the defect", "file": "`mdl/backend/modelsdk/domainmodel_write.go` (`moduleNameFor`); callers in `mapping_write.go`, `json_write.go`, `microflow.go`, `domainmodel_alter.go`", "insight": "Walk the containment chain to the first enclosing module instead of reading one level. Build the parent map once (the old code re-scanned `ListUnits()` per call, inside a loop over every document — O(n²)), and **guard the walk against cycles**: the project root is its own container, so an unguarded loop hangs. Behaviour is unchanged for direct children, so the domain-model callers are unaffected. **Generalisable — the shape to look for**: a containment lookup that reads one level where the model allows arbitrary nesting. It works on every fixture built by `create module` + `create X` (nothing is foldered) and fails on every real project, so unit tests pass while users see \"not found\". **Diagnostic trap**: `describeImportMapping` wraps any error containing \"not found\" in `mdlerrors.NewNotFound`, so the surfaced message is identical whichever layer failed and cannot be used to identify the engine — call the reader and the backend directly to tell them apart. The legacy `sdk/mpr` reader walks the chain correctly (`buildContainerModuleNameMap`), so **the two engines disagreed** and only the default (modelsdk) was broken. Test `mdl/backend/modelsdk/module_resolution_test.go`"} -{"area": "mdl/backend", "date": "2026-08-12", "symptom": "`alter entity … add attribute` on an entity that has a **RegEx or Range** validation rule silently turns that rule into a **Required** rule: the regex reference (or the min/max bounds) is gone, the field is merely mandatory, and `mx check` reports nothing — a Required rule is perfectly valid. Happens on BOTH engines", "cause": "A matched pair of fallbacks. The modelsdk READER (`ruleTypeFromGen`) collapsed every RuleInfo except Unique to `\"Required\"`, and BOTH writers (`ruleInfoToGen`, `serializeRuleInfo`) fell back to `RequiredRuleInfo` for any type they did not recognise. ALTER ENTITY round-trips the whole entity through read→write, so the two defaults compose into a silent downgrade", "file": "`mdl/backend/modelsdk/domainmodel.go` (`ruleTypeFromGen` now derives the name from `$Type`), `mdl/backend/modelsdk/domainmodel_write.go` + `sdk/mpr/writer_domainmodel.go` (both rule-info serializers return **nil = refusal**, plus `reproducibleRuleType`/`validationRulesAreReproducible`), guards in both `UpdateEntity` implementations", "insight": "**A read default and a write default are individually defensible and jointly destructive** — neither function looks wrong alone; the corruption only exists in the round trip. When auditing a rewrite path, check the reader's fallback and the writer's fallback TOGETHER. **`mx check` is not a safety net for a type substitution**: both rules are valid, so the build stays green and the only evidence is the stored `$Type`. **A fourth gen-vs-reality key mismatch**: `modelsdk/gen` binds `RegExRuleInfo`'s reference to BSON `RegularExpression`, while the Studio Pro document (Mendix Email Connector 6.4.2) stores `RegExIdentifier` — so RegEx rules cannot be written through gen at all today, which is *why* the fix is a refusal rather than a round trip. There is no `supplements.json` or `cmd/modelsdk-codegen` in this tree, so correcting gen is not currently available. **Injection fixtures need ORDERED BSON**: re-marshalling a unit through `bson.M` randomises key order and Mendix refuses to load it (`Expected '$ID' as the first property of a storage object`) — use `bson.D` throughout, or the fixture's own invalidity masks the thing you are testing. **Required/Unique rules were never affected**, and are authorable today via the `not null error '…'` / `unique error '…'` attribute constraints — only the standalone `create validation rule` statement is dead (grammar with no AST, visitor or handler). Tests `sdk/mpr/writer_validationrule_test.go`, `mdl/backend/modelsdk/validationrule_test.go`"} -{"area": "mdl/backend", "date": "2026-08-11", "symptom": "`ALTER PAGE` over `--mcp` fails against Studio Pro **11.13** with `pg_patch_page: … PROP_NOT_PRIMITIVE: Property 'widgets' is not a primitive property`. `CREATE PAGE` is fine; the page itself is left intact", "cause": "11.13 gave `pg_read_page` a **`depth` argument defaulting to 4**, replacing anything deeper with the literal string `\"...\"`. ALTER PAGE is read-modify-**replace-whole-page**, so the truncated read went straight back as the new page body. Measured live: `Administration.Account_Overview` read 32,594 bytes at full depth but **1,052 bytes** at the default, its entire tree reduced to `{\"widgets\":[\"...\",\"...\"]}`. Every ordinary page truncates — three of three PgTest pages did", "file": "`mdl/backend/mcp/page.go` (`pgReadPage`, `pgReadFullDepth`, `hasTruncationSentinel`), `mdl/backend/mcp/client.go` (`SupportsToolArg`)", "insight": "Request the full depth, and **guard rather than trust it**: refuse a read still carrying the sentinel instead of letting a partial page reach a write (ADR-0005 guard-don't-drop). Two traps. (1) **Do not send `depth` unconditionally** — 11.11/11.12 declare `pg_read_page` `additionalProperties:false` without it, so the whole call fails; gate on a live `tools/list` probe of the tool's input schema, because `serverInfo.version` is frozen at `1.0.0` across 11.11/11.12/11.13 and cannot discriminate releases. (2) **Match the sentinel only as an array element** — a caption or title legitimately reading `\"...\"` is real content, and a naive substring scan rejects valid pages. The release notes announced none of this, exactly as 11.12 silently removed `pg_write_page` (#697): on any Studio Pro upgrade, re-probe `tools/list` and diff the input schemas, not just the tool names. Tests `mdl/backend/mcp/page_depth_test.go`; controls: stub the depth arg (full-depth test fails) and stub the guard (truncation test fails)", "refs": ["#697"]} -{"area": "mdl/backend", "date": "2026-08-12", "symptom": "A `call java action` whose parameter is a **microflow** (`MCPServer.AddTool`'s `ExecutingMicroflow`, and every other \"register a callback\" action) fails `mx check` with **CE0115** — the microflow's name is stored as a literal string", "cause": "The modelsdk engine's java-action read had no case for the parameter type, so `codeActionBasicFromGen` fell through to its `default` and returned a `StringType`. The microflow builder's `microflowTypeParams` branch was therefore never taken and it authored `Microflows$BasicCodeActionParameterValue{Argument: \"'M.MyFlow'\"}` instead of `Microflows$MicroflowParameterValue{Microflow: \"M.MyFlow\"}`", "file": "`mdl/backend/modelsdk/java_read.go` (`codeActionBasicFromGen`), `java_write.go` (`codeActionParamTypeToGen`), `mdl/executor/cmd_javaactions.go` (`astDataTypeToJavaActionParamType`, `bareDataTypeName`)", "insight": "**A `default:` that returns a plausible type is how a read path lies.** The legacy parser and the executor both handled this correctly for years; only the gen→semantic converter did not, and it degraded to String rather than erroring — so every layer downstream looked correct in isolation. **Fix read and write together**: without the write case an *update* of such an action rewrites the parameter as a String, which is worse than the read bug. **Watch what DESCRIBE starts printing** — the fix made it emit the honest `Microflow`, which then round-tripped into an entity type with an empty module (`.Microflow`) until `astDataTypeToJavaActionParamType` learned the bare word. Probe the stored `$Type` from the marketplace `.mpk` before theorising; MCP Server 5.1.0 stores `JavaActions$MicroflowJavaActionParameterType`. Tests `mdl/backend/modelsdk/java_read_test.go`, `mdl/executor/cmd_javaactions_test.go`, example `mdl-examples/bug-tests/javaaction-microflow-parameter.mdl`; verified 0 errors under `mx check` 11.12.1. Reported in mxcli-chat FINDINGS §36", "ce": ["CE0115"]} -{"area": "mdl/backend", "date": "2026-08-13", "symptom": "`GRANT ON Module.Specialization (READ *, WRITE *)` writes a model Mendix rejects with **CE0066** \"Entity access is out of date\", while the same grant on the generalization checks clean. The executor passes 3 MemberAccess entries and storage holds 2", "cause": "`ReconcileMemberAccesses` — which runs after every program and after every `create association` — recomputes each rule's expected member set from the entity's **own** attributes and the module's **FROM-side** associations. An inherited association matched neither, so it was classed stale and deleted, one step after the GRANT that had just written it correctly", "file": "`mdl/backend/modelsdk/domainmodel_security_write.go` (`sameModuleAncestors`, `entitiesByName`, `assocRefBelongsTo`, `ownerIDs` in `ReconcileMemberAccesses`), `mdl/executor/cmd_associations.go` (reconcile after DROP)", "insight": "**The write path was innocent** — `AddEntityAccessRule` stored all 3; the deletion happened in the reconcile that ran next. When a value is written and then absent, instrument the *later* pass before the writer. **Preserve what cannot be checked**, as the attribute branch already did (#758): an association is qualified by the module that DECLARES it, so an ancestor in another module (the reported `OpenAIDeployedModel extends GenAICommons.DeployedModel`) names a domain model that is not loaded here. **The counter-control is the whole test**: a walk that collected every association in the module passes both positive cases and puts an entry on the TO side of an `OWNER Default` association, which is *itself* CE0066 — so assert that the unrelated entity does **not** get one. **Reconcile must add as well as keep**, or a rule written before the ancestor gained the association never catches up. Verifying this surfaced a neighbouring bug: `drop association` never reconciled at all, leaving entries Mendix rejects with CE1613 — on the declaring entity too, so it predates inheritance support. Tests `mdl/backend/modelsdk/reconcile_inherited_assoc_test.go` (5 cases incl. both controls), `mdl/executor/cmd_associations_mock_test.go`; example `mdl-examples/bug-tests/grant-on-specialization.mdl`, measured 1 error → 0 on mxbuild 11.12.1. Reported in mxcli-chat FINDINGS §25", "refs": ["#758"], "ce": ["CE0066", "CE1613"]} -{"area": "mdl/backend", "date": "2026-08-14", "symptom": "Authoring a pluggable widget property that lives in a **conditionally shown group** writes a widget Mendix rejects with **CE0463**, while the same widget's other properties take non-default values happily. On ProgressCircle both `showLabel: true` and `labelType: 'percentage'` do it; `showLabel: false` and General-group properties are clean. No DESCRIBE involved", "cause": "Two gaps on the same axis. **Serialization**: #574 nulls the TextTemplate of a HIDDEN conditional property, but left a VISIBLE one null — and Mendix stores an empty `Forms$ClientTemplate` there. Null and empty are each invalid in the other's state, so nulling hidden ones was only half the rule. **Extraction**: the editorConfig reader did not understand a ternary's ELSE branch (`cond ? (…) : hidePropertiesIn([…])`), so the `showLabel` gate was never seen and `labelText` read as visible whenever `labelType` was `\"text\"` — its default", "file": "`mdl/backend/widgetobj/builder.go` (`ApplyVisibilityRules`, `bsonFieldIsNil`), `mdl/executor/editorconfig_extract.go` (`parseGuard` `:` case, `ternaryCondition`, `trailingExpr`)", "insight": "**Fixing one gap alone inverts the bug rather than closing it** — filling visible templates without the missing gate made `showLabel: false` fail where `true` had, because mxcli still thought labelText was visible. Measured both ways round before and after; a fix that moves which case fails is not a fix. **Let Mendix say what the shape should be**: `mx update-widgets` on a COPY of the failing project reconciles the widget, and diffing that against mxcli's output named the single meaningful path (`Object/Properties[N]/Value/TextTemplate` null vs `Forms$ClientTemplate`) out of 969. **The good/bad control does the isolation for free**: authoring the same widget with the boolean both ways gave two documents differing in exactly ONE path, so no other candidate needed testing. **Only CONDITIONAL properties are filled** — Studio Pro's convention for an unset TextTemplate is not uniform (a DataGrid custom-content column stores null for `tooltip` and an empty template for `exportValue`, per `emptyClientTemplateRules`), so filling every unset one would trade this bug for its mirror image. **The extractor's preamble matters**: the ternary is preceded by a whole `switch`, and walking back past the `?` to the function start yields a fragment with an unbalanced `}` that parses to nothing and looks like \"unsupported shape\" — hence `trailingExpr`. Regression signal: the widgetdemo showcase applies with **0 CE0463** (its 4 CE1613 are a pre-existing attribute reference). Tests `mdl/backend/widgetobj/widget_visibility_test.go`, `mdl/executor/editorconfig_extract_test.go`; example `mdl-examples/bug-tests/pluggable-describe-roundtrip.mdl` now exercises the group. Reported in mxcli-ledger FINDINGS #104 follow-on", "refs": ["#104", "#574"], "ce": ["CE0463", "CE1613"]} -{"area": "mdl/backend", "date": "2026-08-14", "symptom": "`ALTER PAGE` **`REPLACE `** or **`INSERT AFTER `** naming a DataGrid2 column with a BARE name reports \"Altered page\", but the column vanishes (REPLACE) or nothing changes (INSERT) — and Studio Pro / `mx check` then cannot **LOAD** the project: `System.InvalidCastException: Unable to cast object of type '…LayoutWidgets.DivContainers.DivContainer' to type '…CustomWidgets.WidgetObject'`", "cause": "`ast.WidgetRef` treats only a **dotted** name as a column, so a bare `NextRunAt` gives `Widget=\"NextRunAt\"`, `Column=\"\"`, `IsColumn()==false`. The op skipped the `InsertColumns`/`ReplaceColumn` paths — which work correctly — and fell through to the generic widget path, which built the `COLUMN` as a **layout container** and wrote it into the grid's column list. Nothing refused because `findBsonWidget` **recurses into pluggable-widget internals**, so the bare name *did* resolve — to the column node. `DESCRIBE PAGE` then skipped the malformed node, which is what made REPLACE look like a clean deletion and INSERT like a harmless no-op", "file": "`mdl/backend/pagemutator/mutator.go` (`refuseObjectListItemTarget`, called from `InsertWidget` + `ReplaceWidget`)", "insight": "Discriminate on the resolved node's **`$Type`**: an object-list item (DataGrid2 column, Accordion group, PopupMenu basicItem) is `CustomWidgets$WidgetObject`, a real widget is `Forms$*` / `CustomWidgets$CustomWidget`. Refuse the bare form and name the qualified `grid.column` in the message — do **not** auto-resolve, since guessing which grid was meant is what produced the invalid document. Do not discriminate by *counting* name matches: `columnMatchCount` already existed but only refused when `n > 1`, so the single-match case (the common one) sailed through, and a widget that merely shares a name with some column elsewhere would be a false positive. Guard belongs in the **mutator**, not the executor — `FindWidget` matches columns too, so an executor-level check on it only catches names that are absent entirely, which the mutator already refuses. Repro `mdl-examples/bug-tests/891-alter-page-bare-column-target.fail.mdl`. Issue #891", "refs": ["#891"]} -{"area": "mdl/backend", "date": "2026-08-18", "symptom": "`ALTER PAGE SET DynamicCellClass` / `SET Visible` on a DataGrid 2 column reports success and changes nothing visible: the value does not come back from `describe page`, and `mx check` stays at 0 errors", "cause": "`setColumnPropertyMut` wrote every value to `PrimitiveValue`. `columnClass` and `visible` are **Expression**-valued, and a `CustomWidgets$WidgetValue` carries *every* field at once — Expression, PrimitiveValue, TextTemplate, AttributeRef — with the unused ones empty, so key-presence cannot tell you which one to write and the always-primitive default looked plausible. The value landed in a field Mendix does not read", "file": "`mdl/backend/pagemutator/mutator.go` (`columnValueField`, `buildColumnPropKeyMap` now also returns kinds, `bsonWidgetResult.colPropKinds`)", "insight": "Take the kind from the schema — the column's `PropertyTypes[].ValueType.Type` — and write Expression, PrimitiveValue or the TextTemplate path accordingly; refuse the structured kinds (Attribute, DataSource, Action, Widgets) rather than writing a string where a reference belongs. Verified on Mendix 11.13.0: before, the write was invisible to DESCRIBE and `mx check` said 0 errors; after, it round-trips and mxbuild **validates** it — a valid expression builds at 0 errors and a bare identifier is CE0117, matching what CREATE produces for the same input. **Generalisable**: when a container type carries every variant field at once, presence is not a discriminator — the schema is. A silent wrong write is worse than the loud failure it was hiding behind", "ce": ["CE0117"]} -{"area": "mdl/backend", "date": "2026-08-17", "symptom": "Authoring a pluggable widget with an **object list** (Accordion group, Pop-up menu item, chart series) raises **CE0463** \"the definition of this widget has changed\" on the widget itself, on a widget created seconds ago against the current package", "cause": "An object-list item's **required TextTemplate** that the author left unset was serialized as `null`. `emptyClientTemplateRules` in `widgetobj/builder.go` is a hardcoded per-widget table covering only DataGrid columns, so every other object-list widget fell through it. Required-ness is genuinely absent from the widget XML for these properties — and the widget schema **defaults `required` to true** — so the Accordion's `headerText` is mandatory even though nothing says so", "file": "`mdl/backend/widgetobj/builder.go` (`isUnsetRequiredTextTemplate`, `buildDefaultTextClientTemplateProperty`), plus the entry plumbing in `mdl/types/widget_property_type.go`, `modelsdk/widgets/loader.go`, `sdk/pages/pages_widgets_advanced.go`, `mdl/backend/modelsdk/widget_pluggable_write.go` (`convertPropTypeIDs`)", "insight": "Serialize a required unset TextTemplate with the widget's **shipped ``**, read off the template ValueType's `Translations` beside the `Required` flag. **Both weaker forms were measured and fail**: `null` is CE0463, and an *empty* `Forms$ClientTemplate` is **CE4899** \"Property 'Groups/1/Text' is required\" — so the intuitive \"emit an empty template\" fix only moves the error. Populating is what `mx update-widgets` itself writes (verified: mxcli and the reference now emit the same `'Header'`/`'Koptekst'`). Scope it to **required** properties: filling every TextTemplate is the documented way to take CE0463 from 33 to 127. Note `PropertyTypeIDEntry` exists in THREE places (`mdl/types` — canonical and aliased by `modelsdk/widgets`; `sdk/pages` — what the builder consumes; `sdk/widgets` — the legacy engine's own), so a field added for this must be carried through `convertPropTypeIDs` or it silently never arrives. Repro `mdl-examples/bug-tests/891-accordion-group-nested-widgets.mdl`. Issue #891", "refs": ["#891"], "ce": ["CE0463", "CE4899"]} -{"area": "mdl/backend", "date": "2026-08-18", "symptom": "`describe database connection` omits `returns` / `map (…)` / `parameter` on the **default (modelsdk) engine** while the legacy engine renders them, so a describe → exec round trip drops the return entity, the column mapping and every query parameter", "cause": "The modelsdk reader populated neither `TableMappings` nor `Parameters`, so the DESCRIBE renderer (which does handle them) only ever saw empty slices — dead code on that engine. The round-trip test existed but was pinned to the legacy backend via `setupTestEnv`, so it passed while the default engine was lossy", "file": "`mdl/backend/modelsdk/integration_read.go` (`tableMappingsFromGen`, `queryParametersFromGen`); test `mdl/executor/roundtrip_dbconnection_test.go`", "insight": "Mirror the legacy reader field for field, and run the round-trip test over `gateEngines` rather than the default backend. **The cross-engine DESCRIBE matrix is the check that finds these**: write with engine A, read with engine B, all four combinations — a gap on one engine is invisible from inside that engine. Fixing one field (TableMappings) and not its sibling (Parameters) is the characteristic mistake; enumerate every child collection of the document before calling it done"} -{"area": "mdl/backend", "date": "2026-08-18", "symptom": "`calculated by Module.Microflow` on an attribute is accepted by `check` and by exec (\"Added attribute\"), but the stored document holds a plain `DomainModels$StoredValue` with no calculation link — the microflow name appears nowhere in the domain model unit, `mx check` reports **0 errors**, and the attribute is simply empty at runtime. Both the CREATE (inline) and ALTER (`ADD`/`MODIFY ATTRIBUTE`) paths. A microflow whose signature cannot work is accepted too, masked by the same drop", "cause": "`attributeToGen` in the **modelsdk** writer had arms for OqlViewValue / ODataMappedValue / ODataMappedPrimitiveCollectionValue and a `default:` that emits StoredValue — no `CalculatedValue` arm — so the binding the executor had already resolved fell through and was discarded. The **legacy** writer had the arm all along (`sdk/mpr/writer_domainmodel.go`), which is why the feature read as implemented; modelsdk is the default engine (`--engine`), so everyone hit the broken path. The reader had no `CalculatedValue` case either, so an unrelated ALTER on the same entity destroyed a binding made in Studio Pro", "file": "`mdl/backend/modelsdk/domainmodel_write.go` (`attributeToGen`) + `domainmodel.go` (`attributeFromGen`) + `mdl/executor/calculated_attributes.go` (`resolveCalculatedValue`, called from the three sites in `cmd_entities.go`)", "insight": "Add the write arm (`genDm.NewCalculatedValue`, `SetMicroflowQualifiedName` → the `Microflow` ByNameRef key, `SetPassEntity`) **and** the read arm — a write-only fix leaves the read-modify-write data loss in place, which is the worse half. Derive `PassEntity` from the signature rather than hardcoding it (legacy hardcoded `microflowRef != \"\"`): measured on 11.13.0, an entity-parameter microflow (`PassEntity=true`) and a parameterless one (`PassEntity=false`) BOTH build at 0 errors, so refusing the parameterless form would have been wrong. Signature rules are refused at exec (the #833 placement), and each was checked against mxbuild rather than assumed — wrong entity parameter and wrong return type are both **CE7247**, but the return-type message is *\"should be Integer/Long\"*, so **Integer and Long are one family** and a strict equality check refuses valid MDL (caught only by reading the CE text). To ask mxbuild about a binding mxcli now refuses, stub the check and rebuild — `--engine legacy` does NOT bypass it, because the validation lives in the engine-independent executor. Tests `TestAttributeToGen_CalculatedValue`, `TestAttributeFromGen_CalculatedValue`, `TestResolveCalculatedValue_*` (the backend three fail with `value is *domainmodels.StoredValue` when reverted); fixture `mdl-examples/bug-tests/917-calculated-attribute-binding.mdl`. Issue #917", "refs": ["#833", "#917"], "ce": ["CE7247"]} -{"area": "mdl/backend", "date": "2026-08-19", "symptom": "`ALTER PAGE` at a **dotted** DataGrid2 column target (`insert after grid1.Col { container … }` / `replace grid1.Col with { container … }`) reports \"Altered page\", and `mx check` then aborts before checking anything: `System.InvalidCastException: Unable to cast object of type '…DivContainers.DivContainer' to type '…CustomWidgets.WidgetObject'`. Separately: a widget **inserted into a customContent cell** binds nothing — `ContentParams` naming an association path stores the raw path as the attribute name (**CE1613**) and `describe page` shows the path with its first hop silently gone", "cause": "Two defects. (a) #891's guard covers only the BARE column form; the dotted form is legitimate and skips the guard entirely, so a **non-`column` body** still fell through to the generic widget path and was serialized into the grid's column list. The wrong thing is the *pairing* (widgets at an object-list target), not the target. (b) The entity-context walk descended into a pluggable widget's own widget properties but not into an object-list ITEM's — a column keeps its cell widgets one level deeper at `Objects[].Properties[content].Value.Widgets`, the same descent `findInWidgetChildren` gained in #834 — and never read a **pluggable** widget's datasource at all, so the grid's own entity was invisible too. Empty entity context ⇒ `resolveAssociationAttributePath` bails ⇒ the raw path is stored as the attribute name", "file": "`mdl/backend/pagemutator/mutator.go` (`refuseWidgetsAtColumnTarget` called from `InsertWidget`/`ReplaceWidget`; `findEntityContextInChildren`, `widgetOwnEntity`, `entityFromEntityRef`)", "insight": "(a) Refuse when `columnRef != \"\"` — resolve the column FIRST so a mistyped name still reports not-found with the available names, then refuse and name both alternatives (`column …` body, or address the widget inside the cell by its own name, per #834). (b) Descend into `Value.Objects[].Properties[].Value.Widgets`, keyed on the BSON shape rather than the `columns` property key so Accordion groups and PopupMenu items are covered by the same code; resolve a pluggable datasource through the shared `entityFromEntityRef`, which also gives it the IndirectEntityRef (association) case the plain reader already had. **Note the guard cannot key on the target alone** the way #891's does, and a unit test on the mutator alone under-proves it: reverting only shows a nil-deref in the fixture. The honest control is the CLI + `mx check` — measured on Mendix 10.24.20.105674, exception before / 0 errors after, and CE1613 before / 0 after. Repros `mdl-examples/bug-tests/935-alter-page-widgets-at-column-target.mdl` and `935-customcontent-column-entity-context.mdl`. Issue #935", "refs": ["#834", "#891", "#935"], "ce": ["CE1613"]} -{"area": "mdl/backend", "date": "2026-08-19", "symptom": "`GRANT ON (READ (A))` **shrinks** an existing access rule instead of widening it: attributes granted by an earlier GRANT come back `None`, and `(CREATE, DELETE, READ *, WRITE *)` followed by a narrow re-grant loses create, delete and every write right. No error, no warning, no diff. Reported as WHERE-specific and as a \"silent security regression\"", "cause": "Two defects. (a) The codec engine's upsert **cleared the matched rule's MemberAccesses and rebuilt them from the current statement alone**. The executor emits an entry for *every* member of the entity (unnamed ones at the rule's default, normally `None`), so each GRANT reset every member it did not mention; `AllowCreate`/`AllowDelete`/default rights were overwritten the same way. Legacy has merged additively since it shipped (`mergeAccessRule`), so this was an **engine regression**, not a longstanding gap — which is why the documented contract (\"GRANT is additive … never removes permissions\") held on one engine and not the default one. (b) **Both** engines matched a stored rule on its module-role set alone, ignoring `XPathConstraint`, collapsing two legitimate rules into one", "file": "`mdl/backend/modelsdk/domainmodel_security_write.go` (`AddEntityAccessRule`); `sdk/mpr/writer_security.go` (the upsert's match + `mergeAccessRule`); lattice shared via `mdl/types.AccessRightsLevel` / `HigherAccessRights`", "insight": "**Measure the reported trigger before scoping the fix to it.** WHERE was a red herring — the same loss reproduced with no WHERE and with `READ *`, so a fix aimed at the constrained path would have passed the reporter's repro and left most of the bug. Equally, the report understated the damage: only attributes were mentioned, but structural rights went too, so re-derive the blast radius rather than inheriting it. **Run the other engine as the control** — `MXCLI_ENGINE=legacy` merging correctly is what turned \"longstanding bug\" into \"regression\" and handed over the reference semantics (OR create/delete, take the higher default, take the higher rights per member) instead of having to invent them. For (b), **check the platform's own docs before calling multi-rule a user error**: Mendix's refguide says \"Rules are additive … all access rights of those rules are combined\", so one role holding one rule per constraint is the normal way to write row-level security, and `mx check` confirms it at 0 errors. Put the constraint in the match key and treat the empty constraint as a *value*, not a wildcard, so an unconstrained and a constrained rule coexist and re-running a script stays idempotent (ADR-0008). **Fixing (b) breaks anything keyed on roles alone**: `formatAccessRuleResult` then echoed a *different* rule's rights back at the user, so it needed the constraint too (REVOKE passes `anyXPath`, since it narrows every matching rule). Rights merge on `None < ReadOnly < ReadWrite`, so a merge never narrows — that is what keeps GRANT and REVOKE inverses rather than two spellings of \"set\". Issue #936", "refs": ["#936"]} -{"area": "mdl/backend", "date": "2026-08-20", "symptom": "`ALTER STYLING … SET '' = ''` prints `Updated styling on widget \"x\"` and writes **nothing**. The same statement works on a Studio Pro-authored widget, and the inline `DesignProperties: [...]` form works everywhere", "cause": "Two defects composing into a silent no-op. `bsonnav.DSet` takes a `bson.D` **by value**, so it can only overwrite an existing key — it returns `false` and does nothing when the key is absent, and `DSetArray` ignored that result. And the key WAS absent: the codec omits an empty, never-appended `PartList`, so `Forms$Appearance` on an mxcli-authored widget carried no `DesignProperties` at all (Studio Pro writes the empty marker `[3]` on all 16 pages of a blank app)", "file": "`mdl/backend/bsonnav/bsonnav.go` (`DSetArray` now returns bool + refuses a markerless write, new `DSetArrayIn`); `mdl/backend/pagemutator/mutator.go` (`writeDesignProperties`); `mdl/backend/modelsdk/widget_write.go` (`Forms$Appearance` MandatoryLists, `CustomWidgets$CustomWidget` LabelTemplate)", "insight": "**Fixing the append without fixing the marker turns a silent no-op into an unopenable project.** A Mendix array's first entry is its typed-array marker; measured on 11.13, a `DesignProperties` array written bare fails to LOAD (`Type OptionDesignPropertyValue does not contain a constructor with a parameter of type Appearance`) and `create-module-package` dies with \"Unknown export error\", while a doubled marker throws `StorageLoadException`. So `DSetArray` refuses rather than guessing, and the marker is passed explicitly at the one call site that creates the property. **Diff against `mx update-widgets`' own output** to find missing keys: handing an mxcli-authored page to Mendix's tool and diffing the unit it writes back (pointer identity preserved, key order intact) surfaced `DesignProperties`, `LabelTemplate` and a set of int32/int64 width mismatches in one pass. Not changed: `Forms$Page.AllowedModuleRoles` is marker 3 where Studio Pro writes 1 — `NewByNameRefListV3` documents CE0557 as the reason, and nothing currently misbehaves. Issue mendixlabs/mxcli#931", "refs": ["mendixlabs/mxcli#931"], "ce": ["CE0557"]} -{"area": "mdl/backend", "date": "2026-08-21", "symptom": "A microflow that reads `if Module.SomeRule(param = $x) then` is written with **no Condition at all** — `mx check` reports **CE0080** \"The 'Condition' property is required.\" naming the Decision, whose caption still shows the call. `DESCRIBE` renders it as `if true then`, so the round trip looks like valid MDL, and re-applying an identical `CREATE OR MODIFY` over a Studio Pro-authored microflow turns a clean project into a broken one", "cause": "Two defects, one behind the other. (a) `splitConditionToGen` handled only `ExpressionSplitCondition` and fell through to `default: return nil`; the caller then skipped `SetSplitCondition` entirely. The READ side had been implemented for #723, and that asymmetry is what made it silent. (b) `modelsdk/gen` binds `Microflows$RuleCall.Rule` to BSON key `Rule`, where Mendix stores **`Microflow`** (rules share the microflow namespace) — so fixing (a) alone writes a reference Mendix never reads and produces the same CE0080 for a different reason", "file": "`mdl/backend/modelsdk/microflow_write.go` (`splitConditionToGen`, new `ruleCallToGen`, the `Microflows$RuleCall` marker registration); `modelsdk/gen/microflows/types.go` (`initRuleCall` + `RuleCall.InitFromRaw` — STORAGE-NAME OVERRIDE on both sides); strike the row off `modelsdk/gen/keyaudit_test.go`; tests `mdl/backend/modelsdk/microflow_rule_split_write_test.go`; repro `mdl-examples/bug-tests/939-rule-split-condition.mdl`", "insight": "**\"Works on Mendix 10, fails on 11\" was an ENGINE difference, not a version one** — same script, same project, `MXCLI_ENGINE=legacy` is 0 errors and the default (modelsdk) is CE0080. The microflow write path branches only on `major >= 10` / `<= 9`, so 10.24 and 11.12 take identical code; check the engine before believing a version story. **A round-trip test cannot see a wrong storage key** — it passes whichever key is used as long as both sides agree, which is exactly the shape of (b) — so assert the KEY on the encoded document, in a separate test with its own control. **Reconstruct the missing document type when the fixture does not exist**: MDL cannot author rules and no module in a blank Mendix 11 app ships one, so the repro rewrote an mxcli-created microflow's unit as `Microflows$Rule` (keeping only the ten properties `initRule` declares) — `mx check` 0 errors on the synthetic rule is the control that it is a valid stand-in. Verify the fix against the legacy engine's BSON, not just against a green build: the two documents are identical modulo key order. Issue mendixlabs/mxcli#939", "refs": ["#723", "mendixlabs/mxcli#939"], "ce": ["CE0080"]} -{"area": "mdl/backend", "date": "2026-08-21", "symptom": "`ALTER WORKFLOW` over `--mcp` inserts a `CALL MICROFLOW` and Studio Pro shows **CE0495 \"Duplicate name\"** on the workflow, while `mxcli`'s own read-back looks fine and the statement reported success", "cause": "A `CALL MICROFLOW` activity is named after its target microflow (`buildCallMicroflowTask`: `task.Name = n.Microflow.Name`), so two calls to the same microflow collide before anything inspects them. The file backends deduplicate in `wfmutator`; the MCP backend had no equivalent and sent the derived name verbatim. Two separate gaps, both needed: PED **accepts** the duplicate with `SUCCESS` and does **not** auto-rename (measured live), and `mcpWorkflowMutator.Save()` early-returned when only activity ops had run, so `ped_check_errors` — the only thing that sees a consistency error — was never called. `CREATE`/`CREATE OR REPLACE` are **not** affected: the executor dedups at `cmd_workflows_write.go` for every backend", "file": "`mdl/backend/wfnames/` (shared policy); `mdl/backend/mcp/workflow.go` (`resolve`/`searchActivities` collect names on the walk, `Save`/`checkSettled`); `mdl/backend/wfmutator/mutator.go` (`serializeAndDedup`, `buildSubFlowBson`)", "insight": "Deduplicate **before the add** — PED refuses `set` on an activity's `/name` (`Element type does not support renaming`), so there is no repair after the fact. Collect the taken-set on the walk `resolve` already does, or the fix costs a PED round-trip per level. **Use a taken-set, not a seen-count**: counting re-collides the moment the workflow already contains the suffixed name (`A, A, A_2` counts its way to `A, A_2, A_2`). Uniqueness is workflow-**wide**, so recurse into the inserted activity's own outcome and boundary-event sub-flows. **`ped_update_document` reporting `SUCCESS` proves nothing about consistency** — it reports op-level failures only; `ped_check_errors` is the only signal, and it reads Studio Pro's **background** error list, which lags the write in BOTH directions (26 samples at a 20ms poll interval: error becomes visible 77-115ms after the write, stops being shown 75-128ms after the fix; one symmetric debounce, no growth under load). Asked too early it answers \"No errors found.\" for a document just broken **and** still reports an error a just-applied op has already cleared — measured against an immediate check as the control, 6 trials each: immediate missed the real error 6/6 and falsely failed on a cleared transient 6/6, settled 0/6 on both. `Backend.pedCheckDocument` owns the pacing for every call site (delay, then re-ask while clean) so a new write path cannot forget it. **Poll at a fine interval when measuring a debounce** — a first pass at 50-100ms reported 170-350ms and \"grows under load\", both pure granularity artifacts. Issue #945", "refs": ["#945"], "ce": ["CE0495"]} -{"area": "mdl/backend", "date": "2026-08-21", "symptom": "`ALTER WORKFLOW … REPLACE ACTIVITY X WITH ` silently renames it to `X_2`, and re-running the same script compounds it — `X_2_2`, `X_2_2_2`, … A no-op replace (nothing changed) renames too. Invisible to `mxcli check`, `exec` output, and — for `CALL MICROFLOW` steps, whose DESCRIBE renders only the target microflow — to `DESCRIBE` as well", "cause": "The dedup helper was written for INSERT and reused by REPLACE. `collectAllActivityNames()` walks the still-unmodified tree, so the activity about to be spliced out is still in the name pool when the replacement's name is checked against it — a same-name replace therefore always \"collides\" with itself. The MCP backend had the same shape, propagated deliberately in PR #204 on a **guess** that the file backends' choice was intentional", "file": "`mdl/backend/wfmutator/mutator.go` (`serializeAndDedup` gains `freeName`, `ReplaceActivity` passes the outgoing name); `mdl/backend/mcp/workflow.go` (`activityRefMatch.name` / `wfLocation.name`, `delete(loc.taken, loc.name)`); tests `mdl/backend/wfmutator/replace_samename_test.go`, `mdl/backend/mcp/workflow_dedup_test.go`", "insight": "Free **only the outgoing activity's name**, and read it off the **resolved element** — the reference may be a caption, not the name, so using `activityRef` frees the wrong key (or none). Keep a control proving a replacement colliding with a *surviving* activity still dedupes, or the fix over-corrects into CE0495. **Check the severity claim before believing it**: the report argued renames detach in-flight `System.WorkflowUserTask` records, but Mendix's [workflow-versioning docs](https://docs.mendix.com/refguide/workflow-versioning/) list \"changing names, captions, and titles\" as explicitly **non-conflicting** — the engine matches structurally, not by name. The real harm is unbounded accretion and the ADR-0008 idempotence break. **The fix only half-closes that**: a no-op replace still rewrites the `.mpr` because `addFreshPersistentID` re-mints `PersistentId` on every activity write in both engines and neither reads it back (measured: untouched siblings keep theirs, the replaced one does not). Issue #944", "refs": ["#204", "#944"], "ce": ["CE0495"]} -{"area": "mdl/backend", "date": "2026-08-21", "symptom": "`CREATE OR REPLACE WORKFLOW` silently deletes a boundary event (timer + its whole handler flow) or an event sub-process that the script does not restate — `exec` reports \"Created workflow\" and exit 0, and `mx check` afterwards reports 0 errors because the result is a valid workflow that simply no longer does what it did. `DESCRIBE WORKFLOW` does not show the construct either, so nothing reveals the loss", "cause": "Two separate defects. (1) The **default (modelsdk) engine's workflow reader had no boundary-event support at all** — `grep BoundaryEvent mdl/backend/modelsdk/workflow_read.go` → 0, while `sdk/mpr/parser_workflow.go` → 17. Both engines *write* them, so a boundary event mxcli itself had just written read back as absent. (2) A rewrite rebuilds from the statement, so anything unstated is dropped, and no guard existed — unlike the queued-call and validation-rule paths", "file": "`mdl/backend/modelsdk/workflow_read.go` (`boundaryEventsFromGen`, wired into the 6 gen types with `BoundaryEventsItems`); `mdl/executor/validate_workflow_rewrite.go` (`checkNoDroppedWorkflowConstructs`, called from `cmd_workflows_write.go`); `mdl/executor/cmd_workflows.go` (clause order)", "insight": "**Read the stored side from the RAW unit, not the semantic model** — the reader is what was blind, and a guard sharing its blind spot cannot see what it protects; `GetRawUnit` also covers constructs no engine models yet (event sub-processes exist in `modelsdk/gen` but have no semantic type, reader or writer anywhere). Match the `$Type` by **substring** so all three timer variants and any later one are caught. Authorable constructs get the queue treatment (restate them and the rewrite proceeds); unauthorable ones refuse outright. **Fixing a reader can expose a describer bug**: with boundary events finally readable, `describe → exec` stopped parsing, because the call-microflow describer emitted `boundary event` BEFORE `outcomes` and the grammar requires the reverse — pre-existing on legacy, invisible while the default engine emitted neither. Always re-run a describe→re-exec round trip after widening a reader. Issue #948", "refs": ["#948"]} -{"area": "mdl/backend", "date": "2026-08-23", "symptom": "`GRANT … ON Module.Entity` over `--mcp` fails with `failed to read module security for X: GetModuleSecurity: not supported by the MCP backend` — `--mcp-verbose` prints **no PED tool call at all**, and `mxcli mcp capabilities` reports entity access rules as available in the same session. `SHOW MODULE ROLES` / `SHOW USER ROLES` / `SHOW PROJECT SECURITY` fail the same way", "cause": "The MCP backend embeds `unsupportedBackend` and overrides only what it supports; `gen_unsupported.go`'s own header says it delegates \"the reads those [writes] need\" — i.e. a read is added **reactively, per write method**. `AddEntityAccessRule` (#704) shipped and was unit-tested, but the executor calls `validateModuleRole` → `GetModuleSecurity` **before** it, and a pre-check made outside the backend package is invisible from inside it. Three reads were missing, not one", "file": "`mdl/backend/mcp/security.go` (`GetModuleSecurity`, `ListModuleSecurity`, `GetProjectSecurity` → `b.reader`); tests `mdl/backend/mcp/grant_executor_test.go`", "insight": "**Test the statement, not the backend method** — a unit test on the method passes against a completely unreachable feature, which is exactly how this shipped; drive real MDL through `executor.New` + `SetBackendFactory` against `newFakePED` and assert a `ped_update_document` actually happened. **Probe the whole class**: the reported symptom was GRANT, but the same three-line omission had broken every security `SHOW`, none of which writes anything. **The capability report is a hand-written YAML declaration, not a verified claim** — `capabilities.go` says report and behaviour \"cannot drift\", and that holds only for the four *method-gated* keys; `entity_access_rules` is a display-only row, so it can and did contradict the very next command. Routing a read to the local `.mpr` is safe **only when PED is not a writer of that document**: roles are by-name references Studio Pro resolves on apply, so a stale file yields \"module role not found\", never a wrong write — unlike the domain model, which needs the dirty-set router. A module created over PED this session has no security document on disk at all, so answer that case explicitly rather than letting the reader say \"not found for module mcp~module~X\". Issue #900", "refs": ["#704", "#900"]} -{"area": "mdl/backend", "date": "2026-08-24", "symptom": "A pluggable widget's text-template property is authored and **silently dropped** — the document keeps the enumeration that says the text exists but no text, so `tooltipType: 'customText', tooltip: '…'` builds as *\"'Tooltip' cannot be empty when 'Custom' type is chosen\"*. The tell that makes it findable: `tooltip: 'Value {Score}'` WORKS while `tooltip: 'Plain text'` does not", "cause": "`setTextTemplateValue` only ever **updated an existing** TextTemplate and returned the element untouched when it was nil. A CONDITIONAL template is stored nil while its condition hides it (#574), and the Slider's tooltip is hidden under its own default `tooltipType: \"value\"` — so a statement that turns the condition on and writes the text in one go finds nothing to edit. `ApplyVisibilityRules` then fills the now-visible property with an EMPTY template, which is what reaches disk. The `{AttrName}` spelling escapes it only because `SetTextTemplateWithParams` **replaces** the whole template instead of editing it", "file": "`mdl/backend/widgetobj/builder.go` (`setTextTemplateValue`); the params gap it exposed is `mdl/executor/widget_engine.go` (`applyOperation` \"texttemplate\" + `resolveMapping` \"TextTemplate\", `BuildContext.ClientParams`)", "insight": "Build the envelope when the slot is nil (`updateTemplateText(BuildEmptyClientTemplate(), text)`) rather than special-casing visibility — `ApplyVisibilityRules` still runs afterwards and re-nulls a genuinely pruned property, so authored text can never survive into a slot the editor hides. **Studio Pro is the tool that localises this**: `pg_read_page` shows the stored template as a well-formed, EMPTY `Pages$ClientTemplate`, which distinguishes \"no text was written\" from \"the template is malformed\" — the BSON cannot. Two traps. Fixing the drop makes the **`{1}` spelling reachable**, and the MAPPING pass ignored `contentparams:` where the explicit-property pass has routed it since #928, so the newly-landing text hit CE0720 until both passes agreed. And **DESCRIBE still does not emit any text-template property for a pluggable widget** (`extractExplicitProperties` reads AttributeRef and PrimitiveValue only), so a tooltip survives a write and is lost on the next describe→exec — measured on the Timeline control too, i.e. every widget, tracked separately. Repro `mdl-examples/bug-tests/254-slider-custom-tooltip.mdl`. Issue #254", "refs": ["#254", "#574", "#928"], "ce": ["CE0720"]} -{"area": "mdl/backend", "date": "2026-08-25", "symptom": "`describe microflow` prints `range($List)` for a range that stores an offset and an amount — both expressions dropped, no warning, exit 0, and `mxcli check` clean on the truncated output, which re-executes as an *unpaged* range. Mirror-image on the write side: `range($L, $o, $a)` authored through mxcli builds as **CE6520** *\"Amount and offset are not specified\"*, and on `--engine legacy` the project will not **load** at all (*\"Expected '$ID' as the first property of a storage object, but got 'NewOperation'\"*)", "cause": "Mendix nests the bounds one level below where mxcli looked: `Microflows$ListRange{ListName, CustomRange}` → `Microflows$CustomRange{LimitExpression, OffsetExpression}`. The modelsdk engine had them **flat on both sides** — `listOperationToGen` wrote them onto the ListRange and `listOperationFromRaw` read them from there — so mxcli round-tripped its own documents perfectly and only Mendix disagreed. The legacy engine had no `ListRangeOperation` case at all, fell through to `return nil`, and `serializeListOperationAction` appended that nil under `NewOperation`, writing an **empty sub-document** that Mendix's loader rejects outright", "file": "`mdl/backend/modelsdk/microflow_write.go` (`listOperationToGen`), `mdl/backend/modelsdk/microflow_read_actions.go` (`listOperationFromRaw`), `sdk/mpr/writer_microflow_actions.go` (`serializeListOperation` + the nil guard in `serializeListOperationAction`), `mdl/executor/validate_microflow.go` (`checkRangeHasABound`, MDL068); tests `mdl/backend/modelsdk/microflow_listrange_test.go`, `sdk/mpr/writer_listoperation_test.go`, `mdl/executor/validate_range_bounds_test.go`; repro `mdl-examples/bug-tests/list-range-custom-bounds.mdl`", "insight": "**A clean round trip proves nothing when one engine wrote the document.** Both sides being wrong in the same direction is what hid this for a release: the reporter concluded \"the authoring side works correctly\" from exactly that evidence. Read a document Studio Pro wrote, or cross-read with the *other* engine — here the legacy PARSER had been right all along, so `--engine legacy` on a default-engine project was a free control. **`generated/metamodel` settles the nesting**: a `*Microflows$X` pointer field is a child element, a plain string is a property. When a writer's `default:` returns nil, check what the CALLER does with it — appending nil is how an unloadable file gets written, and omitting the key (CE0008, which names the activity) is always the better failure. The reader keeps a **flat fallback** so a project written by the broken writer still describes its bounds and repairs itself on the next `exec` — verified: CE6520 → describe → exec → 0 errors", "ce": ["CE0008", "CE6520"], "rules": ["MDL068"]} -{"area": "mdl/backend", "date": "2026-08-26", "symptom": "Every layout reports `Layout Type: Responsive` — including the Phone, Tablet, ModalPopup and native ones, 19 of the 22 Atlas ships", "cause": "Two faults compounding. `LayoutType` is **not on `Forms$Layout`**; it is on the content wrapper. `modelsdk/gen` nonetheless exposes `Layout.LayoutType()`, bound to a key the document does not carry, so it reads `\"\"` — a phantom property, not a wrong key, which is why `keyaudit_test.go` does not list it. `DESCRIBE LAYOUT` then defaulted `\"\"` to `\"Responsive\"`, rendering a failed read as a fact", "file": "`mdl/backend/modelsdk/page.go` (`layoutTypeOf` reads the wrapper), `mdl/executor/cmd_pages_describe.go` (no fallback)", "insight": "**Never default a value every real document carries** — an empty one means the read failed, and a plausible default is what hides it. `generated/metamodel` was right about the location all along (property of `PagesWebLayoutContent`/`PagesNativeLayoutContent`), so the arbiter rule held; what no source got right was the *value set*, six values splitting by platform (web: Responsive/Phone/Tablet/ModalPopup, native: Default/Popup) against two declared in gen and in the metamodel enum. Measured across all 22"} -{"area": "mdl/backend", "date": "2026-08-26", "symptom": "A layout mxcli wrote builds cleanly (`mx check`: 0 errors) but Studio Pro refuses to open the project: `System.InvalidOperationException: Sequence contains no matching element` at `MprProperty.cs`", "cause": "The layout carried `MainPlaceholderName`. `modelsdk/gen` declares it on `Layout` so the setter compiles, and mxbuild tolerates unknown properties — but `generated/metamodel`'s `PagesLayout` declares only eight properties and none of the 22 layouts Atlas ships on 11.13.0 carries the key. Studio Pro resolves every stored property against the type's list", "file": "`mdl/backend/modelsdk/layout_write.go` (`layoutToGen`), `mdl/executor/cmd_pages_layout_v3.go` (the header refuses any property but `layouttype`)", "insight": "**A `Forms$Layout` has exactly ten keys** — `$ID`, `$Type`, `Appearance`, `CanvasHeight`, `CanvasWidth`, `Content`, `Documentation`, `Excluded`, `ExportLevel`, `Name` — measured identical across all 22. Which placeholder is \"main\" is a *naming convention*: every Atlas layout names one `Main`, and a page binds to a placeholder by qualified name anyway, so the document has nothing to say. gen's whole placeholder family (`MainPlaceholder`, `AcceptButtonPlaceholder`, `UseMainPlaceholderForPopups`, …) is phantom here. `mx check` is not the control for this class — the key set is. Pinned by `TestLayoutToGen_WritesExactlyTheKeysStudioProWrites`"} -{"area": "mdl/backend", "date": "2026-08-26", "symptom": "`ALTER PAGE … SET Layout` / `ALTER LAYOUT` cannot find any widget in a layout — \"widget X not found\" for a widget DESCRIBE LAYOUT clearly shows", "cause": "Two finder gaps compounding. `findBsonWidget` starts from the page's `FormCall`, which a `Forms$Layout` does not have (its tree hangs off `Content`), so every widget in every layout was unreachable; and `findInWidgetChildren` never descended into a `ScrollContainer`'s five named slots, so a layout's topbar (`Top`) and navigation (`Left`) were out of reach even once the root was found — as was anything inside a scroll container a *page* places", "file": "`mdl/backend/pagemutator/mutator.go` (`findBsonWidgetInLayout`, region descent in `findInWidgetChildren`), `mdl/backend/pagemutator/scrollregion.go`", "insight": "**A region is addressed by slot, not by name** — it has no `Name`, so `INSERT INTO layoutContainer.top` reuses the dotted widgetRef that also serves DataGrid2 columns, and which one it means is decided by the named widget's `$Type`. Only `INSERT INTO` takes a region: BEFORE/AFTER position a widget among siblings, and silently treating them as INTO would put widgets somewhere the script did not ask for. The MDL slot name is `center`; the stored key is `CenterRegion`. Controlled by emptying the slot list and by respelling the centre key"} -{"area": "mdl/backend", "date": "2026-08-27", "symptom": "No export mapping mxcli writes matches its Studio Pro original, so every one stays in #260's silent-loss set even once its source kind is authorable", "cause": "The export writers hardcoded three properties the IMPORT twin already read off the element: `MinOccurs 0` (a schema root has **1**), `MaxLength 0`, and `IsKey` not written at all. `MaxLength` is the one that cannot be a constant — Studio Pro stores **0 for a string element and -1 for a numeric one**, mirroring the bound schema element exactly as `MaxOccurs` already did", "file": "`mdl/backend/modelsdk/mapping_write.go` (`exportValueElementToGen`, `exportObjectElementToGen`) + `mapping_read.go`, `sdk/mpr/writer_export_mapping.go`, `modelsdk/mpr/serialize_mappings.go`, `mdl/executor/cmd_export_mappings.go`, `model.ExportMappingElement`", "insight": "The export element model had **none** of these fields and the reader populated none of them, so this is four layers (model, reader, builder, three writers), not a writer patch — and a writer patch alone would have written zeros from an empty model. Clone them from the schema element where the builder already clones `MaxOccurs`. Check the import twin first when an export property looks wrong: this was a divergence between the two writers, not a considered decision, and the import side is the correct reference. Repro `mdl-examples/bug-tests/mapping-277-export-property-set.mdl`, tests `sdk/mpr/writer_export_mapping_properties_test.go`. Issue ako/mxcli#277", "refs": ["#260", "ako/mxcli#277"]} -{"area": "mdl/backend", "date": "2026-08-27", "symptom": "A rebuilt mapping drops `MessageDefinition2`, so a document written by Studio Pro 11.10+ never round-trips", "cause": "The key is version-introduced — `modelsdk/gen/mappings/version.go` records `messageDefinition2` as `Introduced: \"11.10.0\"` — and gen generates **no accessor** for it, so nothing read or wrote it. A blank 11.13 app's own mappings carry it as `\"\"`; none of the older pinned fixtures has it at all", "file": "`model.ImportMapping`/`ExportMapping` (`MessageDefinition2 *string`), `mdl/backend/modelsdk/mapping_read.go` (`messageDefinition2FromRaw`) + `mapping_write.go`, `sdk/mpr/parser_*_mapping.go` + `writer_*_mapping.go`, `mdl/executor/cmd_import_mappings.go` + `cmd_export_mappings.go`", "insight": "**Carry it, do not derive it.** A pointer, because nil (absent) is NOT the same as present-and-empty — writing the key onto a pre-11.10 document is the overlay-rule mistake CLAUDE.md warns about, which mxbuild tolerates and Studio Pro refuses to open. The executor decides: carry `existing.MessageDefinition2` on an update, apply the version gate only on a CREATE where there is nothing to read it off. A plain version gate in the writer looks equivalent and is not — it re-adds the key to every older document a rewrite touches, which turned four previously-clean fixtures red. gen having no accessor means the read goes to raw BSON, the route `parameterEntityFromRaw` takes. **`MappingSourceReference` is the same family and deliberately NOT carried**: its gate (10.16) predates every project in the field, and the codec emits it through a package-level `TypeDefaults` registration, so making it conditional would touch every mapping write to preserve one pre-10.16 fixture. Issue ako/mxcli#279", "refs": ["ako/mxcli#279"]} -{"area": "mdl/backend", "cause": "The **reader**, not the writer: `mdl/backend/modelsdk/navigation_read.go` type-asserted only the `$Type`s `modelsdk/gen` declares for those two slots, and neither is what the documents carry \u2014 `LoginPageSettings` is stored as `Forms$FormSettings` with the page under `Form` (gen expects `Navigation$NavigationProfileLoginFormSettings` / `LoginPage`), and `NotFoundHomepage` as `Navigation$HomePage` (gen and `generated/metamodel` both expect `Navigation$NotFoundHomePage`). A failed type assertion leaves the field empty, so the loss is silent", "date": "2026-09-01", "file": "`mdl/backend/modelsdk/navigation_read.go` (`navLoginPageOf`, `navNotFoundPageOf`), cross-check `generated/metamodel/types.go` `NavigationNavigationProfile`", "insight": "**The other engine is the control.** Legacy read the same bytes correctly throughout, which is what identifies a reader bug: `describe navigation X` on both engines must agree, and a disagreement localises the defect to the one that reads through gen. Accept the `$Type` the documents actually carry and keep gen's as a fallback branch. Note the two slots fail in **opposite directions** and want opposite fixes: for the login page a real Studio Pro document and `generated/metamodel` agree with the writers, so **gen** is wrong; for the not-found page \u2014 Studio Pro's **\"Fallback page\"** \u2014 metamodel and gen agree with each other and the three mxcli **writers** are the odd one out, emitting `Navigation$HomePage` where Studio Pro stores `Navigation$NotFoundHomePage`. ako/TestApp supplied the reference document that settled it. **Correction (2026-09-01): mxbuild does NOT accept either.** Measured on 11.13 against a build emitting the old spelling, `mx check` and `mxbuild --target=deploy` both exit 1 with \"Object of type 'Mendix.Modeler.WebUI.Navigation.HomePage' cannot be converted to type '...NotFoundHomePage'\" -- the project will not LOAD, so every downstream check is lost. What actually let it through is that nothing ever BUILT a project with a fallback page set: the automated mx-check coverage is doctype-tests/ only and no script there sets one, so the first was added by the fix itself. **Generalisable: 'the build tolerates it' is a claim that needs the same control as the fix** -- revert the writer, rebuild, and run the tool, or the reason a bug escaped gets recorded backwards and sends the next reader looking in the wrong place. Keep reading both `$Type`s regardless, but for the repair path rather than round-tripping: a pre-fix project does not build, and mxcli reads BSON directly, so accepting the old spelling is what lets it open and fix one. Repro `mdl-examples/bug-tests/navigation-describe-profile-pages.mdl`", "symptom": "`DESCRIBE NAVIGATION` prints `home page` and the menu but **silently omits `login page` and `not found page`**, so pasting its output back (the documented copy workflow) deletes both from the profile. The clauses are on disk and `MXCLI_ENGINE=legacy` prints them"} -{"area": "mdl/backend", "date": "2026-09-07", "symptom": "CE0066 \"Entity access is out of date\" after `ALTER ENTITY M.Gen ADD ATTRIBUTE ...` where a SPECIALISATION of Gen also has an access rule — and `UPDATE SECURITY`, project-wide or scoped, reported \"All entity access rules are up to date\" and changed nothing (mendixlabs/mxcli#1047, reported against 0.21.0 on MPR v2, reproduced on 11.12.1 on BOTH engines). The reporter's own tool named the one missing member — `+ Spec: ProbeSecond.Gen.AfterSpec (ReadWrite)` — and took mx check 1 -> 0.", "cause": "ReconcileMemberAccesses computes the same-module ancestor set (sameModuleAncestors -> ownerIDs) and then uses it ONLY for the association pass. The attribute pass walked the entity's OWN attributes (ent.AttributesItems() / entityDoc[\"Attributes\"]), so a specialisation's expected member set never contained an inherited attribute: nothing looked missing, nothing was added, and `modified` stayed 0 — which the command prints as \"up to date\". Fixed by walking the chain for attributes too, qualifying each against the entity that DECLARES it, with a nearer entity's attribute shadowing an ancestor's of the same name.", "file": "`mdl/backend/modelsdk/domainmodel_security_write.go` (ReconcileMemberAccesses, collectAttrs) and `sdk/mpr/writer_security.go` (entityAttrsInChain, ownAttrsOf, generalizationRefOf); tests `mdl/backend/modelsdk/reconcile_inherited_attr_test.go`, `sdk/mpr/writer_security_inherited_test.go`; example `mdl-examples/bug-tests/1047-inherited-member-reconcile.mdl`", "insight": "The bug was one line of REUSE that never happened: the ancestor set was already computed two lines above the attribute loop and only the association loop consumed it. When a function computes a set and uses it for one of two symmetric passes, check the other pass. Three things worth carrying: (1) A count that drives a message is part of the contract — reporting 0 modified is what turned a broken model into the words \"All entity access rules are up to date\", and a false success is worse than an error because it ends the investigation. Assert the count, not just the stored state. (2) Legacy keyed coverage by BARE attribute name while preserving any ref not qualified against this entity, so naively adding inherited attributes would have preserved AND re-added the same member — the duplicate only shows on the second reconcile, so the regression test runs it twice. (3) The repair path cannot be proven by an .mdl file once the write path is fixed: ALTER ENTITY reconciles as it writes, so the script ends clean either way. Break the project with a PRE-FIX binary and repair it with the fixed one — that finally showed `update security` taking a real CE0066 from 1 to 0, which an earlier fix had recorded as unproven.", "ce": ["CE0066"]} -{"area": "mdl/backend", "date": "2026-09-08", "symptom": "A property Studio Pro writes on an offline entity config (CompatibilityMode) was read from the model and silently discarded, so any future write path would have dropped it with no error and no mx check failure", "cause": "types.NavOfflineEntity carried three of the four properties Studio Pro actually writes. TestFieldCountDrift, which exists to catch exactly this on hand-copied structs, did not list NavOfflineEntity or NavigationProfile — so adding the field left the guard passing vacuously", "file": "`mdl/types/navigation.go`, `mdl/backend/mpr/convert.go`, `mdl/backend/mpr/convert_roundtrip_test.go`", "insight": "A drift guard is only worth what its list covers, and a guard that passes on a struct it does not know about is worse than none — it reads as coverage. Check the guard names your type before trusting a green run. Also: measure which properties are actually WRITTEN before deciding what to carry — gen declared six here and the reference document had four, with DownloadMode and ShouldDownload occurring zero times, so the risk was inverted from the expected one (writing a property Studio Pro fills in on load, not dropping one)", "refs": ["ako/TestApp", "ako/mxcli#413"]} -{"area": "mdl/backend", "date": "2026-09-08", "symptom": "An offline navigation profile authored by mxcli builds, routes and installs as a PWA, and shows an empty app — every gate green", "cause": "MDL had no syntax for offline synchronization, so a created offline profile got an empty OfflineEntityConfigs list. A Mendix offline profile downloads nothing until each entity has a sync mode; `mx check` reports 0 errors either way because an empty list is valid", "file": "`mdl/grammar/MDLParser.g4` (navSyncDef), `mdl/backend/modelsdk/navigation_write.go`, `sdk/mpr/writer_navigation.go`", "insight": "Creating a document kind is not the same as being able to configure it, and the gap is invisible to every static check — the symptom is an empty screen at runtime. When adding a profile/document kind, ask what makes it DO anything, not just what makes it exist. The write is an overlay keyed by entity so CompatibilityMode (stored, unauthorable) survives; building the element from the spec alone would clear it silently, the access-rule defect again", "refs": ["ako/TestApp", "PROPOSAL_offline_sync_configuration.md"]} -{"area": "mdl/backend", "date": "2026-09-08", "symptom": "`alter page P { set PageSize = 10 on }` errors `pluggable property \"PageSize\" not found` on a grid that `create page \u2026 (PageSize: 20)` had just written and the app really pages at. `mxcli check --references` passes the script, so it fails only at exec, after earlier statements have landed; DESCRIBE PAGE prints the same capitalised `PageSize:`, so describe \u2192 edit \u2192 exec produced a script mxcli refused to run", "cause": "A pluggable property key is lowerCamel in the widget template (`pageSize`). CREATE resolves the author's spelling case-INsensitively (widget engine `lookupProperty`, and `WidgetV3.GetStringProp` before it); ALTER went through `setPluggableWidgetPropertyMut`, which compared the template key byte-for-byte, so only the exact `pageSize` worked. Direct sequel to Findings #1 (2026-07-27), which fixed the same class for FIRST-CLASS props and deliberately left the pluggable fallback case-sensitive with the comment 'template keys must match the template exactly'", "file": "`mdl/backend/pagemutator/mutator.go` (`setPluggableWidgetPropertyMut`)", "insight": "`strings.EqualFold` against the widget's own PropertyTypes. **The disproven belief is the reusable part**: keys are STORED case-sensitively, which is not a reason to MATCH them that way \u2014 the resolver searches one object type's PropertyTypes, and across every shipped template and definition that scope holds no two keys differing only in case (96 scopes, 1208 keys, 0 collisions). Measure the ambiguity before assuming it; here there was none, and the assumption cost a whole verb. **Cheapest localiser**: run the failing statement with the template's own casing \u2014 `set pageSize` succeeded where `set PageSize` failed, in ONE measurement, on the same widget in the same project. Both engines share `pagemutator`, so an engine split says nothing here (verified: modelsdk and legacy both fixed by the one change). Tests `TestSetPluggableProperty_MatchesTemplateKeyRegardlessOfCase` (+ typo-still-errors control, + `TestPluggablePropertyKeysAreUniqueIgnoringCase` pinning the no-collision argument); repro `mdl-examples/bug-tests/alter-page-pluggable-property-casing.mdl`; verified 0 errors on `mx check` 11.13.0. Two reporter claims did NOT hold: `describe page` DOES emit PageSize, but only when it differs from the widget default 20 (deliberate, so describe round-trips) \u2014 at the default it is omitted, which reads as 'describe cannot show it'. Still open and separate: `check --references` does not resolve pluggable property names at all, so a genuine typo (`PagSize`) still checks clean and fails at exec", "refs": ["mendixlabs/mxcli#1069"]} -{"area": "mdl/backend", "date": "2026-09-09", "symptom": "A control that should have proven a guard was load-bearing PASSED with the guard removed — the test could not distinguish a correct writer from one that reset the property on every write", "cause": "The fixture stored `false` for a boolean property, and false is also the zero value. A writer that ignored the spec entirely wrote false; the correct writer preserved false. Identical output, so the assertion held either way", "file": "`mdl/backend/modelsdk/navigation_throw_sync_test.go`", "insight": "For a BOOLEAN property, a preservation test must exercise BOTH stored values — the non-zero one is the only case that can fail. This is the second time in one feature: CompatibilityMode needed a synthetic `true` because all seven reference configs carry false. The generalisation: when every real document agrees on a value, the fixture drawn from real documents cannot test preservation, and a synthetic counter-case is not optional. The tell is a control that fails to fail — if stubbing the guard leaves the suite green, the test is measuring nothing, and that is worse than no test because it reads as coverage", "refs": ["ako/mxcli#413", "ThrowPartialSyncError"]} -{"area": "mdl/backend", "date": "2026-09-09", "symptom": "`call web service` (legacy SOAP) on the DEFAULT engine: `mxcli exec 06b-soap-examples.mdl` reported success on all three microflows and `mx check` (11.13.0) then failed the project with `[CE0008] \"No action defined.\" at Action activity 'Activity'` plus two `[CE0109] \"Undefined variable 'Root'.\"`. Only reachable by rerunning with MXCLI_ENGINE=legacy, which is why legacy was still the documented fallback", "cause": "The codec engine READ the action (`actionFromGen` \u2192 `*microflows.WebServiceCallAction`, with a raw fallback) but `microflowActionToGen` had no case for it, so it hit `default: return nil` and the enclosing ActionActivity serialized with no action at all. The #850 shape: a missing WRITE case is not an unsupported feature, it is a silent drop that exec reports as success. The CE0109s are knock-on \u2014 the dropped action never bound $Root", "file": "`mdl/backend/modelsdk/microflow_webservice_write.go` (new), `microflow_write.go` (switch case)", "insight": "**Mirror the legacy serializer, and prove it by diffing the two engines' documents \u2014 do not re-derive the shape from the metamodel.** There is no Studio Pro-authored SOAP document in this repo, so legacy's output is the only reference that exists and is also what users' projects already contain. Method: exec the same script on each engine, `mxcli bson dump` both, normalise the random $IDs, diff. Three discrepancies fell out that no amount of reading would have shown, all in how the codec emits NULLS and MARKERS: (1) a Part property with no child encodes to nil and the encoder then SKIPS the key (`if val != nil`), so an unset part is an ABSENT key, never a null one \u2014 carry the null as a primitive `bson.Null{}` value instead, which marshals in place; (2) `codec.TypeDefaults.NullFields` does emit the key but APPENDS it after every property, so it cannot reproduce alphabetical key order; (3) both NullFields and list markers are registered per `$Type` and several types are SHARED between writers \u2014 `Microflows$HttpConfiguration` needs HttpHeaderEntries marker 3 for SOAP and 2 for REST, and legacy writes CustomLocationTemplate as null for SOAP but omits it for REST, so a global registration would have silently changed the REST path. Write those explicitly per call site. (The package already carries one such collision: `Microflows$HttpHeaderEntry` is registered 2 in microflow_write.go and 3 in odata_write.go, decided by file order.) Verified: the two engines' CallWebServiceAction documents are now identical key-for-key and value-for-value; `mx check` goes 3 structural errors \u2192 0, leaving only the script's 4 deliberate CE1613 dangling refs, which is exactly legacy's output. Revert control: removing the switch case fails all six unit tests with the CE0008 message. The doctype engineScriptSkip for modelsdk/06b was removed and the script now runs on BOTH engines", "refs": []} -{"area": "mdl/backend", "date": "2026-09-10", "symptom": "`mxcli diff-local` on an MPR v2 project fails with `Error: mprcontents directory not found` while mprcontents/ exists and is populated; `MXCLI_ENGINE=legacy` works", "cause": "The modelsdk engine (the default) never overrode `Backend.ContentsDir()`, so it fell through to the generated `unimplemented` stub and returned \"\". diff-local reads \"\" as 'not a v2 project'.", "file": "mdl/backend/modelsdk/backend.go", "insight": "gen_unimplemented.go's promise that an unoverridden method 'fails loudly rather than silently dropping data' is CONDITIONAL on the method having an error to fail through: the generated body is `errUnimplemented` only when a result is `error`, a panic when there are no results at all, and a silent `var r0 T; return r0` otherwise. ContentsDir is in the third bucket and its zero value is a MEANINGFUL in-band answer (\"\" == MPR v1), so the missing implementation was indistinguishable from a v1 project rather than looking like a bug. The detectable signature was the contradiction between two questions the same command asks: Version() (implemented) says 2, ContentsDir() says v1. Guard added in mdl/backend/modelsdk/unimplemented_silent_test.go \u2014 reflect over FullBackend for error-less methods, go/parser the package for methods actually declared on *Backend, since reflection cannot tell a promoted method from an override (Go synthesises a wrapper named (*Backend).X for both). It immediately found a second one, InvalidateCache (a latent panic, no caller today).", "refs": ["mendixlabs/mxcli#1080"]} -{"area": "mdl/backend", "date": "2026-09-10", "symptom": "SOAP `call web service` writes a document mxbuild accepts and Studio Pro would not have written. A SEND MAPPING is silently DROPPED by both engines \u2014 `send mapping Mod.Export` parses, `mxcli check` passes, `exec` reports success, and nothing in the stored action references the mapping. Operation ARGUMENTS are dropped the same way", "cause": "sdk/mpr.serializeWebServiceCallAction was written without a Studio Pro reference and hardcodes five things it cannot know, and the codec engine's new writer reproduced it deliberately for parity. Measured against three Studio Pro-authored calls in ako/TestApp (Mendix 11.14.0, Clients.GetOrders / GetCustomerOrders / SaveOrder): ServiceName is the WSDL SERVICE name (\"OrdersWS\") not the local part of the imported service's qualified name (\"OrderSoapClient\"); ImportMappingCall.ContentType is \"Xml\" for a SOAP import mapping, not \"Json\"; Range.SingleObject follows cardinality (false for a list) rather than being always true; VariableType is the real result type (DataTypes$ObjectType with an Entity, DataTypes$BooleanType) rather than always DataTypes$VoidType; and a send mapping is Microflows$MappingRequestHandling {ContentType, MappingId, MappingVariableName}. Arguments live in RequestBodyHandling.ParameterMappings as Microflows$WebServiceOperationSimpleParameterMapping entries keyed by an escaped ParameterPath (\"http%3A//www.example.com/:GetOrder|OrderId\")", "file": "`sdk/mpr/writer_microflow_actions.go` (serializeWebServiceCallAction), `mdl/backend/modelsdk/microflow_webservice_write.go`", "insight": "**A guessed type name in a comment becomes a permanent refusal.** Legacy refused send mappings citing `Mendix$AdvancedRequestHandling`, said it 'requires a Studio Pro-generated example to determine the correct type storage name', and that refusal then shipped for as long as nobody went looking. The real type is `Microflows$MappingRequestHandling` \u2014 which THIS CODEBASE ALREADY WRITES for REST result/request handling \u2014 and the guessed name occurs in none of the three reference documents. The lesson is not about SOAP: when a writer refuses because a storage name is unknown, check whether a sibling feature already writes it before treating the refusal as a standing constraint. **Second, and the reason this was found at all: 'no reference exists' is a claim about where you looked.** The parity work asserted that no Studio Pro-authored SOAP document existed to pin against and used that to justify mirroring legacy; one existed in a separate repo the whole time (ako/TestApp, which carries both a consumed client and a published service). Byte-parity with what ships is a legitimate goal for a change scoped to stopping a silent drop \u2014 it is NOT evidence the shape is right, and conflating the two is how six defects got a passing test. Where a reference project exists, name it in the code so the next reader does not repeat the search", "refs": []} -{"area": "mdl/backend", "date": "2026-09-11", "symptom": "Making SOAP calls describe structurally instead of as base64 silently rewrote Studio Pro's own documents: a describe -> exec round trip over ako/TestApp flipped `Range.SingleObject` false -> true with no error at all, and turned Clients.SaveOrder's `DataTypes$BooleanType` result into VoidType, which mxbuild reported as `[CE0366]` + `[CE6011]`", "cause": "`webServiceActionRequiresRawBSON` admitted keys BY NAME. That was sound while the supported set was the only nine keys the writer emitted \u2014 but a real call carries FIFTEEN, and six of the new ones are boilerplate mxcli writes at ONE fixed value. Admitting `HttpConfiguration`, `RequestHeaderHandling`, `IsValidationRequired`, `ProxyConfiguration`, `RequestProxyType` and `NewResultHandling` by name meant any call configured beyond mxcli's defaults would be normalised on the next exec", "file": "`mdl/backend/modelsdk/microflow_read_actions.go`, `sdk/mpr/parser_microflow_actions.go` (webServiceActionRequiresRawBSON and its value predicates)", "insight": "**The question a raw-fallback gate answers is not 'do I know this key' but 'would writing this back produce the same document'.** The two coincide only while the writer emits exactly the supported set; the moment a feature lands that widens what is representable, the by-name test starts approving documents it cannot reproduce \u2014 and the loss is invisible, because the result is a VALID model that differs from the user's. **The round trip is the only thing that catches it**: unit tests on the new feature all passed, `mx check` on the newly-written calls was 0 errors, and the regression only appeared when describe -> exec was run over the REFERENCE documents and the BSON diffed (`mxcli bson dump --type microflow --object`, ids normalised away). Two of the five diffs that surfaced were pre-existing microflow describe drift (NoCase case values, bezier control vectors) and unrelated \u2014 worth separating before blaming the change. Fixing it also SHRANK the feature's reach honestly: Studio Pro's own SOAP calls keep the raw form until `Range.SingleObject` is explained, and only mxcli-authored calls describe structurally. **A result type can come from somewhere MDL cannot see** \u2014 SaveOrder's Boolean is the WSDL operation's return type, not an import mapping's entity \u2014 so 'binds a result' does not imply 'derivable'", "refs": []} -{"area": "mdl/backend", "date": "2026-09-11", "symptom": "`describe microflow` -> `exec` \u2014 the documented copy operation \u2014 silently turns OFF a microflow's \"apply entity access\". Measured across 342 microflows in 4 projects (11.14.0): every microflow storing `ApplyEntityAccess: true` came back `false` (16/342, all 4 distinct Administration documents). A blocking `show message` also becomes non-blocking (16 microflows). `mxcli check` and mxbuild are both silent \u2014 the model is valid either way", "cause": "TWO causes wearing one symptom. (1) `ApplyEntityAccess` is HARDCODED false in both writers (`mdl/backend/modelsdk/microflow_write.go` SetApplyEntityAccess(false), `sdk/mpr/writer_microflow.go` {Key:\"ApplyEntityAccess\", Value:false}) and `microflows.Microflow` has no field for it, so the read side drops it first. (2) `ShowMessageAction.Blocking` is carried correctly end to end on BOTH engines \u2014 the loss is in DESCRIBE, which has no `blocking` keyword to emit, so the re-parse sets false", "file": "`mdl/backend/modelsdk/microflow_write.go`, `sdk/mpr/writer_microflow.go`, `sdk/microflows/microflows.go` (Microflow struct), `mdl/executor/cmd_microflows_format_action.go` (show message)", "insight": "**A round-trip audit needs a preservation control, or it cannot tell a bug from its own blind spot.** Here it was `StableId`: 342/342 preserved, exactly as ADR-0008 claims \u2014 a method that normalised ids away too aggressively would have reported that as churn, and everything else with it. **Separate `hardcoded in the writer` from `unspellable in DESCRIBE`**: they look identical in a before/after diff and need completely different fixes (model plumbing vs grammar), and `Blocking` proves a property can be perfectly carried by both engines and still be lost by the text round trip. **The same property handled two ways in one codebase is the tell**: `microflows.Rule` carries ApplyEntityAccess correctly (`rule_write.go`, `parser_rule.go`) while `microflows.Microflow` does not \u2014 so this was never an unknown-property gap, just an unfinished one. **Check what an 'empty' value actually holds before calling it a loss**: `ConcurrenyErrorMessage` looked like 58 lost translations and every single one had `Text: \"\"`, i.e. an empty entry versus no entry. Two more traps worth knowing: `ActionActivity.Caption` changes ONLY where `AutoGenerateCaption` is true (a user-set caption survives), and ~90% of the 417-line diff on a real microflow is layout \u2014 bezier vectors, sizes, connection indices \u2014 which buries the two lines that matter", "refs": []} -{"area": "mdl/backend", "date": "2026-09-12", "symptom": "Four plain widget keywords were refused by the DEFAULT engine with \"widget *pages.X not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy\": statictext, dropdown, staticimage, dynamicimage (plus NanoflowSource as a list data source). Nothing in mdl-examples/ or in any skill used those keywords, so the doctype gate ran green the whole time. Then, closing the gap: `statictext` on EITHER engine produced a project that could not be loaded at all — `mx check` aborts with TypeCacheUnknownTypeException for Forms$Text before running any validation, and Studio Pro fails the same way", "cause": "Two unrelated things. (1) The modelsdk widget dispatch simply had no case arms for those five types — reachable set measured at five, not the twenty-three a name scan suggests: nineteen pages.* widget structs are constructed by nothing, and EntityPathSource / ShowHomePageClientAction are written by NEITHER engine, so their fallback named a path legacy could not take either. (2) `Forms$Text` is not a Mendix type. It is absent from modelsdk/gen, from generated/metamodel, and from all 3,257 Studio Pro Texts$Text holders in ako/TestApp; `buildTextWidgetV3` minted it for both `text` and `statictext`. Writing it is worse than a build error — the project is unopenable and unfixable in the modeler", "file": "`mdl/backend/modelsdk/widget_write_legacy_gaps.go` (new), `mdl/backend/modelsdk/widget_write.go`, `sdk/mpr/writer_widgets_display.go`, `mdl/executor/validate_widget_retired.go` (new), `mdl/executor/cmd_pages_builder_v3_widgets.go`", "insight": "**\"Parity with the other engine\" is the wrong bar until you have checked whether the other engine is right.** That was the starting assumption here and measuring overturned it: ako/TestApp carries three Studio-Pro-authored Forms$StaticImageViewer widgets (FeedbackModule) and legacy disagreed with all three — it omitted AlternativeText (non-omitempty in generated/metamodel) and wrote BSON null for the unset Image. Legacy's dynamic image was worse: a hand-rolled AlternativeText carrying a FallbackValue key that Forms$ClientTemplate does not have, four lines after a comment in the SHARED serializer saying exactly that (\"Must be Fallback object, not FallbackValue string\"). sdk/mpr was corrected to match rather than pinned as truth. **Look for a Studio Pro reference inside the fixture before concluding there is none** — `bson.Unmarshal` every mprcontents unit and count instances of the $Type; the marketplace modules a blank app ships are Studio Pro output. The same 40-line scanner settles value-shape questions no doc answers: measured 0 nulls against 4,400+ empty strings for by-name references, so an unset QualifiedName is \"\", never null; and marker 3 in 3,257 of 3,257 Texts$Text lists, so legacy's marker 2 for a non-empty one is the wrong side of that divergence, not modelsdk's 3. **A widget keyword the example corpus never uses is a keyword nothing tests**, whatever the coverage numbers say. **Check that the project still LOADS, not just that it builds** — a load failure aborts `mx check` before the error list, so grepping for CE-codes finds nothing and reads like success", "refs": []} -{"area": "mdl/backend", "date": "2026-09-12", "symptom": "The modelsdk (default) engine leaves 19 of FullBackend's 276 methods to the errUnimplemented stub, which tells the user to \"rerun with MXCLI_ENGINE=legacy\" — the engine being retired. Nineteen unported methods reads as nineteen reasons legacy has to stay shipped and tested", "cause": "Seventeen of the nineteen cannot be reached at all: they are interface surface that only the MPR backend's own delegation and callers holding a concrete *mpr.Reader / *mpr.Writer (api/, examples/, cmd/mxcli commands that open a reader directly) ever touch, so the stub can never fire. The two that ARE reachable — GetRawUnitByName and ParseMicroflowBSON, four call sites in mdl/executor/cmd_microflows_builder.go — were invisible because each is a fast path with a working slow-path fallback: the stub errored, the code fell through to an O(n) module walk, and the result was identical", "file": "`mdl/backend/modelsdk/raw_lookup.go` (new), `mdl/backend/modelsdk/unimplemented_reachability_test.go` (new), `scripts/backend-reachability.sh` (new)", "insight": "**Grep cannot answer \"is this interface method reachable\" and the compiler can.** `b.reader.GetRawUnitByName(...)` inside the MPR backend and `ctx.Backend.GetRawUnitByName(...)` in the executor are indistinguishable to a regex, and receiver names vary, so a grep-based count said all nineteen had callers. Deleting one method from its interface at a time and rebuilding gives a yes/no per method with no judgement involved: 17 DEAD, 2 LIVE. Slow (one `go build ./...` each, minutes for the set) but decisive — script it, commit it, and pin its OUTPUT in a test rather than re-running it in CI. **A fallback hides an unimplemented method completely.** Nothing was broken and no test failed, because the fast path's failure was indistinguishable from a cache miss; the only symptom was work being done twice. Look for `if x, err := …; err == nil` fast paths when auditing what an engine cannot do. **Measure the speedup before claiming one**: restoring the fast path made no measurable difference (620ms vs 608ms over three runs on a 138-microflow project, within noise), because `mxcli exec` runs `check` first and check's own helpers load ListMicroflows anyway — so the cache the slow path builds is already warm. The value here is the removal of the errUnimplemented cliff, not speed", "refs": []} -{"area": "mdl/backend", "date": "2026-09-11", "symptom": "`ALTER PAGE REPLACE`/`INSERT` inside a data view bound `datasource: selection ` re-scopes the new widget's attribute binding to the OUTER data view's entity (**CE1613** \"The selected attribute 'Mod.Outer.Attr' no longer exists\"), and inside a Gallery/DataGrid 2 sourced by a **microflow/nanoflow** drops it entirely (**CE0402** \"No value specified\", `describe` shows `ContentParams: [{1} = ]`). `mxcli check --references` and `exec` both report success; `CREATE PAGE` binds the same widget in the same position correctly", "cause": "The mutator resolved a widget's scope in TWO walks that each knew a different subset of the ten Forms$*Source kinds. `Forms$ListenTargetSource` carries no EntityRef at all \u2014 only the listen target's NAME \u2014 so the entity walk saw no source on the selection data view and left the context at the enclosing one. The flow walk (`findNearestDataSourceDoc`) read only a widget's TOP-LEVEL `DataSource` key, so a pluggable list \u2014 whose source sits at `Object.Properties[datasource].Value.DataSource` \u2014 contributed nothing, and its `Objects[].Properties[].Value.Widgets` descent (the one the entity walk gained in #935) was missing too", "file": "`mdl/backend/pagemutator/mutator.go` (`resolveSourceScope`/`resolveSourceScopeVia`, `listenTargetDataSource`, `widgetOwnDataSourceDoc`, `pluggableDataSourceDoc`; `EnclosingEntity`/`EnclosingEntityForChildren`/`EnclosingDataSourceFlow` now share the one walk `findNearestDataSourceDoc`, and `findEnclosingEntityContext` + its two helpers are deleted)", "insight": "**Count the source kinds before fixing one.** `generated/metamodel/types.go`'s `DataSource is implemented by` list closes the set at ten, and they divide exactly three ways \u2014 seven carry an EntityRef, two are flows, one (ListenTarget) borrows the scope of the widget it names \u2014 so one resolver can be complete, where three successive per-kind patches (FINDINGS #55 association+flow, #935 pluggable, this one) each left a hole. **A nearer source that resolves to no entity must SHADOW the outer one**: inheriting is what wrote the wrong entity, and it also mis-scoped a flow-sourced list nested in an entity-bound data view \u2014 a case the report did not name and the old code got wrong. The listen target is found by a shape-independent search for \"a document with this Name that has a data source\", which is what makes it work when the target is a pluggable widget keeping its source three levels inside its Object; a visited-set guards a hand-written listen cycle. **Measurement trap: a CE1613 SUPPRESSES the CE0402s in the same `mx check` run** \u2014 the first reading said mxbuild tolerated the unbound parameter, and the CE0402s only appeared once the re-scoped binding was fixed, so count bindings in `describe`, not errors. The issue's own second repro (a Gallery over a DATABASE source) no longer reproduced \u2014 #935 had fixed it \u2014 and the live defect was its flow-sourced variant, so re-measure a report against main before trusting its class. Tests `mdl/backend/pagemutator/mutator_selection_source_test.go` (7, incl. dangling/cyclic listen targets and the shadowing control); repro `mdl-examples/bug-tests/1076-alter-page-selection-and-flow-scope.mdl` \u2014 2 \u00d7 CE1613 + 3 unbound before, 0 errors after, on mxbuild 11.10.0. Each half proven load-bearing by stubbing it alone and rebuilding the CLI", "refs": ["mendixlabs/mxcli#1076", "#55", "#935"], "ce": ["CE0402", "CE1613"]} -{"area": "mdl/backend", "date": "2026-09-15", "symptom": "On the default engine, `describe workflow` printed `wait for notification x;` without its boundary events (and their handler flows); the legacy engine described them. A describe -> exec round trip lost them.", "cause": "workflowActivityFromGen's typed switch had no case for *genWf.WaitForNotificationActivity, so it fell to workflowSimpleActivityFromGen, which reads only Name and Caption from raw BSON. That path's comment said the wait activities 'have no genWf struct' — true once, stale by the time gen gained WaitForNotificationActivity with BoundaryEventsItems().", "fix": "Typed case reading boundaryEventsFromGen(a.BoundaryEventsItems()), like the five other activities that carry boundary events.", "file": "mdl/backend/modelsdk/workflow_read.go", "insight": "A fallback path documented as 'for types gen does not model' silently keeps catching a type after gen starts modelling it; the reader then under-reads without any error. When gen is re-vendored, grep the typed switches for types that newly have structs. The bug surfaced only because a probe described what it had just written — reading back your own write, per activity type, is the cheap check."} -{"area": "mdl/backend/mcp", "date": "2026-09-15", "symptom": "Over MCP (--mcp) against Studio Pro 11.14, `create workflow` fails with `ped_create_document …: Validation errors … {\"/context\":\"Expected reference (string), got undefined\",\"/workflowName\":\"Expected string, got object\"}`; and on any version a workflow's on-created microflows and event handlers were silently absent from the created document", "cause": "(1) Studio Pro 11.14's Workflows$Workflow CONSTRUCTOR takes `context` (entity qualified name) and plain-string `workflowName`/`caption`; the mapper sent the older `parameter` element and a StringTemplate. The element (update) shape is unchanged. (2) mapWorkflowActivity hard-coded `onCreatedEvent: {$Type: Workflows$NoEvent}` and mapWorkflow never sent `onWorkflowEvent`, so constructs added to the semantic model later were dropped", "file": "`mdl/backend/mcp/workflow.go` (`workflowConstructorTakesContext`, `mapWorkflowContent`, `mapOnCreatedEvent`, `mapWorkflowEventHandlers`)", "insight": "`ped_get_schema` is a live oracle — ask it rather than guessing a version: the constructor schema text says `context: Reference<'DomainModels$Entity'` on servers that need the new shape. Any hard-coded default in a PED mapper (NoEvent, empty list) becomes silent data loss the moment the semantic model gains the property — grep the mappers for literals when a construct becomes authorable. ped_check_errors lags: a fresh document reports 'No errors found.' and its real errors only on a later call, so re-check before trusting a clean verdict"} -{"area":"mdl/backend","date":"2026-09-15","symptom":"AddAttribute and UpdateAttribute were unimplemented on the default engine, so api/ could only work on the retired legacy one — and api/'s whole integration suite reported green while verifying nothing","cause":"api/ imported mdl/backend zero times: it held a concrete *mpr.Writer and bypassed the backend abstraction, so the two methods had no caller through a backend value and were never ported.","file":"api/api.go","fix":"api.New takes a backend.FullBackend (plus an api.Open convenience that owns its connection); the two methods are implemented on the codec backend and struck off unreachableUnimplemented.","insight":"unreachableUnimplemented is a CENSUS OF WHO BYPASSES THE ABSTRACTION, not dead interface surface. Read its reason column as a map and it names api/, the MCP backend, and the cmd/mxcli commands holding a concrete reader; a method is on it BECAUSE such a caller exists and unreachable BECAUSE that caller does not use a backend value. So the list shrinks by closing a bypass, never by deleting methods — and the pin fails loudly when you do, which is how it should be used. Two traps in the port itself. (1) The generated gen list offers only Append and Remove, so replacing an element in place means rebuilding the list; the naive remove-then-add moves the edited attribute to the bottom of the entity, which is a diff on every edit — control: break the rebuild and the order test catches it. (2) THE SUITE THAT WOULD HAVE VERIFIED THE PORT HAD ONLY EVER SKIPPED. api/'s integration tests pointed at ../mx-test-projects/test-source-app, which is not in the repository, so all ten skipped on every machine and in CI since they were written (the #808 shape). `go test ./api/` passing said nothing. Repointed at the committed testdata/expr-checker fixture the codec backend's own tests use, and made a missing fixture FATAL rather than a skip, since a committed fixture's absence is a broken checkout. Before trusting a suite to verify a refactor, check it is not skipping."} -{"area":"mdl/backend","date":"2026-09-15","symptom":"The MCP backend held a concrete *mpr.Reader for its local reads, keeping three methods (GetDomainModelByID, GetWorkflow, ListNavigationDocuments) on FullBackend with no caller through a backend value — and 190 tests in the package, not one of which called Connect, so swapping the reader underneath it would have landed unverified with the suite green.","cause":"Those three reads had no codec-backend implementation, so MCP could not compose a backend and kept a legacy reader instead. Each was a re-keying or widening of a read the package already did, not new decoding.","file":"mdl/backend/mcp/backend.go","fix":"Implemented the three on the codec backend (mdl/backend/modelsdk/mcp_bypass_reads.go), sharing domainModelFromGen / workflowFromGen / navProfileFromGen with their siblings; added Backend.ConnectReadOnly and pointed MCP's reader field at backend.FullBackend. Wrote mdl/backend/mcp/connect_reads_test.go — the first test to call Connect at all.","insight":"A test can be vacuous in two different ways in one file, and the second is the one you miss. The read-only assertion SKIPPED (MyFirstModule has no entities), which the runner prints — so it was obvious. The GetWorkflow assertion looped over the listing and the fixture has ZERO workflows, so the loop body never ran and the test reported PASS having called nothing. Same failure class as #808, but silent. The fixes differ by shape: for the skip, pick a probe needing nothing pre-existing (CreateEntity, not AddAttribute); for the empty loop, SEED the thing through a read-write backend, then reconnect read-only and assert a specific named item is in the listing BEFORE fetching it — that guard is what converts a future vacuous pass into a failure. Cheapest way to find both: print the counts a test loops over before trusting it. MCP's read-only constraint also has a real control: revert ConnectReadOnly to Connect and the write-refusal test fails with the reported symptom, and the companion TestConnect_TheSameWriteSucceedsReadWrite proves the probe write is not failing for an unrelated reason."} -{"area":"mdl/backend","date":"2026-09-15","symptom":"Deleting the legacy sdk/mpr backend (mdl/backend/mpr) turned up three things the removal plan had not sized: a runtime error telling users to rerun on the engine that had just been deleted, an integration suite that had been exercising the retired engine rather than the default one, and a cross-engine parity test whose comparison became vacuous.","cause":"Each is a reference to the second engine that was invisible while two engines existed. errUnimplemented's message named MXCLI_ENGINE=legacy as the fallback; setupTestEnv in mdl/executor/roundtrip_helpers_test.go hardcoded mprbackend.New() as its default, so most of the package's integration tests ran on legacy; TestODataService_EngineWriteParity compared writer A's key set against writer B's.","file":"mdl/backend/modelsdk/backend.go","fix":"errUnimplemented now asks for a bug report instead of naming a fallback. setupTestEnv defaults to the codec backend. The parity test keeps its value assertion (a published OData service retains AllowedModuleRoles) and drops the comparison, renamed to match. --engine/MXCLI_ENGINE kept as a warning-only no-op; `bson compare` and mdl/enginecompare deleted.","insight":"Deleting one of two implementations is not finished when the code compiles — grep for the deleted thing in three places the compiler cannot reach. (1) RUNTIME STRINGS: an error message naming a removed fallback is strictly worse than no fallback, because the user follows it and gets a second failure; this one had been shipping the instruction 'rerun with MXCLI_ENGINE=legacy' from 19 generated stubs. (2) TEST DEFAULTS: a shared setup helper's hardcoded choice of implementation decides what a whole package actually covers, and here it silently pointed most of mdl/executor's integration tests at the engine being retired — deleting the other one moved them onto the default engine for the first time, which is coverage gained, not lost. (3) DIFFERENTIAL TESTS: a test that compares A to B has no meaning with one implementation, but the PROPERTY it was a means to usually does — for the OData one, that a service keeps its role grants, which mx check reports as 0 errors either way. Drop the comparison, keep the property, rename the file so the next reader is not misled. One counter-case worth noting: the doctype gate's engine MATRIX was kept at one entry rather than collapsed, because its selection function still converts a stale MXCLI_TEST_ENGINES=legacy into a loud failure instead of a gate that selects zero engines and reports success."} -{"area": "mdl/backend/mcp", "date": "2026-09-15", "symptom": "Over MCP against Studio Pro 11.14, `create workflow` with any `multi user task` fails: `ped_create_document …: {\"/flow/activities/1/taskPage\":\"Expected an object, but the value is missing.\"}`", "cause": "The 11.14 Workflows$MultiUserTaskActivity CONSTRUCTOR takes `taskPage: Element<'Workflows$PageReference'>`; mapWorkflowActivity sent the older bare `pageReference` string. Same class as the workflow constructor's `context` change in the same release", "file": "`mdl/backend/mcp/workflow.go` (`multiUserTaskConstructorTakesTaskPage`, `adaptMultiUserTaskPages`)", "insight": "PED constructor shapes drift per Studio Pro release independently of the element (update) shapes, and serverInfo.version is frozen at 1.0.0, so probe the constructor schema text rather than gating on a version. When one constructor of a document family changed shape in a release, check the others in that family before trusting the mapper — this one surfaced only because a phase-3 live probe used a multi-user task"} -{"area":"mdl/backend/modelsdk","date":"2026-09-15","symptom":"`describe microflow` prints a notify action as `notify workflow $Workflow;` with no `$X =` output variable, although the stored action has one; a rewrite from that output drops it","cause":"`modelsdk/gen` binds NotifyWorkflowAction.outputVariableName to the BSON key \"VariableName\"; Studio Pro 11.14 and the 11.6 metamodel store \"OutputVariableName\". The reader used gen's accessor and got nothing; the hand-built writer wrote the right key, so only reads were wrong","file":"`modelsdk/gen/microflows/types.go` (`initNotifyWorkflowAction`, STORAGE-NAME OVERRIDE), `mdl/backend/modelsdk/microflow_read_actions.go`","insight":"A property whose writer is hand-built and whose reader goes through gen can be wrong in one direction only, which a write-then-read test on the codec cannot catch — both sides agree with themselves. Read a Studio Pro-saved document instead (ako/TestApp's ZzMxcliExample_Notify is the fixture). 16 gen types bind `VariableName`, and most are right (the metamodel stores `variableName` for aggregate, cast and create actions), so fix by type against the metamodel, never by search-and-replace. Settled on mxbuild 11.6/11.10/11.13: under `VariableName` the variable is undefined (CE0109)."} -{"area": "mdl/backend", "date": "2026-09-15", "symptom": "Five `TestConnect_*` tests in `mdl/backend/mcp/connect_reads_test.go` (added by ako/mxcli#468) fail in a devcontainer — on pristine origin/main — with `Connect: connect to MCP server \"http://127.0.0.1:NNNNN/mcp\": ... dial tcp 192.168.65.254:NNNNN: connect: connection refused`, while CI is green", "cause": "The tests built the backend with `New(ped.srv.URL+\"/mcp\", \"\")`. An empty dial address falls through to `defaultDial` → `dialFor` in `mdl/backend/mcp/client.go`, which rewrites a localhost endpoint to `host.docker.internal:` whenever that name resolves — the intended convenience for reaching a Studio Pro on the Docker host. Inside a devcontainer it resolves, so the request goes to the host gateway and never reaches the httptest listener on the container's own 127.0.0.1. On a CI runner the name does not resolve and the rewrite is a no-op, which is why the suite was green there", "file": "`mdl/backend/mcp/connect_reads_test.go` (`backendFor`, used by `connected`, `TestConnect_GetWorkflowReadsASeededWorkflow`, `TestConnect_DisconnectIsIdempotent`); precedent `fakePED.connectClient` in `client_test.go`", "insight": "**A fake server must be dialled at the address it is listening on, never through production address resolution.** Any helper that takes an optional dial/host override and defaults to environment-sensitive logic (DNS lookups, proxy env, gateway rewrites) makes a test pass or fail by where it runs, not by what it tests — and CI is precisely the environment where such a rewrite is inert, so it cannot catch it. Pass the listener address explicitly (as `connectClient` already did) rather than changing `defaultDial`, which is correct for real connections. The tell in the error is the dialled IP differing from the URL's host. Control: revert the dial to `\"\"` in the devcontainer and the same five fail with the reported message; `TestConnect_TheSameWriteSucceedsReadWrite` passes either way because it never builds an MCP backend, which is what localises the defect to the constructor call", "refs": ["ako/mxcli#468"]} -{"area":"mdl/backend","date":"2026-09-15","symptom":"Six FullBackend methods sat on the unreachable census being treated as abstraction bypasses awaiting a port, when nothing anywhere wanted them: the whole WidgetSerializationBackend interface (SerializeWidget/ClientAction/DataSource/WorkflowActivity), GetUnitTypes and UpdateLayout. They had been superseded and simply never removed.","cause":"scripts/backend-reachability.sh reports DEAD for 'nothing calls this through a backend value', and that one verdict covers three different situations. The census header read all of them as bypasses, so the standing instruction was to port them — which for these would have meant implementing methods with no caller.","file":"mdl/backend/mutation.go","fix":"Deleted WidgetSerializationBackend, GetUnitTypes and UpdateLayout from the interface; regenerated unimplemented_gen.go and mcp/unsupported_gen.go (276 -> 270 methods); dropped the mock stubs and a duplicate *Backend impl. Census 11 -> 6, and the remaining six are genuine bypasses. Rewrote the census header and the probe script's own header to name the three causes.","insight":"A reachability probe that removes a method and rebuilds answers ONE question — is anything calling this through the interface — and DEAD has three causes wanting opposite fixes: BYPASS (a caller wants it but holds a concrete type; port the caller), ORPHAN (no caller anywhere; delete), DUPLICATE (callers exist but through a narrower package-local interface with a DIFFERENT SIGNATURE; delete). The probe cannot separate them; a grep for callers under any type can. DUPLICATE is the one that misleads, and it is worth knowing its two tells. First, a raw call-site count looks healthy — SerializeWidget had 6 and SerializeClientAction 4 — so the name reads as live; only comparing signatures shows the interface copy is unused (bson.D on the real path vs (any, error) on FullBackend). Second, the supersession is usually DOCUMENTED at the replacement, not at the corpse: WidgetBuilderBackend.SerializeWidgetToOpaque says 'This replaces the direct mpr.SerializeWidget call' — so grepping the NEW method's doc comment finds the old one faster than auditing the old one does. A stale comment on the dead method actively lies (the vestigial *Backend SerializeWorkflowActivity claimed the ALTER WORKFLOW paths used it; they use codecWorkflowDeps). Generalisable: when a helper has both an (any, error) and a concrete-typed variant, suspect the general-typed one is the abandoned interface obligation."} -{"area":"mdl/backend","date":"2026-09-15","symptom":"Backend.CreateEntity and CreateAssociation left the caller's semantic element holding an EMPTY ID, so the very next call failed with \"entity not found: \" (note the blank). The legacy writer had populated them, so this was a silent behaviour difference across the engine swap that no test caught.","cause":"assignEntityIDs/assignAssociationIDs mint identities on the gen element built by entityToGen/assocToGen, and nothing copied them back to the *domainmodel.Entity the caller passed in. assignID only fills an EMPTY id, so api/ was unaffected (its builders pre-assign one) — which is exactly why no existing test saw it.","file":"mdl/backend/modelsdk/domainmodel_write.go","fix":"copyAssignedIDs writes the minted entity and attribute ids back onto the caller's element, matching attributes BY NAME rather than by position; CreateAssociation does the same for its own id. Both only fill an empty id, so a caller-assigned one is preserved.","insight":"Found by RUNNING examples/modify_project after repointing it, not by any test — the unit suite, check-mdl and the whole integration gate were green. That is the argument for keeping example programs runnable and actually running them: an example is the only caller that uses the API the way an outside user would, with no pre-assigned ids and no knowledge of internals. Two specifics worth carrying. (1) The empty-string ID makes the error message read \"entity not found: \" with nothing after the colon — a trailing-blank in an error is a strong tell that an identity was never populated rather than looked up and missed. (2) When copying minted ids back, match sub-elements BY NAME, never by index: entityToGen can add, skip or reorder (an audit pseudo-type becomes a System-module generalization, not an attribute), so an index-based copy hands the caller ANOTHER attribute's identity — which looks like it worked and is worse than the empty id it replaced. The control matters here too: a test that only asserts 'id is non-empty' passes against an implementation that overwrites a caller-assigned id, which would break api/; assert the reported id equals the STORED one, and add a companion test that a pre-assigned id survives."} -{"area": "mdl/backend/mcp", "date": "2026-09-15", "symptom": "Over MCP against Studio Pro 11.14, every `create microflow` fails at ped_create_document: `{\"/flows/0/$Type\":\"Expected an element with $Type property.\",\"/returnType\":\"Expected one of [Void, Boolean, …], got {\\\"type\\\":\\\"Void\\\"}\"}`", "cause": "The 11.14 Microflows$Microflow CONSTRUCTOR became a canvas skeleton: flows are `$Type`d elements, returnType is a bare enum with returnTypeEntity/returnTypeEnumeration beside it, parameters go in a `parameters` list, and every object constructor declares only x/y (plus caption/loopType). Fixing just the two rejected keys is not enough: a create that still sends relativeMiddlePoint, action, returnValue or a split/loop source is ACCEPTED and those are silently dropped (read back: positions 0,0, `action: null` → 'No action defined', empty return value)", "file": "`mdl/backend/mcp/microflow.go` (`microflowConstructorTakesSkeleton`, `adaptMicroflowSkeleton`, `CreateMicroflow`)", "insight": "A validation error lists only what the validator rejects, not what it discards — after making a PED create pass, READ THE DOCUMENT BACK before calling it fixed. The skeleton create plus one ped_update_document that `set`s /objectCollection/objects/N/{action,returnValue,splitCondition,loopSource} (N counts the parameters, which the constructor's $id(/objects/N) does not) round-trips cleanly. Reads expand one level, so a nested value (messageTemplate text, variableType entity, case value) looks empty until its own path is read — don't mistake the stub for loss"} -{"area":"mdl/backend","date":"2026-09-15","symptom":"Moving cmd/mxcli/project_tree.go from a concrete sdk/mpr reader to the codec backend silently dropped System.VerifyPassword from `mxcli project-tree` output. Build clean, all tests green, 131 bytes missing from a 78KB JSON tree.","cause":"The System module's Java actions are platform built-ins with NO stored unit in the .mpr. sdk/mpr's ListJavaActions/ListJavaActionsFull appended them via BuildSystemJavaActions(); the codec backend only decoded stored units, so it reported them as absent.","file":"mdl/backend/modelsdk/java.go","fix":"Moved the System Java action definitions from sdk/mpr to modelsdk/meta (which already owns the virtual System module's entities and associations) and appended them in both codec ListJavaActions and ListJavaActionsFull; sdk/mpr now delegates rather than holding a second copy. Regression tests plus a control that the STORED actions are still returned.","insight":"Found by diffing the command's output against a binary built from the pre-port commit — not by any test, and no test would have caught it. That baseline-diff is the technique worth keeping for any reader swap: build the old binary first (git stash -u; make build; cp bin/mxcli /tmp/before), then require byte-identical output on every command whose reader you touched. A 131-byte difference in 78KB of JSON is invisible to eyeballing and to 'it still runs'. Two structural lessons. (1) A SYNTHESIZED element is the thing a reader swap loses, because it exists in one reader's code rather than in the data — grep the old reader for 'not stored in' / 'virtual' / 'Build*' helpers before trusting a port. (2) The census in unimplemented_reachability_test.go CANNOT catch this class of bypass: it only lists methods with no implementation, so a caller holding a concrete reader while calling only IMPLEMENTED methods is invisible to it. project_tree.go called 36 semantic methods, every one on FullBackend, and never appeared — which is also why the Phase 3 write-up wrongly described all remaining bypasses as raw-unit debugging tools. The complete list of bypasses is the sdk/mpr IMPORTER list, not the census."} -{"area": "mdl/backend", "date": "2026-09-15", "symptom": "Every ALTER WORKFLOW op addressing an activity (e.g. `insert boundary event on bugSplitJump …`) failed with `ambiguous activity \"bugSplitJump\" (2 matches); use @N to disambiguate` whenever the workflow also contained `jump to bugSplitJump`. Measured live over MCP against Studio Pro 11.14; `bugSplitJump@1` worked.", "cause": "buildJumpTo (mdl/executor/cmd_workflows_write.go) names every jump `JumpTo` and, with no MDL caption, sets Caption = the target's name. Both activity resolvers — mcpWorkflowMutator.searchActivities and wfmutator's findActivitiesRecursive/findActivityIndexRecursive — matched `name == ref || caption == ref` into ONE pool, so the jump's caption shadowed its target's name. Same defect in the MPR mutator, it was only reported over MCP.", "file": "mdl/backend/mcp/workflow.go", "fix": "Resolve in two tiers in both backends: without @N, activities whose name equals the reference win when any exist, else caption matches are used; with @N, every name-or-caption match still counts in DESCRIBE order. A first attempt also scoped @N to the chosen tier and CI caught it: mdl-examples/doctype-tests/24-workflow-examples.mdl addresses a second call activity as `ACT_Process@2` (found 1 matches) — @N is an established positional index over the pooled matches, so only the unqualified case may change. The jump's default caption was left alone — existing projects already carry that shape, and changing it would rewrite every mxcli-authored jump on the next re-run.", "insight": "A reference that can match either an identity (name, deduplicated) or a label (caption, free text) must rank them, never pool them — any label that happens to repeat an identity turns a unique name ambiguous, and mxcli's own defaults manufacture exactly that collision. The tell is `N matches` where N counts a jump/marker you did not think of as a candidate. When fixing a resolver, grep for its twin in the other backend (wfmutator vs mcp): the two were written to agree on @N numbering and had the same bug. Controls: both new tests fail with the reported message against the pooled resolver."} -{"area": "mdl/backend", "date": "2026-09-15", "symptom": "Timer boundary events written over `--mcp` (Studio Pro 11.14): every one reports CE0126 \"Missing value for parameter 'Timer'\"; an interrupting timer's `jump to` outside a parallel split is stored as an End, and inside a split an End path gets a target-less jump; a `{ }` path gets an End appended after the end-of-path marker and is refused; the timer events of a single user task are not stored at all. mxbuild 11.13 builds all of these", "cause": "Neither timer boundary event constructor has `firstExecutionTime` (ped_get_schema), so the delay mxcli sent was ignored; the interrupting constructor normalizes the path terminator (End outside a split, jump inside) and needs `isInsideOfParallelSplit`, which mxcli sent only for notification events; the single user task constructor drops `boundaryEvents`, and the re-add covered notification events only", "file": "`mdl/backend/mcp/workflow.go` (`markBoundaryEventsInSplits`, `timerBoundaryEventOps`, `applyTimerBoundaryEvents`, `applyUserTaskBoundaryEvents`, `InsertBoundaryEvent`)", "insight": "**Three traps that each looked like success.** (1) `ped_update_document` re-validates the WHOLE document, so one malformed boundary path makes every later update fail — the post-write `set` of the delay included; refusing bad shapes before sending is load-bearing, not cosmetic. (2) An `add` to a `boundaryEvents` list does NOT append and does not keep add order (I, NI-A, NI-B in one batch stored NI-B, NI-A, I); finishing timers by statement index put each delay on the OTHER event and `ped_check_errors` said \"No errors found.\" — only reading `firstExecutionTime` back per event caught it. Identify an added element by the `persistentId` that appeared, never by index. The constructor, by contrast, keeps order. (3) `create or modify` over MCP cannot see a workflow created earlier over MCP and not yet saved — the executor's existence check reads the .mpr on disk — so a live update-path probe must target a document that is on disk", "fix": "Send no `firstExecutionTime`; set it afterwards at the event's stored path. Refuse interrupting-timer paths running to their end, interrupting-timer paths in a split not ending in a jump, and non-interrupting-timer paths ending in a jump. Set `isInsideOfParallelSplit` on interrupting timers. Restore a jump outside a split by removing the constructor's End and adding the jump (measured clean). Re-add a single user task's events one `add` at a time, mapping statement index to stored index by persistentId, and address nested paths through that map; ALTER insert reads ids before/after the add"} -{"area": "mdl/backend", "date": "2026-09-15", "symptom": "`create or modify workflow` over `--mcp` (Studio Pro 11.14) stored the flow's activities reversed, with an old activity left in and a new one missing ([Start, a, b, End] rewritten as A, B, C → Start, B, A, a, End; a larger flow also lost its parallel split and held a name twice); `alter workflow … replace activity X` left X in place. The update calls all reported SUCCESS", "cause": "One `ped_update_document` batch is not applied in the order sent. Every measured batch fits: ops run highest index first, and at one index the adds go in as a block in op order before the removes — so a remove at an index where something was just added removes the added element. `UpdateWorkflow` sent the flow's removes plus its middles in reverse at index 1 in one batch (and index-less adds for event sub-processes/handlers, which come out reversed); `ReplaceActivity` sent remove @k plus adds at k, k+1; `InsertAfterActivity` sent incrementing indices", "file": "`mdl/backend/mcp/workflow.go` (`UpdateWorkflow`, `InsertAfterActivity`, `ReplaceActivity`, `addAtOp`); simulator `pedListSim` in `mdl/backend/mcp/workflow_listops_test.go`", "insight": "**The code comment claimed the reverse-at-index-1 trick worked, and no fake PED modelled batch semantics, so the unit tests asserted the ops sent rather than the list stored.** Fix tests by simulating the server's list semantics and asserting the resulting ORDER, and keep a table test that replays each raw-PED measurement through the simulator — that is what makes the simulator trustworthy and each control meaningful. The first guess at the rule (\"adds first, then removes\") fit two measurements and failed the third; fit the model to every data point before building on it. Also: a live update-path probe needs a workflow the executor can see — either on disk, or created earlier in the same exec (the backend's session list)", "fix": "Never add to and remove from the same list in one batch: add the statement's elements at a single index in their own order (flow middles @1, event sub-processes and handlers @0, replacement activities @k+1), then remove the stored/replaced ones in a second update. Adding first leaves duplicates, not a gutted workflow, if the second update fails"} -{"area":"mdl/backend","date":"2026-09-15","symptom":"Wiring FindCustomWidgetType from modelsdk/mpr.Reader onto the codec Backend by straight delegation made `mxcli extract-templates` extract 0 of 6 templates, reporting for each widget: '[SKIP] Combo box: widget type is bson.D, want bson.D'. The type assertion in the caller names the same type on both sides of 'want'.","cause":"modelsdk/mpr builds RawType/RawObject with the v2 BSON driver (go.mongodb.org/mongo-driver/v2/bson) while sdk/mpr and every caller use v1 (go.mongodb.org/mongo-driver/bson). They are unrelated Go types that both print as 'bson.D', so the mismatch is invisible in the error text. types.RawCustomWidgetType declares the fields as `any` to avoid a BSON dependency, which removes the compiler's ability to catch it too.","file":"mdl/backend/modelsdk/widget_custom_find.go","fix":"Convert at the backend boundary with the package's existing v2ToV1BSON helper, so RawType/RawObject always hold v1 bson.D — the currency sdk/mpr established and callers assert. Verified by extracting all 6 templates byte-for-byte identically to the pre-change binary (1.2MB datagrid.json included); reverting the conversion fails the new test with 'RawType is bson.D, want v1 bson.D'.","insight":"An `any` field crossing an engine boundary can carry the RIGHT type name and the WRONG package, and the error message will look like a tautology. When a type assertion fails with identical type names on both sides, the question is which import path each came from, not what the type is — the two BSON drivers coexist in this repo on purpose (modelsdk is v2, sdk/mpr and the CLI are v1) and widget_pluggable_write.go's v2ToV1BSON already existed for the write direction. A cast written to silence that compile/assert error panics at runtime instead. Two process notes from the same change. (1) GREP FOR AN EXISTING IMPLEMENTATION BEFORE WRITING ONE: the walker had been in modelsdk/mpr all along (FindAllCustomWidgetTypes + collectCustomWidgets, and it populates UnitName/WidgetName which a fresh implementation would omit); only the backend wiring was missing, which is exactly what 'this should be unreachable' in the unimplemented error meant. (2) unimplemented_gen.go still emits the stub after a method is implemented — the generator writes a complete fallback set and Backend's own method shadows it — so the thing to update is the unreachableUnimplemented map in unimplemented_reachability_test.go, which fails loudly if a listed method becomes implemented."} -{"area":"mdl/backend","date":"2026-09-15","symptom":"Phase 4a took sdk/mpr from 27 importers to 0, but nothing stopped the count from creeping back — there was no build or test guard, only the plan document and a habit.","cause":"The invariant lived in prose. A single new `import \"github.com/mendixlabs/mxcli/sdk/mpr\"` compiles, passes every test, and reintroduces exactly the blind spot Phase 4a existed to close: the unimplemented-method census in mdl/backend/modelsdk lists methods with NO implementation, so a caller reaching one through a concrete *sdk/mpr.Reader never appears in it. That is what hid project_tree.go's 36 semantic reads (#477) and cmd_extract_templates.go's FindCustomWidgetType (#484) until each was found by hand.","file":"mdl/backend/sdkmpr_import_guard_test.go","fix":"TestNothingImportsTheLegacyEngine parses every .go file's imports (go/parser, ImportsOnly) and fails naming any file that imports sdk/mpr, with the remedy in the message. Controlled by dropping a one-line file importing sdk/mpr into examples/ — it fails and names the file.","insight":"A zero-count invariant needs TWO positive controls or it passes vacuously forever, and the failure mode is silent by construction: a walk rooted at the wrong directory, a skipped-dir rule that is too broad, or an import-parsing mistake all report '0 importers' and read as success. So assert (1) a plausible number of files was actually scanned (here >500; it sees 2551) and (2) the detector can see imports AT ALL, by counting a package the repo definitely does import (mdl/backend, 120 files). Only then does 0 mean zero. This is scripts/check-tunnel-deps.sh's pattern — it asserts chisel IS in the linux graph before asserting it is absent from windows/darwin — and the same reasoning as a bug-fix control: a test that only ever passes has not been shown to detect anything. Practical note: skip sdk/mpr's own directory by comparing the path to the repo root rather than by basename, or a directory named mpr elsewhere is skipped too."} -{"area": "mdl/backend/modelsdk", "date": "2026-09-16", "symptom": "`describe enumeration System.WorkflowActivityType` -> \"enumeration not found\" while `describe entity System.WorkflowActivityRecord` prints `ActivityType: Enumeration(System.WorkflowActivityType)` in the same session. `show enumerations` omits every System enum; `check --references` REJECTS a valid attribute typed against one (a false positive that blocks correct scripts); `CATALOG.attributes LEFT JOIN CATALOG.enumerations` resolves 0 of 19 on a blank app. Values could only be guessed at until the build rejected one with CE1613", "cause": "`modelsdk/meta.SystemEnumerations` — all 15 System enumerations with their values — had been in the tree since #889 with ZERO non-test consumers. The System module is not stored in the .mpr at all (measured: the string `WorkflowActivityType` occurs in 0 of 370 mprcontents units and 0 bytes of the .mpr sqlite), so its entities/associations/Java actions are each synthesized by a `Build*` helper and appended to a listing. The enumeration half had the data table and neither the helper nor the wiring, so the entity attribute printed a type naming a document nothing could produce", "file": "`modelsdk/meta/system_enumerations.go` (new, `BuildSystemEnumerations`); wired in `mdl/backend/modelsdk/enumeration.go` (`ListEnumerations`, `GetEnumeration`)", "insight": "**A data table with no consumer looks exactly like a missing feature.** Before opening any file, `grep -rn --include=*.go | grep -v _test` — zero hits is the whole diagnosis, and it took one command. The virtual System module has one wiring point PER LISTING, so the question for any new System doctype is \"which listings must it appear in\", not \"is the data there\". **One append fixed four of the five reported symptoms at once** (describe, show, check --references, catalog) because the catalog builder takes `ctx.Backend` as its reader — so `ListEnumerations` is the single choke point. Two traps. (1) `search` was NOT fixed by it: the strings index is built from value CAPTIONS (`builder_modules.go`), and the meta table carries names only, so System enums produce 0 rows in `CATALOG.strings` while user enums produce 36. Defaulting a caption to the value name would be inventing text indistinguishable from a developer's own — left unfixed and filed instead. (2) A stale `.mxcli/catalog.db` made the catalog look unfixed for three measurements; delete it before concluding anything about catalog output. Diagnosis tip: the sibling guard `TestModelerSystemEntities_HaveResolvableGeneralizations` existed and its enumeration twin did not — when one half of a synthesized module has a resolvability test, check whether the others do", "refs": "mendixlabs/mxcli#1102, mendixlabs/mxcli#1071, #889", "ce": "CE1613"} -{"area":"mdl/backend","date":"2026-09-16","symptom":"With sdk/mpr at zero importers, `rm -rf sdk/mpr` still would not have been safe: it had two live dependencies that an import-based check cannot see. sdk/mpr/version has six importers (two of them shipping code, cmd/mxcli/docker/build.go and patch.go), and cmd/mxcli/docker/update_widgets_test.go reads sdk/mpr/testdata/v1-project by FILESYSTEM PATH.","cause":"The zero-importer guard matched the exact string \"github.com/mendixlabs/mxcli/sdk/mpr\". A SUBPACKAGE is a different import path, and a testdata directory is not an import at all — it is an os.DirFS string. Neither shape appears in a check written against the parent package's path.","file":"sdk/mpr","fix":"Repointed the six version importers at mdl/types (sdk/mpr/version.ProjectVersion is `type ProjectVersion = types.ProjectVersion`, an ALIAS, so it is the same type rather than a compatible one — modelsdk/mpr/version declares a duplicate struct and would NOT have been), moved the v1-project fixture to modelsdk/mpr/testdata/, verified both with the package still present, and only then deleted. 163 files, 41,674 lines.","insight":"Before deleting a package, search for THREE things, not one: the package's own import path, its subpackages' import paths (`.../pkg/`), and its directory as a literal string (testdata read through os.DirFS, go:embed, scripts). The last two are invisible to any importer census. Repoint everything FIRST and prove the build and tests green while the package still exists — that separates 'the repoint was wrong' from 'the deletion was wrong', which a single combined commit cannot distinguish. Two measurements worth keeping: the shipped binary is byte-identical in SIZE before and after, confirming the linker had already dropped the package, so this deletion removes source weight and not runtime behaviour; and `sdk/widgets` dropped to zero importers as a side effect but must NOT be deleted, because modelsdk/widgets/dirty_template_test.go reads sdk/widgets/templates/mendix-11.6 by path — the same path-not-import trap, found by grepping for the directory name rather than the import."} -{"area": "mdl/backend", "date": "2026-09-17", "symptom": "Every MDL write to an EXISTING persistent entity resets each attribute's storage GUID to the attribute's own `$ID`. The next deploy against a database that already holds data drops and recreates every column of that entity: rows and associations survive, all attribute values are gone. Reported from production \u2014 28 attributes of 607 rows emptied by one edit. `mxcli check`, `exec`, `mx check` and the build are all clean, and `DESCRIBE ENTITY` is byte-identical before and after. All six ALTER forms do it, INCLUDING `SET DOCUMENTATION`, which touches no attribute", "cause": "The unclosed half of #657. `UpdateEntity` rebuilds the target with `entityToGen`, which rebuilds every ATTRIBUTE via `attributeToGen` -> fresh `NewAttribute` (`raw==nil`), so the codec's `EmitGUID` default writes `GUID = $ID`. #657 carried `orig.Raw()` onto the rebuilt ENTITY and noted that siblings survive via the list-rebuild raw passthrough \u2014 but the target's own children are all rebuilt, and nothing carried their raw. The new GUID comes out equal to the STORED `$ID` (not a random one) because `canon.TransplantIDs` then substitutes the stored `$ID` over every 16-byte occurrence of the fresh one, GUID included, since the GUID *is* that value", "file": "`mdl/backend/modelsdk/domainmodel_alter.go` -> `UpdateEntity`; fix in `mdl/backend/modelsdk/domainmodel_child_identity.go` (`carryChildIdentity`); guard in `modelsdk/canon/storageguid.go`", "insight": "Three separate guards could not see this, and the reason is worth more than the fix. (1) The corruption is IDEMPOTENT: GUID is derived from a now-stable `$ID`, so the second identical write produces byte-identical bytes, elision fires and the executor reports `Unchanged`. Damage happens exactly once, on the first write, and every later diagnostic \u2014 including re-running the same script \u2014 looks clean. Measured: same unit bytes, 0 of 16 GUIDs matching the original. (2) `TestFreshGUIDFieldsHaveAnIdentityDecision` sees only codec `FreshGUIDFields`; a GUID derived from `$ID` is `EmitGUID`, a different registration, so it was never in that guard's view \u2014 the same blind spot that let `Workflows$*.PersistentId` through in #949. (3) `identityFields`/`CarryIdentity` reach only top-level properties of the document ROOT; these GUIDs sit on nested elements. When a write 'succeeds' but the reader shows nothing changed, assert on raw BSON \u2014 and ask whether the damage REPEATS, because a one-shot corruption defeats every same-vs-same check. Pair a GUID carry on the identity the caller tracked (`domainmodel.Attribute.ID`, which `attributeFromGen` round-trips), never on a structural pairing: `TransplantIDs`' matching is deliberately tolerant because a wrong `$ID` match only makes a diff bigger, but a wrong GUID match makes the runtime adopt another column's data under a new name and type. That is also why the canon guard REFUSES rather than repairs. The guard found the one legitimate GUID writer on the first full-suite run \u2014 `marketplace.ApplyIdentities`, which transplants captured GUIDs onto a module's replacement documents, exactly as Studio Pro's update does \u2014 so it needs a named opt-out (`UpdateRawUnitOwningStorageGUIDs`), and running the whole suite is how you find out which paths a new write-path refusal breaks. Issue mendixlabs/mxcli#1119", "refs": ["mendixlabs/mxcli#1119", "#657", "#949"], "ce": []} -{"area": "mdl/backend", "date": "2026-09-17", "symptom": "END-TO-END CONFIRMATION of the #1119 data loss on a live database, plus the part nobody had measured: a recreated column with a MODEL DEFAULT is silently backfilled with that default, so the loss can look like plausible data rather than empty cells.", "cause": "Same as the #1119 record: `UpdateEntity` rebuilt each attribute with `raw==nil`, so `EmitGUID` wrote `GUID = $ID` and the runtime's synchroniser saw every attribute as deleted-and-re-added.", "file": "`mdl/backend/modelsdk/domainmodel_child_identity.go`; measured against Mendix 11.13.0 + PostgreSQL 16 via `mxcli run --local --ensure-db`", "insight": "The experiment, worth repeating for any identity-related write: blank `mxcli new` app on 11.13.0, subject `Administration.Account` (Studio Pro-authored, so `GUID != $ID` on all three attributes \u2014 an entity mxcli CREATED is immune, because its GUID equals its $ID from birth and an ALTER then reproduces the same value, which is exactly how this can be missed in testing). Seeded 607 rows. (1) `mendixsystem$attribute.id` holds the model `GUID` byte-for-byte once the .NET field order is undone \u2014 verified on all three attributes, and it matches the GUID, never the `$ID`. (2) PRE-FIX binary, one `ALTER ENTITY \u2026 SET DOCUMENTATION`: all three GUIDs became equal to their `$ID`, `mx check` reported 0 ERRORS on the result, then boot logged `ConnectionBus: Executing 14 database synchronization command(s)` and `fullname` went 607 -> 0, `email` 607 -> 0. The column ORDER in `\\d` changed, which is the cheap tell that a column was dropped and re-added rather than altered. (3) The boolean `IsLocalUser` read as intact at first \u2014 607 non-null \u2014 and that was a MEASUREMENT TRAP: it carries `default true` in the model, so the fresh column was backfilled from the default. Proved by setting all 607 to `false` first: after the GUID change they were all `true` again. So `count(col)` is not a data-loss test; seed a NON-DEFAULT value and check the values. (4) FIXED binary, same ALTER: GUIDs unchanged, and after redeploy all 607 rows kept `fullname`, `email` AND the non-default `islocaluser=false`. The control is the pre-fix binary built from the parent commit in a git worktree, not a stubbed guard, so it exercises the real shipped path.", "refs": ["mendixlabs/mxcli#1119"], "ce": []} -{"area": "mdl/backend", "date": "2026-09-22", "symptom": "`MOVE ENTITY Mod.E TO Target` either silently destroyed the entity's whole table on the next deploy, or \u2014 after the #1119 write guard landed \u2014 was REFUSED outright for any entity participating in an association, naming a `DomainModels$CrossAssociation` the user never mentioned.", "cause": "`MoveEntity` (`mdl/backend/modelsdk/association_move_write.go`) rebuilds the entity with `entityToGen` and carried NEITHER of the two identity carries `UpdateEntity` has \u2014 no `SetRaw(orig.Raw())` (#657, the entity's own GUID) and no `carryChildIdentity` (#1119, attributes and indexes). Separately `crossAssocFromGenAssoc` built a fresh `NewCrossAssociation` that preserved the association's `$ID` but not its GUID, so `EmitGUID` wrote `GUID = $ID`. The guard saw only that third one, because the conversion happens IN PLACE in the source unit while the entity lands in a different unit where its `$ID` pairs with nothing.", "file": "`mdl/backend/modelsdk/association_move_write.go` (`MoveEntity`, `crossAssocFromGenAssoc`, `crossAssocRawFromAssoc`)", "insight": "MEASURED on Mendix 11.13.0 + PostgreSQL 16, same 250-row starting state both ways: GUID re-minted -> the old table is dropped and an empty one created (250 -> 0 rows); GUID preserved -> the runtime RENAMES the table (`myfirstmodule$moveprobe` -> `administration$moveprobe`, 3 DDL commands) and every row survives with its values. So a module move preserves data IFF the GUID does \u2014 the runtime resolves the entity by GUID, not by table name \u2014 and a move is therefore the WORST case of this class, losing a whole table rather than a column. No warning is warranted; the carry is the fix. Two method notes. (1) Converting an element to a different `$Type` cannot use `SetRaw`, because the encoder passes `$Type` through from raw, and cannot use gen's `SetDataStorageGuid` either: gen binds it under key `DataStorageGuid` where Studio Pro stores `GUID` (a keyaudit row) AND types it `string` where the property is a 16-byte binary. The way through is a RAW transform of the stored document \u2014 here `$Type` rewritten, `ChildPointer` -> `Child`, the two `*Connection` waypoints dropped, everything else verbatim \u2014 with the target key set taken from `generated/metamodel`, the arbiter: exactly the 13 keys `DomainModelsCrossAssociation` declares, confirmed against the emitted document. `SetRaw` + `InitFromRaw` leaves the element clean, so the encoder's existing-element path passes it through and the registered `EmitGUID` default (fresh elements only) does not double up. (2) The MXCLI-CREATED IMMUNITY bit three times in one session and is now the first thing to check when a GUID test or repro script passes: an entity mxcli created has GUID == $ID from birth, so 'GUID = $ID' re-mints the same value and nothing is detectable. It voided a runtime control, and it makes the MDL bug-test script in `mdl-examples/bug-tests/` unable to fail \u2014 verified by running the pre-fix binary against it. A raw carry onto a MOVED entity is nonetheless safe: the unmodeled `Image`/`ImageData` are qualified names for a document that does not move with the entity (so keeping them is correct, and today's move silently DROPPED the image), and everything needing a rewrite is modeled and therefore dirty. Separate, pre-existing, NOT this fix: `MOVE ENTITY` rewrites no qualified-name references, so a move of a referenced entity leaves 33 CE1613s in a blank 11.13 app \u2014 identical count before and after this change.", "refs": ["ako/mxcli#503", "mendixlabs/mxcli#1119", "#657"], "ce": ["CE1613"]} -{"area": "mdl/backend", "date": "2026-09-23", "symptom": "Six statements were affected and only one was in the report's title. On upstream, `ALTER ASSOCIATION ... SET COMMENT|SET OWNER`, `CREATE OR MODIFY ASSOCIATION` (even an identical re-run), `RENAME ASSOCIATION` and `RENAME ENTITY` each re-minted the storage GUID of EVERY element in the module's domain-model unit — reporter measured 282 in one module (37 entities, 224 attributes, 21 associations) and a runtime crash, `Cannot invoke \"...Table.getTableName()\" because \"table\" is null`. On a branch carrying the #1119 write guard the same six statements were REFUSED instead, naming 9 elements on a blank 11.13 app, so the commands were simply unavailable for any Studio Pro-authored module.", "cause": "`Backend.UpdateDomainModel` (`mdl/backend/modelsdk/domainmodel_alter.go`) removes and rebuilds the WHOLE Entities and Associations lists, so every element arrives `raw == nil` and the codec's `EmitGUID` default writes `GUID = $ID`. Its own doc comment claimed it preserved \"each element's identity\", which was true of the `$ID` (explicitly `SetID`-ed) and false of the `GUID`. This is the same defect as #657 (entity) and #1119 (its children) on the OTHER list-rebuild shape: `UpdateEntity` swaps one entity into an otherwise raw-passthrough list, so its siblings were always safe; here there are no passthrough siblings at all.", "file": "`mdl/backend/modelsdk/domainmodel_alter.go` (`UpdateDomainModel`); the carry helper is `domainmodel_child_identity.go` (`carryChildIdentity`)", "insight": "WHAT MAKES THIS THE EXPENSIVE ONE IS THE STATEMENT NOBODY ASSOCIATED WITH IT: `RENAME ENTITY` routes through the same function, and an entity's name IS its table name. Per the #503 measurement (11.13.0 + PostgreSQL 16, same 250-row start both ways) a preserved GUID makes the runtime RENAME the table and a re-minted one makes it DROP the table and create an empty one — so a rename loses a whole table, where an ALTER loses a column. The title said ALTER ASSOCIATION; the census of call sites is what found the rest. METHOD: enumerate every caller of a GUID-bearing converter (`entityToGen`, `assocToGen`, `attributeToGen`) rather than reproducing the reported statement — that closed the class in one pass and turned up a fifth site, `UpdateAttribute`, reachable only through the public `api/` package and therefore invisible to any MDL repro. The fix is simpler than #503's: `assocToGen` is Association -> Association, so the stored and rebuilt `$Type` agree and a direct `SetRaw` works, where a cross-module move had to transform the raw because the `$Type` changes. A raw carry here also SUBSUMES #872, which patched the association line anchors property-by-property after the same 'the rebuild only carries what the semantic model models' mechanism dropped them. TWO MEASUREMENT TRAPS, both cost time. (1) A GUID census keyed on element NAME reads a RENAME as 'the old element vanished, a new one appeared' — 4 moved and 1 added where the truth was 0; key it on `$ID`. (2) `ALTER ASSOCIATION ... SET DOCUMENTATION` is a parse error; the real spelling is `SET COMMENT`, so a probe written from the wrong keyword reports the statement unaffected. THE GUARD IS NOW THE CHEAPEST ASSERTION AVAILABLE for this class: a write that lands on a Studio Pro-authored unit IS proof no GUID in it moved, which is why the end-to-end check is 'the statement succeeded', not a BSON diff — verified anyway by grepping the 16-byte binaries under key `GUID` out of the `.mxunit` before and after (9 of 9 identical, all five statements, `mx check` 0 errors).", "refs": ["mendixlabs/mxcli#1169", "#657", "mendixlabs/mxcli#1119", "ako/mxcli#503", "#872"], "ce": []} -{"area": "mdl/backend", "date": "2026-09-17", "symptom": "A microflow's URL (the deep link Studio Pro shows on the microflow's properties, Mendix 10.6+, e.g. `item/{Key}`) disappears after any `CREATE OR MODIFY MICROFLOW` \u2014 including one that only edits the body. `mxcli check`, `mx check` and mxbuild all report success before and after; the loss is visible only in Studio Pro (#1120)", "cause": "`microflowToGen` wrote `out.SetUrl(\"\")` and `out.SetUrlSearchParametersQualifiedNames(nil)` unconditionally in its `major >= 10` block, `microflowFromGen` never read either back, and `sdk/microflows.Microflow` had no field to hold them \u2014 so the value had no path across a rewrite at any of the three layers", "file": "`mdl/backend/modelsdk/microflow_write.go` (microflowToGen), `mdl/backend/modelsdk/microflow.go` (microflowFromGen), `mdl/executor/cmd_microflows_build.go`, `sdk/microflows/microflows.go`", "fix": "Carry `Url`/`UrlSearchParameters` the way AllowConcurrentExecution/MarkAsUsed/ApplyEntityAccess already are: field on the semantic microflow, read in microflowFromGen, written from the model in microflowToGen, and seeded from the stored microflow in the executor's rewrite path. DESCRIBE emits a `-- URL: \u2026` note, because a describe -> rename -> exec COPY still has nothing to preserve from", "insight": "This is the fifth property in `microflows.Microflow` lost this way and the first with NO checker behind it, which is what made it a user report rather than an internal find. The earlier four were all caught by a build error eventually (CE4899 for the concurrency flags, CE0122 for Excluded) or by a security review (ApplyEntityAccess); a microflow with no URL is simply a valid microflow, so every gate stays green and only a human opening Studio Pro can see it. **Generalisable**: when auditing a rebuild for guard-don't-drop, rank the constants it writes by whether a checker would notice their absence \u2014 the ones nothing checks are the ones that reach users, and they are exactly the ones a 'does this look like configuration?' audit skips. The mechanical version is to diff a Studio Pro document key by key against the writer's output; here `grep 'out.Set.*(\"\")\\|(nil)' microflow_write.go` finds the whole remaining set in one line (`ExportLevel` pinned to \"Hidden\", `ConcurrencyErrorMicroflow`/`ConcurrencyErrorMessage` emptied \u2014 both still unguarded, though CE4899 makes the concurrency pair loud). Measured control: reverting either half (SetUrl or the read) alone fails TestMicroflowRoundTrip_DeepLinkURL with the reported symptom, so both halves are load-bearing"} -{"area": "mdl/backend", "date": "2026-09-17", "symptom": "A microflow's **export level** (Studio Pro's Hidden/API switch \u2014 whether it is part of the module's public surface when the module is exported as a package) is reset to `Hidden` by any `CREATE OR MODIFY MICROFLOW`. Every checker stays green, because a hidden microflow is a valid microflow; the module's API is simply smaller", "cause": "`microflowToGen` wrote `out.SetExportLevel(\"Hidden\")` unconditionally, `microflowFromGen` never read it back, and `sdk/microflows.Microflow` had no field \u2014 the identical three-layer gap as the deep-link URL in the same function", "file": "`mdl/backend/modelsdk/microflow_write.go` (microflowToGen), `mdl/backend/modelsdk/microflow.go` (microflowFromGen), `mdl/executor/cmd_microflows_build.go`, `sdk/microflows/microflows.go`", "fix": "Same carry as the URL, plus a DEFAULT: `\"\"` is not a member of `MicroflowsExportLevel`, so an empty model value is written as `Hidden` rather than passed through (the precedent is `json_write.go`). DESCRIBE emits `-- Export level:` only when the value is not `Hidden`", "insight": "Found by running the mechanical audit the URL fix prompted \u2014 `grep 'out.Set.*(\"\\|(nil)' microflow_write.go` over the one function \u2014 which is the cheap move after any instance of this class and turned up three more constants in one line. **The measurement that shaped the fix**: three real marketplace modules (Business Events 3.12.0, External Database Connector 6.2.3/6.3.0) store `Hidden` on 3 of 3 microflows and 55 of 55 documents overall, all three exporting at module level `Source` \u2014 so the hardcoded value was not wrong, it was a default masquerading as a constant. That is the shape of the trap: the audit finds the constant, but only a reference document tells you whether to carry it, default it, or leave it alone. A marketplace `.mpk` is a free source of these \u2014 `unzip -o pkg.mpk project.mpr` gives a real Studio Pro-authored MPR to query, no Studio Pro and no network needed (`mx-modules/` holds three). **Never carry an enum-valued property straight through without a default**: a stored document that says nothing reads as `\"\"`, and writing `\"\"` back is precisely the unloadable-model write CLAUDE.md warns about \u2014 mxbuild tolerates it and Studio Pro throws at MprProperty.cs. Controls: pinning the writer back, stubbing the reader, and neutralising the executor carry each fail a different test with the reported symptom"} -{"area": "mdl/backend", "date": "2026-09-17", "symptom": "Unit tests for a carried microflow property (URL, export level, concurrency) all pass, and the end-to-end behaviour against a real project is still unverified \u2014 the integration gate that would have caught it, `TestMxCheck_DoctypeScripts`, `t.Skip`s whenever `mx` is absent, which is every run in a fresh container", "cause": "Two separate measurement errors, both invisible to `go test`. (1) The test fixture paired `Url: \"item/{Key}\"` with `UrlSearchParameters: [\"\u2026.Key\"]` \u2014 the SAME parameter \u2014 which mxbuild rejects as **CE5612**: a parameter used in the URL path may not also be a search parameter. Nothing in a unit test validates the model, so the fixture described a document Mendix refuses to build. (2) `bin/mxcli` was stale: `go build ./mdl/...` and `make test` had been run after each fix, but not `make build`, so the end-to-end run exercised a binary predating two of the three commits", "file": "`mdl/backend/modelsdk/microflow_roundtrip_flags_test.go`, `mdl/executor/microflow_carried_properties_test.go`, `mdl/executor/roundtrip_doctype_test.go` (the skipping gate)", "fix": "Fixture uses a distinct `Filter` parameter and says why. End-to-end procedure that actually measures it: `mxcli setup mxbuild -p ` (~719 MB, works through the session proxy), copy `testdata/expr-checker` as the fixture, create the microflow with mxcli, seed the unauthorable properties straight into the stored unit with `mpr.NewWriter` + `UpdateRawUnit`, then `mx check` BEFORE (the fixture must be a document Mendix accepts, or it proves nothing), `mxcli exec` a body-only rewrite, read the unit back, `mx check` after", "insight": "**A skipping integration gate is worse than no gate**: `mxCheckAvailable()` + `t.Skip` means a green `make test` says nothing about mxbuild, and reading the CLAUDE.md line about #808 is not the same as checking whether it applies to your own run \u2014 `ls ~/.mxcli/mxbuild` is. **Rebuild the binary before any end-to-end run**, and check its mtime against the last commit: a stale `bin/mxcli` produced a result (URL survived, export level did not) that looked exactly like a genuine second-read-path defect, and sent me hunting for a duplicate resolver that does not exist. **Seed the fixture through the writer, not by hand-editing BSON**, and always `mx check` the seeded state first: the CE5612 error came from the seed, not from mxcli, and without the before-check it would have been misattributed to the fix. Measured, mxbuild 11.6.6: pre-fix binary rewrites the microflow to `Url=\"\"`, empty search params, `ExportLevel=\"Hidden\"`; post-fix keeps all three; `mx check` 0 errors on both the seeded control and the rewritten project"} -{"area": "mdl/backend", "date": "2026-09-20", "symptom": "Every page mxcli wrote carried `Autofocus` (a property Mendix introduced in 11.1.0) and every page and snippet carried `Variables` (10.17.0), whatever the project's version. On a Mendix 10 project the document therefore held a key that project's metamodel does not declare — the class of defect that makes Studio Pro throw InvalidOperationException at MprProperty.cs while mxbuild reports 0 errors. Found while fixing mendixlabs/mxcli#1121 (the same defect in the page's PARAMETERS) and deferred there as out of scope.", "cause": "`pageToGen` called `SetAutofocus(\"DesktopOnly\")` unconditionally, and both `Forms$Page` and `Forms$Snippet` registered `Variables` in `codec.RegisterTypeDefaults(...).MandatoryLists`, which the encoder emits as an empty typed-array marker for every new element. The page writer had no version awareness at all until #1121 threaded `*types.ProjectVersion` into `pageToGen` for the parameter keys.", "file": "`mdl/backend/modelsdk/page_write.go` (pageSupportsAutofocus/pageSupportsVariables, encodePage, docEncoder), `mdl/backend/modelsdk/snippet_write.go` (encodeSnippet), `modelsdk/codec/encoder.go` (Encoder.OmitKeys). Tests `mdl/backend/modelsdk/page_version_keys_test.go`, `modelsdk/codec/omitkeys_test.go`.", "insight": "**A version-floored key can arrive by two different routes, and only one of them is a gen property.** `Autofocus` is set through gen, so the fix is to not set it; `Variables` exists only because the Studio Pro defaults registry adds it, and a gen `PartList` has no 'present but empty' state — so there is nothing to leave unset and the suppression has to happen in the encoder. That registry is global and keyed by `$Type` alone, so it cannot see a project version: hence `Encoder.OmitKeys`, per-encode. **Suppressing a key and dropping data are different things**: the empty `Variables` marker is safe to suppress, but variables the script DECLARED are refused instead (guard-don't-drop, ADR-0005) — silently dropping them leaves widgets referencing names that are gone (CE1151) from a statement that reported success. Two measurement traps hit on the way: the first control run died on a pre-existing entity before reaching the page statement, so the page was never rewritten and the unchanged output read as a pass — a control that does not run looks exactly like a control that passes; and ALTER PAGE turned out to be safe without any change, because the page mutator edits the stored raw BSON and marshals it back, so it can never invent a key. Check which write paths actually rebuild a document before assuming a header fix has to cover all of them.", "refs": ["#1121"]} -{"area": "mdl/backend", "date": "2026-09-18", "symptom": "`ALTER PAGE M.P { SET Documentation = '...' }` is refused: \"unsupported page-level property: Documentation\". Documenting an existing page therefore means re-running its CREATE, because the `/** … */` doc comment on the create statement is the only other source", "cause": "`applyPageLevelSetMut` handled Title/Url/Popup*/Class/Style and had no Documentation case. The field was otherwise fully understood — `pageToGen` has always written it on CREATE via `out.SetDocumentation`, and gen binds it as `property.NewPrimitive[string](\"Documentation\")`. The grammar already parsed it (`identifierOrKeyword EQUALS propertyValueV3`) and the executor has no allowlist, so it was one missing case in the mutator, not a syntax gap", "file": "`mdl/backend/pagemutator/mutator.go` (`applyPageLevelSetMut`)", "insight": "One case covers **Page, Layout AND Snippet** — all three declare Documentation and all three reach this function through `SetWidgetProperty(\"\")`. Store an empty string rather than rejecting it: removing a doc comment from a script has to be expressible, and the property is a bare string with no unset value. **Update the unsupported-property message in the same change** — that list is the only guidance a reader gets, and a stale one sends them back to the CREATE workaround the fix exists to remove (a test asserts the message names Documentation). Note the re-run workaround is worse than it sounds: describe → exec is only as complete as what MDL can spell, so restating a page to document it can silently lose widgets", "refs": ["#527"]} -{"area": "mdl/backend", "date": "2026-09-20", "symptom": "`describe page` → `exec` over a **Studio Pro-authored** page reports `Replaced page`, not `Unchanged` — the rebuild is not semantically equal to what was stored, so ADR-0008's elision cannot fire and the unit churns in version control on every re-run. `mx check` is 0 errors either way. Measured on ako/TestApp Rules.RuleAction_NewEdit at 11.14.0: fourteen differences", "cause": "Four independent classes, all 'the rebuild writes a constant where Studio Pro stores a value': (1) `pageToGen` hardcoded Autofocus/CanvasWidth/CanvasHeight; (2) save_changes/cancel_changes/close_page/delete_object never wrote `DisabledDuringExecution`, and save_changes wrote `SyncAutomatically` true; (3) `AttributeRef.EntityRef` emitted only on the navigated branch; (4) `Forms$PageVariable` had only the one name field set, so the other five keys were never marked dirty", "file": "`mdl/backend/modelsdk/page_write.go` (`carryStoredPageHeader`, `bsonInt`), `widget_write.go` (four action cases + two `RegisterTypeDefaults`), `modelsdk/codec/defaults.go` + `encoder.go` (new `FalseFields`)", "insight": "**mxcli round-tripping its own output proves NOTHING about this class** — measured: the MDL bug-test reports `Unchanged` on the unfixed build too, because mxcli writes the page and mxcli describes it, so its constants agree with themselves. The reference must be a Studio Pro document; a committed CI fixture only works if it is one. **A population selected by name can confirm whatever it excluded**: the first sweep filtered `$Type` on `endswith(\"ClientAction\")`, got a tidy 'True on 82 of 82', and so missed `Forms$NoAction` (False on 83 of ~7,300) and `Forms$MicroflowAction` (5 of 81) — scan by the PROPERTY, not by a name pattern. **Hardcoded looked safe and was not**: CanvasWidth takes seven distinct values across 67 pages and the hardcoded 1200 matched 4, so a round trip moved the canvas of 63. **The int width bit this fix once**: Studio Pro stores both canvas dimensions as int64 while the gen setter takes int32, so the natural `.(int32)` assertion matched nothing — and the first unit test passed anyway because its own fixture wrote int32, i.e. the test encoded the assumption under test (bson-numeric-width). Prefer `TypeDefaults` over patching each construction site: `Forms$PageVariable` is built in three places. Remaining after the fix: 1 of 14, a pluggable-widget Object property — CE0463 territory, deliberately out of scope", "refs": ["#541", "#529"]} -{"area": "mdl/backend", "date": "2026-09-20", "symptom": "An access rule on an entity carrying `AutoOwner` (or `AutoChangedBy`) made mxbuild report the whole module as **CE0066** \"Entity access is out of date\" \u2014 and `UPDATE SECURITY`, the documented repair for exactly that error, printed `Reconciled 1 access rule(s) in module Mod` and left the error standing. Four lines reproduce it on a clean production-security app: `alter entity M.Fab add attribute Owner: AutoOwner;` + `update security;`. The original reporter bisected 9 entities and 36 rules one grant at a time behind a ~40s `mx check` to find it, because CE0066 names only the module.", "cause": "mxcli wrote a MemberAccess for the implicit `System.owner` / `System.changedBy` association, in TWO places that had to agree: the GRANT handler (`cmd_security_write.go`) and `ReconcileMemberAccesses`. Mendix maintains those members itself and treats a rule naming one as out of date. The audit DATE members were already known to work this way (issuetracker #20) \u2014 the owner/changedBy pair was assumed to be the opposite case because they are associations rather than attributes, and Mendix really does add them implicitly. Fixed by writing no entry for any of the four, and by REMOVING a stored one in the reconcile (an explicit case before the foreign-module branch, which otherwise preserves `System.*` forever on the grounds that System is not loaded).", "file": "`mdl/backend/modelsdk/domainmodel_security_write.go` (isAuditMemberRef, ReconcileMemberAccesses), `mdl/executor/cmd_security_write.go`", "insight": "**The decisive probe was removing the entry, not adding anything.** CE0066 says 'out of date', which reads as 'something is missing' and sends you looking for a member to add; the model had one too many. A build flag (`MXCLI_PROBE_NO_SYSOWNER`) that dropped the entry took the module from CE0066 to 0 errors in one mxbuild run and settled it. The same repo's earlier finding had already written the rule down \u2014 *'Ask mxbuild what it wants instead of inferring symmetry'* \u2014 and this defect is that exact inference, made in the same file for the sibling members. **A fix here is not done when the new writes are correct**: `update security` exists to repair a project an older mxcli damaged, so the reconcile has to remove the entry, not merely stop adding it. Measured separately: a stale `System.owner` entry survived even after the flag was turned off, because `!assocRefBelongsTo` preserved it as an unverifiable foreign-module reference.", "refs": ["#554", "#524", "issuetracker #20"]} -{"area": "mdl/backend", "date": "2026-09-20", "symptom": "`DROP ENTITY` left every CROSS-MODULE association pointing at the deleted entity in place. Dropping the local BY-ID (FROM) end made mxbuild 11.14.0 unable to LOAD the project: `System.AggregateException \u2026 (The given key '' was not present in the dictionary.)` at `StreamingBsonUnitReader.ResolvePostponedProperties()` \u2014 no CE code, no document named, so the obvious reading is 'the project is corrupt, restore from git'. Dropping the BY-NAME (TO) end is milder and still wrong: CE1613 at the cross-module association. `show associations` shows a raw GUID where the parent entity should be.", "cause": "`removeAssocsReferencing` swept `dm.AssociationsItems()` and asserted `*genDm.Association` per item, so the SEPARATE `CrossAssociations` collection was never looked at. Fixed with `removeCrossAssocsReferencing`, matching BOTH ends because a cross-module association addresses them differently \u2014 FROM by element id (local), TO by qualified name (another module) \u2014 called in DeleteEntity locally and in its cascade over the other domain models.", "file": "`mdl/backend/modelsdk/domainmodel_alter.go` (removeCrossAssocsReferencing, DeleteEntity)", "insight": "**Reported against a view entity; nothing about it was view-entity specific.** The reporter met it dropping view entities (whose associations are DERIVED from OQL, so there is no CREATE ASSOCIATION to undo) and filed it that way. The first probe \u2014 a view entity and its source entity in the SAME module \u2014 did not reproduce at all, and that negative is the useful one: it says the variable is cross-module, not view-ness. A plain `create association A.X from A.X to B.Y` plus `drop entity A.X` reproduces the identical crash. Two lessons: when a repro fails, vary the dimension the report did not mention before doubting the report, and treat a collection-typed `.(*T)` assertion in a cascade as a place where a sibling type hides. mxbuild's diagnostic distinguishes the two ends for free \u2014 a dangling 16-byte pointer is a LOAD crash, a dangling qualified name is CE1613 \u2014 so testing only one end proves half the fix.", "refs": ["#553", "#556"]} -{"area": "mdl/backend", "date": "2026-09-21", "symptom": "`alter settings workflows add group 'Auditors'` reports \"Added workflow group: Auditors (3 group(s))\" and writes nothing \u2014 `show workflow groups` still lists 2, and `mx check` is 0 errors either way", "cause": "`UpdateProjectSettings` overlays the workflows part field by field onto the PRESERVED raw part, so a child LIST that nothing rebuilds is carried through from disk unchanged. Adding `Groups` to the semantic model and to the read path is not enough; the write needs `settingsoverlay.WorkflowGroups(ws, rawPart)`. Identical shape to the enabled-language list the same function already documents", "file": "`mdl/backend/modelsdk/settings_write.go` + `mdl/settingsoverlay/settingsoverlay.go` (`WorkflowGroups`)", "insight": "For anything under Settings$ProjectSettings, the executor's success message proves NOTHING \u2014 it reports the in-memory model, and the overlay is where a list quietly fails to land. Assert on the re-read document, not the handler's output. Two more things a reference project settles in one dump and a guess gets wrong: the `Groups` typed-array marker is 2, not the 3 every other settings child list uses (`ArrayMarker` preserves a stored one, but the fallback matters on a fresh list), and the element's `$ID` is the RUNTIME's identity \u2014 a booted 11.13.0 app keys `system$workflowgroup.modelguid` on it, byte-identical once the .NET GUID field order is undone, so re-minting it on a description edit would orphan every group membership with a perfectly valid model. Control: deleting the one overlay call reproduces the symptom verbatim. mendixlabs/mxcli#272", "refs": ["mendixlabs/mxcli#272"]} -{"area": "mdl/backend", "date": "2026-09-21", "symptom": "`CREATE OR MODIFY VIEW ENTITY` that changed ONLY the OQL printed `Unchanged view entity: \u2026` while `describe entity` showed the new query stored. Changing the attribute list as well reported `Modified` correctly, which is why it hid. Also: the OQL document's unit was replaced under a FRESH GUID on every run, even a byte-identical one, so an MDL-generated project could never come back clean in git (one of the four units #556 measured).", "cause": "A view entity's OQL lives in a separate `DomainModels$ViewEntitySourceDocument` unit, and the executor DELETED it and INSERTED a fresh one on every write. `ReportMutation` downgrades the verb when writes were offered and none landed, but the counters are incremented only at the update choke points (`writer_core.go` reconcileWithStored / MoveUnit) \u2014 `InsertUnit` is not counted at all. So the domain-model unit was offered and correctly elided, the OQL write was invisible, and the report believed the half it could see. Fixed with `WriteViewEntitySourceDocument`, which keeps the stored unit's id and goes through `UpdateRawUnit` \u2192 reconcile: an identical query is elided, a changed one lands and is counted, duplicates are still cleared.", "file": "`mdl/backend/modelsdk/move_view_write.go` (WriteViewEntitySourceDocument, encodeViewEntitySourceDocument), `mdl/executor/cmd_entities.go`", "insight": "**The first fix that comes to mind \u2014 count InsertUnit \u2014 would have swapped a false \"Unchanged\" for a false \"Modified\".** Measuring before changing is what caught it: re-running a BYTE-IDENTICAL script still re-minted the source document's unit id, so counting inserts would have made every view-entity statement report Modified forever. The right fix was the one ADR-0008 already mandates (wire the write path to canon.Reconcile), and it fixes the churn and the verb together. Generalisation worth remembering: any content that reaches storage through `InsertUnit` is invisible to the elision check, so a statement whose only landing write is a NEW unit can still be mis-reported \u2014 `MoveUnit` has a comment explaining it was counted for exactly this reason, and insert/delete were missed. Control the fix on the identical re-run, not just the changed one.", "refs": ["#583", "#556", "#910"]} -{"area":"mdl/backend","date":"2026-09-22","symptom":"modelsdk/mpr/version.ProjectVersion declared its own struct with the same seven fields as mdl/types.ProjectVersion instead of aliasing it, so a *version.ProjectVersion could not be passed where a *types.ProjectVersion was wanted and vice versa — two unrelated Go types that both print as 'ProjectVersion'.","cause":"The deleted sdk/mpr/version aliased the canonical type (`type ProjectVersion = types.ProjectVersion`); this copy declared a duplicate. CLAUDE.md's shared-types rule asks for the alias, and nothing enforced it. The duplication survived the legacy-engine retirement because it compiles perfectly — the two declarations are field-for-field identical, so only an assignment ACROSS the boundary reveals them as different types.","file":"modelsdk/mpr/version/version.go","fix":"Made it an alias. The four methods it redeclared (IsAtLeast, IsAtLeastFull, String, IsMPRv2) were verified semantically identical to types' first — IsAtLeast differed only in early-return style, same truth table — and now come from types. IsSupported/SupportsFeature could not survive as methods on an aliased type and had ZERO callers anywhere (measured), so they went with Feature, MinVersion, featureVersions and SupportedVersionRange; that map called itself 'the fallback when the YAML registry is unavailable' and the live registry is sdk/versions/mendix-{9,10,11}.yaml via checkFeature.","insight":"A same-shape duplicate type is invisible to every signal except an assignment across the package boundary: it compiles, tests pass, and the error it eventually produces names the same type on both sides of 'want'. So the guard is a COMPILE-TIME assertion, not a runtime test — `var _ *types.ProjectVersion = (*version.ProjectVersion)(nil)` builds only under an alias and fails to build under a duplicate, which is strictly stronger than anything a test body can assert. Write it before the fix and watch it fail to compile; that failure IS the reproduction. Two measurements that made the cleanup safe rather than brave: diff the method BODIES before assuming the redeclarations are redundant (identical behaviour, different style, is the common case and the dangerous one is the near-miss), and count callers of anything the alias forces you to drop — here six exported symbols had zero. Unrelated trap hit while verifying: four cmd/mxcli tests that read skill files failed once in a full `go test ./...` interleaved with `make check-mdl`, which runs sync-skills (rsync --delete into cmd/mxcli/skills/). They pass in isolation, on clean main, and in an uninterleaved full run — do not attribute a skills-reading test failure to your change without re-running it alone."} -{"area": "mdl/backend", "date": "2026-09-22", "symptom": "`create workflow … overview page X` reports `Created workflow` and exit 0 and stores NOTHING — the written unit carries no page reference and not even the page's qualified name as a string. `mx check` passes (a workflow with no overview page is valid) and `describe workflow` omits the clause, so nothing reveals the loss. Running `alter workflow … set overview page X` afterwards DOES write it, which is what makes the split visible", "cause": "Two fields for one concept, never joined: the executor set semantic `Workflow.OverviewPage` (`cmd_workflows_write.go:170`) and `workflowToGen` only ever read `Workflow.AdminPage`, which nothing set. The READ half was wrong in the mirror direction — `workflowFromGen` took `g.OverviewPageQualifiedName()`, so even the correctly-written ALTER read back empty and the catalog's overview-page reference edge never fired", "file": "`sdk/workflows/workflow.go` (the two fields collapsed to one), `mdl/backend/modelsdk/workflow_write.go` (`workflowToGen`), `mdl/backend/modelsdk/workflow_read.go` (`workflowOverviewPageName`)", "insight": "**The Model SDK's StructureVersionInfo settles which of two rival property names is real, in one grep**: `npm pack mendixmodelsdk` then `src/gen/workflows.js` gives `overviewPage: {deleted: \"9.11.0\"}` and `adminPage: {introduced: \"9.11.0\"}` — so AdminPage (a `Workflows$PageReference` CHILD, not a by-name string) is the stored property, and `generated/metamodel` agrees by declaring AdminPage and no OverviewPage. `modelsdk/gen` declares BOTH, which is how a reader and a writer ended up on opposite sides of a 9.11 rename inside one package. **The version branch CLAUDE.md's overlay rule would demand is dead here, and that is a measurement not an assumption**: `workflowToGen` writes `WorkflowV2`, introduced in 11.1.0, unconditionally — so no reachable project wants the pre-9.11 key. Write one spelling, READ both (a read fallback invents nothing). **The differential that proves it on a real build**: same script, same project, only the write suppressed — control 0 errors, fixed `CE7410 \"The selected page 'Overview' should accept a parameter of type 'Workflow'\"` on mxbuild 11.6.6. mxbuild can only validate a page it can see, so the error IS the evidence; with a valid overview page both variants are 0 errors, which is the usual weak-signal trap. Useful side-finding: an overview page takes **System.Workflow**, while a user task's page takes **System.WorkflowUserTask** — two pages, two parameters. NOT fixed: no check rule for CE7410 yet, and `WorkflowV2` being written unconditionally is questionable for a 10.x project. Same shape as the `create … comment 'text'` bug (findings/mdl-grammar.jsonl 2026-08-25): grep for `stmt.X = …` / `wf.X = …` with no matching read. Tests `mdl/backend/modelsdk/workflow_overview_page_test.go`; repro `mdl-examples/bug-tests/workflow-586b-overview-page-dropped.mdl`", "refs": ["ako/mxcli#586"], "ce": ["CE7410"]} -{"area": "mdl/backend", "date": "2026-09-23", "symptom": "`describe java action` prints `ContextObject: entity <>` for a parameter declared `entity not null`; a bare type-parameter reference (`Obj: pEntity`, `returns pEntity`) reads back nameless too. The description no longer round-trips", "cause": "The stored parameter type (`CodeActions$EntityTypeParameterType` / `ParameterizedEntityType`) holds only a BY_ID pointer to the `CodeActions$TypeParameter`. `javaActionFromGen` carried the ID into the semantic type but never resolved it to the name, and the name is all the describer prints. `javascript_read.go` had always done this resolution pass; the Java reader was ported without it", "file": "`mdl/backend/modelsdk/java_read.go` (`resolveJavaActionTypeParameterNames`)", "insight": "The executor's `entity <>` fallback in `formatJavaActionType` is the tell: an empty name at DESCRIBE means the *reader* dropped a by-ID resolution, not that the writer lost it \u2014 the write path sets both ID and name, so a create\u2192read unit test in the backend reproduces it without any project. When a JS and a Java reader cover the same `CodeActions$` shapes, diff their post-processing first; any pass one has and the other lacks is a candidate. The write was never wrong (replaying the fixed description into a fresh project describes identically), so no `mx check` run is needed. Issue mendixlabs/mxcli#1034", "refs": ["mendixlabs/mxcli#1034"]} -{"area": "mdl/backend", "date": "2026-09-23", "symptom": "`CREATE OR MODIFY PERSISTENT ENTITY` was REFUSED by the #1119 storage-GUID guard on a doctype script that had been passing for months: `failed to update entity: refusing to write unit d82b0484-…: 1 element(s) kept their $ID but would be written with a different GUID — dff2ced1-… (DomainModels$Attribute): stored 4b52b36b-…, would write dff2ced1-…`. Two independent defects wore that one message.", "cause": "(1) A REAL data loss the guard caught: `mergeDeclaredOntoStoredEntity` sets `merged.Attributes = declared.Attributes` and `merged.Indexes = declared.Indexes` — the lists the STATEMENT declares, built from text by the visitor and carrying no element ID — and `carryChildIdentity` keyed entirely on that ID, reading an empty one as 'a genuinely new member, so a fresh GUID is right'. Every attribute of a re-declared entity was therefore re-minted, i.e. #1119 through a second executor path. (2) A FALSE POSITIVE in the guard itself: `canon.TransplantIDs` pairs STRUCTURALLY ($Type + shape, LCS-anchored), so on a statement that drops six differently-named attributes and adds one, it paired the NEW attribute with a REMOVED one and handed it that stored `$ID`; the codec had written `GUID = $ID` and the transplant substitutes over every 16-byte binary, so the GUID followed. The guard's premise — written into its own doc comment as 'unambiguously' — that a shared `$ID` after the transplant means the same element, is false.", "file": "`mdl/backend/modelsdk/domainmodel_child_identity.go` (`carryAttributeIdentity`, `carryIndexIdentity`), `modelsdk/canon/storageguid.go` (`sameMember`, `elementGUIDs`)", "insight": "ONE ERROR MESSAGE, TWO DEFECTS, AND FIXING EITHER ALONE LEAVES IT RED — which is why the first fix (the name fallback) changed nothing and the SAME element and GUIDs came back byte-for-byte. That repetition was the signal: an identical failure after a real fix means the reproduction is exercising a different code path than the one reasoned about. What settled it was describing the actual subject: the marketplace `PublishedBusinessEvent` has six attributes and NONE is named `EventId`, so there was no member to carry — the pairing itself was spurious. Reproduce against the real stored document before believing any theory about which elements correspond. METHOD that made this cheap: the CI failure reproduced locally in 0.5s as a backend unit test (strip the IDs off a fixture entity's attributes, call UpdateEntity) versus 26s for the integration subtest, but ONLY the integration subtest could have found the second defect, because the false pairing needs a real drop-six-add-one document. Run both. TRADE-OFF worth restating: the guard now pairs on `$ID` + `$Type` + `Name`, which loses one arm — a RENAME that re-mints a GUID is no longer refused, since the name is what changed — and that arm is covered directly by the carry tests where it is decidable. A backstop that refuses correct writes is worse than a backstop with a hole: the first makes documented statements unusable, and this one already had. Also: feeding a deliberately-approximate pairing to a guard promotes its error rate into refusals. TransplantIDs' correctness bar is low ON PURPOSE (a wrong match only makes a diff bigger); anything that reads its output as identity has to add its own test of identity.", "refs": ["mendixlabs/mxcli#1119", "mendixlabs/mxcli#1169", "ako/mxcli#643"], "ce": []} -{"area":"mdl/backend","date":"2026-09-25","symptom":"A data view with `DataSource: nanoflow Module.NF` (e.g. describe → exec of Feedback v4.0.2's FeedbackModule.ShareFeedback) passes `mxcli check` and exec, then mxbuild 11.13.0 reports CE2633 \"No nanoflow configured for the data source of this data view\". Same result through `alter page … set DataSource = nanoflow X on dv`","cause":"Both writers nested the name in a `Forms$NanoflowSettings` child (ParameterMappings marker 3) by analogy with `Forms$MicroflowSource`, which really does nest `Forms$MicroflowSettings`. Studio Pro's `Forms$NanoflowSource` is FLAT: ForceFullObjects, Nanoflow, ParameterMappings (marker 2) directly on the source — exactly gen's shape. mxbuild found no Nanoflow key. The raw nested builder also never read d.ParameterMappings, so a parameterized source nanoflow lost its arguments. On the read side, the ALTER PAGE flow-context lookup (`flowFromDataSourceDoc`) and describe's argument reader (`flowSourceArgs`) only knew the nested shape, so Studio Pro-authored nanoflow sources yielded no entity context / no arguments","file":"`mdl/backend/modelsdk/widget_write_legacy_gaps.go` (`nanoflowSourceToGen` via gen + `Forms$NanoflowSource` TypeDefaults in `widget_write.go`), `mdl/backend/pagemutator/mutator.go` (`serializeDataSourceBson`, `flowFromDataSourceDoc`), `mdl/executor/cmd_pages_describe_datasource.go` (`flowSourceArgs`)","insight":"**The code comment asserted the wrong shape as a measured fact** (\"Studio Pro nests it in a Forms$NanoflowSettings child … Legacy's shape is the one with a working project behind it\") and a unit test pinned it — both were parity-with-legacy, never measured. `Forms$NanoflowSettings` is not a type in modelsdk/gen or generated/metamodel: **when gen and a hand-rolled builder disagree about a type's shape, grep gen for the type the builder invents before trusting the builder**. What settled it in one step: a 60-line scanner that `bson.Unmarshal`s every mprcontents unit and prints the key-set (with list markers) of each `$Type` instance — 5 of 5 flat nanoflow sources, and 4 of 4 microflow sources nested as gen says, so the microflow path needed nothing. **Enumerate every writer of the type, not just the reported one**: the ALTER PAGE setter had its own copy of the same wrong literal, and the read-side lookups keyed on the wrong shape meant Studio Pro pages were the ones silently mis-read. Readers keep the nested fallback for pages written before the fix. Verified: exec + `mx check` 11.13.0 CE2633 → 0 errors for CREATE PAGE (ShareFeedback round trip, repro script) and ALTER PAGE; ShareFeedback's dataView5 DataSource ndsl now matches Studio Pro exactly. Control: implementation reverted → the 5 new tests fail with the nested key set. Repro `mdl-examples/bug-tests/dataview-nanoflow-source-ce2633.mdl`","refs":[],"ce":["CE2633"]} -{"area": "mdl/backend", "date": "2026-09-25", "symptom": "`returns list of pEntity` for a declared type parameter produced mx check CE1613 \"The selected entity '.pEntity' no longer exists.\" on the action and on a `list of pEntity` parameter; a Studio Pro \"List of \" read back as a bare `List` (DESCRIBE and catalog) and a rewrite of it would serialize a list of an unnamed entity.", "cause": "types.ListType carried only Entity. The reader handled only a ConcreteEntityType list element, the writer always emitted one, and CREATE sent `list of T` down the entity path (Module \"\" + \".\" + T).", "file": "mdl/types/javaaction_types.go (ListType.TypeParameterID), mdl/backend/modelsdk/java_read.go (listTypeFromGen), java_write.go (codeActionListTypeToGen), javascript_read.go, mdl/executor/cmd_javaactions.go (listOfTypeParameter)", "insight": "The Model SDK is the arbiter for which element a slot accepts: `ParameterizedEntityType.createInListTypeUnderParameter` (metamodel 7.21.0+) settles that a list element may be a type parameter, so no version gate. No fixture had a Studio Pro-authored instance, so the evidence is mx check on 11.6.6: previous build CE1613 x2, fixed build 0 errors, clean baseline 0. The JavaScript writer reuses the Java converter, so one write fix covers both; the JS reader is separate raw-map code and needed its own case.", "refs": ["mendixlabs/mxcli#1183"]} -{"area": "mdl/backend", "date": "2026-09-26", "symptom": "`ALTER PAGE … SET ImageUrl = '…' ON img1` (pluggable Image) prints \"Altered page\" and changes nothing: DESCRIBE still shows the old URL. Same silent success for SET on any pluggable widget property of kind Expression, Image, Icon, Action, DataSource, Attribute or Widgets", "cause": "setPluggableWidgetPropertyMut (mdl/backend/pagemutator/mutator.go) wrote every value to Value.PrimitiveValue. imageUrl is a TextTemplate; readers take Value.TextTemplate. The DataGrid 2 column setter had the identical defect fixed on 2026-08-18 (columnValueField) — the widget-level setter sitting next to it was never given the same schema dispatch.", "file": "mdl/backend/pagemutator/mutator.go (setPluggableWidgetPropertyMut, buildPropKindMap), test pluggable_property_kind_test.go, example mdl-examples/bug-tests/1201-alter-page-set-pluggable-texttemplate.mdl", "insight": "When a fix lands on one setter for 'write the field the schema declares', grep the sibling setters in the same file for the same always-PrimitiveValue write — a WidgetValue carries every variant field, so the wrong write never errors and every signal stays green. The live control is cheap without mxbuild: copy testdata/expr-checker, create an image with URL A, ALTER to B, describe (pre-fix binary shows A). A null TextTemplate means the slot is hidden (#574); ALTER does not re-run visibility, so refuse rather than build an envelope there. Not fixed here: a pluggable boolean SET stores \"yes\"/\"no\" — check what CREATE stores before changing it.", "refs": ["mendixlabs/mxcli#1201", "mendixlabs/mxcli#1069", "mendixlabs/mxcli#750", "mendixlabs/mxcli#574"], "rules": []} -{"area": "mdl/backend", "date": "2026-09-26", "symptom": "describe → exec of a snippet turns its Type 'Web' into '' (and would reset a non-default ExportLevel or canvas size)", "cause": "snippetToGen wrote Type, ExportLevel, CanvasWidth and CanvasHeight as constants on every write; the Type constant was \"\"", "file": "mdl/backend/modelsdk/snippet_write.go", "insight": "The snippet twin of #541 (carryStoredPageHeader). \"\" is not a member of PagesType (Native | Web), so every snippet mxcli created was also invalid. Carry the stored header on UpdateSnippet (width-agnostic canvas read — Studio Pro stores int64); a new snippet gets Web/Hidden/800×600, measured on all four Blank-template snippets", "refs": ["ako/mxcli#705", "#541"]} -{"area": "mdl/backend", "date": "2026-09-26", "symptom": "describe → exec of a nanoflow detaches every annotation from its activity (AnnotationFlows 3 → 0, so the next describe prints the notes as free-floating) and deletes ExportLevel, UseListParameterByReference and ReturnVariableName; MarkAsUsed is cleared. Rules drop their annotation flows too", "cause": "nanoflowToGen and ruleToGen never wrote ObjectCollection.AnnotationFlows (microflowToGen did), and nanoflowToGen never set the three header keys; nanoflowFromGen did not read ReturnVariableName or MarkAsUsed and the builder hardcoded MarkAsUsed false", "file": "mdl/backend/modelsdk/nanoflow_write.go, rule_write.go, microflow.go; mdl/executor/cmd_microflows_build.go", "insight": "Three flow flavours share the object/flow converters but each has its own top-level writer, and each dropped a different subset — grep every *ToGen for the same list loop, not just the reported one. For keys with no MDL spelling and unknown version floors (Nanoflow.ExportLevel, UseListParameterByReference), carry ONLY the keys the stored unit has: nothing was ever written on create, Studio Pro fills an absent key, and an undeclared one makes the unit unopenable", "refs": ["ako/mxcli#705"]} -{"area": "mdl/backend", "date": "2026-09-27", "symptom": "Running unchanged `describe user role` output (create or modify user role) rewrites Security$ProjectSecurity: /UserRoles[0] marker 2 -> 3; same for DemoUsers on a demo-user rewrite", "cause": "The encoder's default PartList marker is 3; Studio Pro writes 2 for ProjectSecurity.UserRoles and .DemoUsers (PedApp, TestApp, expr-checker agree). An untouched list passes through raw, so it only shows once an element in it is rewritten", "file": "mdl/backend/modelsdk/security_write.go", "insight": "A marker bug hides until the first rewrite of an element in that list; census the stored marker per (owner $Type, key) across Studio Pro fixtures and register it with RegisterPropertyListMarker", "refs": ["ako/mxcli#731"]} -{"area": "mdl/backend", "date": "2026-09-27", "symptom": "Any write of a domain model (even rewriting a sibling entity) drops LocalizeDate from every DateTime attribute in the unit; external entity rewrites also rewrite Source/Key/Parts marker 2 -> 3", "cause": "attributeTypeToGen emitted DateTimeAttributeType without LocalizeDate and attributeTypeFromGen never read it; Rest$ODataKeyPart had no registered list marker so the encoder default 3 applied", "file": "mdl/backend/modelsdk/domainmodel.go, domainmodel_write.go", "insight": "A whole-unit rewrite re-serializes every sibling from the struct, so a property the struct cannot hold is lost on EVERY entity of the module, not just the edited one. Absent LocalizeDate means Mendix's default true (mxcli wrote it absent for years), so the reader must map absent to true or the fix would unlocalize every mxcli-authored DateTime. Only a Studio Pro fixture with DateTime attributes (TestApp) shows it", "refs": ["ako/mxcli#743"]} -{"area": "mdl/backend", "date": "2026-09-28", "symptom": "create or modify consumed odata service over a Studio Pro client: UseQuerySegment true -> false, Icon, CustomLocationTemplate and the empty ApplicationId/EndpointId/CatalogUrl/proxy/microflow keys deleted, MetadataReferences [3] -> [0], ValidatedEntities [1] -> [0]", "cause": "consumedODataServiceToGen writes the document wholesale from a model that holds none of these; placeholders (UseQuerySegment false, ExportLevel Hidden) and addStrIf-omitted empties replaced what Studio Pro stored", "file": "mdl/backend/modelsdk/odata_write.go (carryStoredConsumedODataService)", "fix": "UpdateConsumedODataService lays the fresh document over the stored one: placeholder keys keep the stored value, an omitted optional key is written \"\" when stored (so a cleared value stays cleared), lists keep the stored marker", "insight": "When the model is a strict subset of the stored document, overlay onto the stored bytes rather than growing the model field by field; but an optional key the writer omits must become \"\" rather than be carried, or clearing it through alter silently stops working", "refs": ["ako/mxcli#743"], "test": "TestConsumedODataServiceRewrite_CarriesWhatTheModelDoesNotHold"} -{"area": "mdl/backend", "date": "2026-09-28", "symptom": "Rewriting a Studio Pro workflow drops the empty Flow and the PersistentId of every outcome that leads nowhere, writes RelativeMiddlePoint/Size \"\" where Studio Pro stores \"0;0\", and deletes an empty EventSubProcesses [2]", "cause": "The writer added an outcome's Flow only when non-nil (Workflows$Outcome.flow is Required), gave condition outcomes no PersistentId, and omitted an empty EventSubProcesses list because the property is version-floored (11.8)", "file": "mdl/backend/modelsdk/workflow_write.go (encodeWorkflow, outcomeFlowToGen)", "fix": "Always write an outcome Flow; PersistentId on every outcome; \"0;0\" layout; empty EventSubProcesses via MandatoryListMarkers, suppressed through Encoder.OmitKeys below 11.8 only when the workflow has none (MandatoryListMarkers now honours OmitKeys)", "insight": "Encoder.OmitKeys suppresses a key whatever it holds, so version-floor only the empty default: suppressing the key outright would drop declared sub-processes on a project whose version could not be read", "refs": ["ako/mxcli#743"], "test": "TestWorkflowToGen_EveryOutcomeStoresAFlow"} -{"area": "mdl/backend", "date": "2026-09-28", "symptom": "Every domain-model rewrite deletes MaybeGeneralization HasOwnerAttr/HasChangedByAttr/HasCreatedDateAttr/HasChangedDateAttr = false on Studio Pro entities (#721 B)", "cause": "entityToGen set only the true flags, for parity with the legacy serializer", "file": "mdl/backend/modelsdk/domainmodel_write.go", "fix": "Write all four flags", "insight": "Legacy-writer parity is not Studio Pro parity; the flags were the last getput loss of a Studio Pro external entity, which is why it surfaced in #743", "refs": ["ako/mxcli#721", "ako/mxcli#743"], "test": "TestEntityToGen_WritesEveryGeneralizationFlag"} -{"area": "mdl/backend", "date": "2026-09-28", "symptom": "`alter workflow M.W { drop userTask1 path 1 }` (and the old `drop path 'Path 1' on userTask1`) deleted the user task's FIRST OUTCOME and printed \"Altered workflow\"; `drop path '' on X` deleted the last outcome/path. Measured on TestApp's Studio Pro-authored workflow.Workflow1: 'Good' of Good/Bad/Ugly gone, project still loads, check clean", "cause": "DropPath (wfmutator and the MCP mutator) matched \"Path N\" against the index of whatever Outcomes list the resolved activity had, never checking it was a parallel split; the insert ops' kind check (#415) lived only in the executor, which reads the STORED workflow, and it deliberately skipped the drops because a drop cannot write a wrong type — but it can address the wrong member", "file": "mdl/backend/workflow_members.go", "fix": "backend.CheckWorkflowMemberList (per storage $Type, the member lists the document declares) guards every member op in both mutators; backend.ParallelPathIndex refuses an empty/unnumbered caption and an out-of-range number; validateAlterWorkflowActivityKinds refuses drop path on a non-split at check time; `drop X boundary event` among several is refused under mdl 1 / warns MDL-V1-BOUNDARYDROP under mdl 0", "insight": "A drop cannot corrupt the TYPE of a list, which is why #415's guard left drops out — but an index-addressed drop on a list of another kind silently removes real content. Old unit fixtures built the split and decision as Workflows$UserTask, so the tests could never have seen it; the guard made those fixtures fail first", "refs": ["ako/mxcli#791", "ako/mxcli#415"], "test": "TestTestAppAlterWorkflow_DropPathOnUserTaskIsRefused"} -{"area": "mdl/backend", "date": "2026-09-28", "symptom": "`alter workflow M.W { drop aiAgentTask1 outcome '' }` (and the old `drop condition '' on X`) deleted the activity's default outcome and printed \"Altered workflow\"; on a parallel split the same statement deleted the first path. Measured on TestApp's Studio Pro-authored workflow.Workflow1 (aiAgentTask1 Outcomes 2 -> 1 items)", "cause": "DropOutcome/DropBranch (wfmutator and the MCP mutator) compared the address with DGetString(Value) / a lenient JSON unmarshal, which read an outcome that stores NO string value (ParallelSplitOutcome, VoidConditionOutcome, BooleanConditionOutcome) as \"\", so an empty value matched the first valueless member", "file": "mdl/backend/wfmutator/mutator.go", "fix": "a value address matches only an outcome whose Value is a stored string (wfmutator: type assertion on DGet; mcp: pedOutcomeElem.hasValue requires a JSON string); 'Default' still names a void outcome by type", "insight": "The #791 review claimed value-addressed drops cannot land on another kind's member; they can when the accessor maps 'absent' to the zero value. Any match of an author-supplied string against a getter that returns \"\" for missing is an address that silently hits the first member lacking the field", "refs": ["ako/mxcli#791"], "test": "TestTestAppAlterWorkflow_DropPathOnUserTaskIsRefused"} -{"area": "mdl/backend", "date": "2026-09-28", "symptom": "ALTER ASSOCIATION (owner, storage, comment, delete behaviour) and CREATE OR MODIFY ASSOCIATION on a cross-module association print success and write nothing; DESCRIBE shows the old values (#792)", "cause": "UpdateDomainModel rebuilt Entities and Associations and left CrossAssociations as raw passthrough under a stale comment claiming they were not in the semantic model — crossAssocFromGen had been filling them on every read", "file": "mdl/backend/modelsdk/domainmodel_alter.go", "fix": "patchCrossAssociations: edit each stored CrossAssociation in place, setting only properties that differ (GUID, export level, capabilities never touched); restrict side gets its refusal text, leaving restrict rebuilds the delete behaviour under its $ID so the message slot is null; a stored one absent from the semantic model is refused. Executor reads the association back after ALTER and errors 'change not persisted' on mismatch", "insight": "A whole-model update that rebuilds some lists and passes others through is only as correct as its list of what the semantic model carries; when a read converter exists for a list, the write side must consume it. The executor's success message was never evidence — a read-back is. A cleared gen Part omits the key; to get Studio Pro's explicit null, rebuild the element so the registered NullFields apply", "refs": ["ako/mxcli#792"], "test": "TestIssue792_UpdateDomainModelPersistsCrossAssociations, TestIssue792_CrossAssociationLeavingRestrictClearsMessage, TestAlterAssociation_RefusesToReportAWriteThatDidNotLand"} -{"area": "mdl/backend", "date": "2026-09-29", "symptom": "describe page/snippet -> exec on a Studio Pro page silently removes a button's confirmation (ConfirmationInfo -> nil), its progress bar (ProgressBar Blocking/NonBlocking -> None) and progress message, resets a microflow call's Asynchronous/FormValidations, flips DisabledDuringExecution false -> true on actions and on the empty NoAction event slots of every input widget, and deletes OnLeaveAction from check boxes, date pickers and radio button groups; mx check and the build stay green (#721 L2, 18 round-trip allowlist entries)", "cause": "The action settings had no MDL spelling, so clientActionToGen/microflowSettingsToGen wrote constants for all of them (DisabledDuringExecution true, ProgressBar None, null message and confirmation, Asynchronous false, FormValidations All), and three input-widget cases never set OnLeaveAction", "file": "mdl/backend/modelsdk/widget_write.go", "fix": "New `with ( Key: value )` settings list after any page action (grammar actionSettingsV3, visitor validates keys per action type, ast.ActionSettingsV3 -> pages.ActionExecution/FlowCallSettings -> writer); describe prints only non-default settings. NoAction slots have no behaviour to spell, so their DisabledDuringExecution is carried from the stored widget of the same name (carryNoActionExecution in UpdatePage/UpdateSnippet). OnLeaveAction written for CheckBox/DatePicker/RadioButtonGroup. A blank message is a Text with no translations, not en_US ''", "insight": "Split the constants by whether they mean something: a setting with behaviour (a confirmation, a progress bar, disabled-while-running on a real action) gets a spelling so a fresh script can say it too; a value on a slot that runs nothing (DisabledDuringExecution on a NoAction, 946 false vs 380 true on the fixtures) is carried, not spelled. Measure the stored population per slot, not per type: a NoAction under an Action key is true on 20954 of 20984, and majority-false on the input-widget events. The last diff to go was a blank ProgressMessage stored as a Text with zero translations", "refs": ["ako/mxcli#721"], "test": "TestClientActionToGen_MicroflowSettings, TestCarryNoActionExecution, TestWidgetToGen_InputWidgetsWriteOnLeaveAction, TestRenderClientActionMDL_Settings, TestActionSettings_DescribeParsesBackToTheSameAction, TestActionSettings_Refusals; roundtrip allowlists (PedApp 2, TestApp 4 entries struck)"} -{"area": "mdl/backend", "date": "2026-09-29", "symptom": "ALTER ASSOCIATION / CREATE OR MODIFY ASSOCIATION / RENAME / mxcli layout on a Studio Pro module turns every API-exported entity in the module Hidden and rewrites every entity's access rules (50 of 93 elements in TestApp WorkflowCommons changed for a no-op write); ALTER ENTITY and MOVE ENTITY reset the target's (and its attributes') export level the same way. mx check stays clean (#801)", "cause": "entityToGen / attributeToGen / assocToGen SET ExportLevel \"Hidden\", and a property the rebuild sets is dirty and wins over the carried raw bytes; UpdateDomainModel rebuilt every entity and association, not only the one named. Member-access writers set only the reference in use, while Studio Pro writes both Attribute and Association keys (the unused one as \"\"), so every rebuilt rule differed from its stored self", "file": "mdl/backend/modelsdk/domainmodel_carry.go", "fix": "UpdateDomainModel passes an entity/association through as its stored element when the semantic model equals the stored one read back (entityUnchanged/assocUnchanged, ID- and nil/empty-insensitive semanticEqual). Rebuilds of existing entities (UpdateEntity, UpdateDomainModel, MoveEntity) go through carryStoredEntity: raw + child identity + stored ExportLevel (entity and carried attributes) + unchanged access rules substituted by the stored element, changed ones keep $ID/caption/documentation. carryStoredAssociation: raw + ExportLevel + unchanged delete behaviour. DomainModels$MemberAccess registers EmptyStringFields Attribute/Association", "insight": "A raw carry only preserves what the converter does NOT set; every constant a converter writes (here ExportLevel \"Hidden\") is still reset on the carried element, so audit the converter's Set* constants, not only unmodelled keys. The whole-list rebuild shape needs an 'unchanged -> pass the stored element' short-circuit or its blast radius is the unit. Invisible on mxcli-authored content, where the stored value IS the constant: TestApp is the only fixture with API entities (WorkflowCommons) and has no API attribute, so the attribute case was set up on top of it. The roundtrip harness had 87 getput entries (#721 B and TestApp associations) that were this bug", "refs": ["ako/mxcli#801", "ako/mxcli#721"], "test": "TestIssue801_UpdateDomainModelChangesOnlyTheAlteredElement, TestIssue801_OtherEntityRebuildsKeepExportLevel"} -{"area": "mdl/backend", "date": "2026-09-29", "symptom": "describe -> exec, CREATE OR MODIFY or an ALTER that rebuilds the document turns an API-exported document Hidden: enumerations, pages, layouts, rules, view-entity OQL source documents, import/export mappings, JSON structures, published and consumed REST services, scheduled events, workflows, database connections, business event services, data transformers, queues, regular expressions and agent-editor documents. The run reports success, mx check is clean; the module's public surface silently shrinks. A workflow's own `export level API` clause was a no-op on create and on rewrite.", "cause": "Each rewrite converter builds a fresh document and writes ExportLevel as a constant (\"Hidden\"), or passes the semantic model's value where the executor itself filled in \"Hidden\" (mappings, database connection, business events), and the unit is replaced wholesale. MDL has no export-level spelling for most of these kinds, so describe cannot print it and the executed script cannot restore it. workflowToGen ignored wf.ExportLevel entirely. The round-trip harness could not see it: every document in TestApp and PedApp is Hidden, the constant itself.", "file": "mdl/backend/modelsdk/export_level_carry.go", "fix": "One byte-level carry, keepStoredExportLevel(unitID, contents): replaces only the top-level ExportLevel element of the freshly encoded rewrite with the stored value, copying every other element verbatim, and never adds the key. Wired into every Update path that writes ExportLevel (UpdateEnumeration/Rule/Layout/ImportMapping/ExportMapping/JsonStructure/PublishedRestService/ConsumedRestService/DataTransformer/DatabaseConnection/BusinessEventService, writeCustomBlob update, WriteViewEntitySourceDocument update; page via carryStoredPageHeader). Kinds with an MDL spelling (workflow, scheduled event, queue, regular expression) use keepStoredExportLevelUnlessSet: an authored level wins. workflowToGen now writes orDefault(wf.ExportLevel, \"Hidden\").", "insight": "A fixture-driven round trip is blind to any constant that happens to equal every fixture value: 775 TestApp documents round-tripped while 10 kinds hid API documents. Set the subject to the non-default value first (here: patch ExportLevel to API on the working copy) and run both the plain describe output and an edited one, because an elided unchanged write passes a converter that still writes the constant. Carrying at the encoded-bytes level covers gen-typed, newElem-built and hand-serialized writers with one helper, where a gen setter per converter would have needed three mechanisms.", "refs": ["ako/mxcli#816", "ako/mxcli#801", "ako/mxcli#812"], "test": "mdl/backend/modelsdk/issue816_export_level_test.go (TestUpdatePaths_KeepStoredExportLevel, 18 kinds); mdl/roundtrip/export_level_test.go (TestTestAppExportLevelSurvivesRoundTrip, -tags integration)"} -{"date": "2026-09-30", "area": "mdl/backend", "symptom": "ako/mxcli#859 review of PR #864: after the built comparison landed, changing or adding `show page M.P with title = 'X'` in a `create or modify microflow` reported \"Unchanged microflow\" and wrote nothing, under mdl 0 and mdl 1 (main spliced it). Nothing warned.", "cause": "builtAsStored compares the declared flow and the stored flow both READ BACK through the codec, so any property the reader drops compares equal whatever either side holds. The ShowFormAction reader never read FormSettings.TitleOverride. Probing encode(built) against encode(readback(built)) over mdl-examples found the reader also dropped ExclusiveSplit/LoopedActivity ErrorHandlingType and a REST call's bound output variable (ResultHandling.ResultVariableName -> RestCallAction.OutputVariable), plus CallWebServiceAction (#861). Before the built comparison such a loss was a visible phantom re-splice; after it, a silently dropped edit.", "fix": "ReadBackMicroflow/ReadBackNanoflow re-encode what they read back and refuse (error -> statement diff, the pre-#859 path) when it is not the document first written, $IDs aside (sameWritten). The reader now reads TitleOverride, the split's and loop's ErrorHandlingType, and a bound REST call's OutputVariable, so those flows keep matching.", "insight": "A comparison made on both sides through the same lossy reader cannot see what the reader loses; the lost property becomes a change that is never written. When equality is decided after a decode, prove the decode lossless for the value at hand (write it again and compare bytes) and fall back when it is not. The probe that found the fields: diff encode(x) with encode(decode(encode(x))) over every mdl-examples flow.", "issue": "ako/mxcli#859", "file": "mdl/backend/modelsdk/microflow_readback.go, mdl/backend/modelsdk/microflow_read_actions.go, mdl/backend/modelsdk/microflow.go, mdl/roundtrip/flow_idempotent_shapes_test.go"} -{"date": "2026-09-30", "area": "mdl/backend", "symptom": "ako/mxcli#843 (rehearsal M2): under mdl 1, `create or modify nanoflow … returns Boolean as $Done` over a nanoflow stored without a return variable refuses \"the stored document has no ReturnVariableName property … set it in Studio Pro\"; the same statement on a microflow reports \"set: ReturnVariableName\".", "cause": "mfmutator.SetHeader refuses any stated header key the stored document lacks (a key the project version does not declare makes the document unopenable). mxcli's nanoflow writer omits ReturnVariableName when the statement has no `as $Var`, while the microflow writer always writes it on 10+, so only nanoflows hit the refusal.", "fix": "Optional mfmutator.PropertyDeclarer on Deps; the codec deps answer from the metamodel version data (type, then Microflows$MicroflowBase; ReturnVariableName is 10.12+) against the project version, and SetHeader inserts the key after its predecessor in the encoder's order. No answer (MCP, unknown version) keeps the refusal.", "insight": "A refusal keyed on 'the stored document lacks the key' conflates 'this version has no such property' with 'the writer left it out'; the metamodel version data separates the two. Studio Pro 11 stores ReturnVariableName on every nanoflow (PedApp: 13 of 13), so adding it matches what Studio Pro writes.", "issue": "ako/mxcli#843", "file": "mdl/backend/mfmutator/header.go"} -{"date": "2026-10-01", "area": "mdl/backend", "symptom": "mendixlabs/mxcli#1214: `alter page … { set Editable = true on lvRows }` printed \"Altered page\" and the list view stayed Editable = false, so its inputs rendered read-only.", "cause": "pagemutator's `editable` case wrote the value only when it was a string (the input-widget Always/Never enum). A list view's and data view's Editable is a BOOLEAN, so `true` fell through and returned nil — success with nothing stored. A lower-case string ('never') was stored verbatim.", "fix": "setWidgetEditableMut: the stored value's type decides the vocabulary (bool → true/false; string → canonical Always/Never, Conditional only via [expr]); anything else, or a widget with no Editable, is refused. EditableIf now also sets the Conditional enum, as CREATE does.", "insight": "A setter switch that type-asserts and returns nil on a miss is a silent no-op generator; read the STORED field's type first, it says which vocabulary the property has on this widget.", "issue": "mendixlabs/mxcli#1214", "file": "mdl/backend/pagemutator/mutator.go"} -{"date": "2026-10-01", "area": "mdl/backend", "symptom": "ako/mxcli#528 / mendixlabs/mxcli#293: a data view footer could not be edited — `replace footer1 with {…}` (describe's name), the name the script wrote, and `dvMain.footer` all reported not found.", "cause": "A data view footer is a REGION: its widgets live in the data view's FooterWidgets and the footer has no stored Name. The builder hoisted the children and discarded the name; describe invented `footer1`.", "fix": "`.footer` resolves as a region in ResolveAlterTarget/ResolvesTarget; INSERT INTO appends (turning ShowFooter on for an empty footer), REPLACE swaps the content (a describe-style `footer { }` block is unwrapped), DROP empties it; a not-found names the footer addresses. A name on a data view footer is MDL-DEPR005; describe prints `footer { }`. REPLACE may also reuse the names of the widgets it removes (ContainedWidgetNames).", "insight": "Same shape as scroll-container regions: a slot without a name is addressed positionally by its owner. Check the grammar's own doc comment too — it advertised `replace footer1`, a form that never resolved.", "issue": "ako/mxcli#528", "file": "mdl/backend/pagemutator/footerregion.go"} -{"date": "2026-10-01", "area": "mdl/backend", "symptom": "mendixlabs/mxcli#1247: `alter page … replace comboBox12 with { combobox … }` to add a sort reset the combo box's translated placeholder and readOnlyStyle; on TestApp's Studio Pro-authored Rules.BusinessRule_NewEdit it also turned the stored Editable \"Never\" into \"Always\" and an expression property's PrimitiveValue \"\" into \"false\". exec reported success, mx check stayed green.", "cause": "A pluggable widget is rebuilt from its template plus the MDL mappings, so every property MDL cannot state comes back at the template value. #830's passthrough is all-or-nothing and only for unchanged widgets.", "fix": "For one pluggable widget replaced by one of the same package, the executor also builds the STORED widget as describe prints it (baseline); pagemutator.mergeUnstatedPluggable keeps the stored Type and every stored property/field on which replacement and baseline agree, and grafts the differing values with their TypePointers re-aimed at the stored Type by key path. Anything that cannot be lined up falls back to the plain replace.", "insight": "'What the statement states' is measured by building the description of the stored widget beside the statement, not by listing mapped keys: a property that comes out equal from both builds is the template's, not the author's.", "issue": "mendixlabs/mxcli#1247", "file": "mdl/backend/pagemutator/pluggable_replace.go"} -{"date": "2026-10-01", "area": "mdl/backend", "symptom": "mendixlabs/mxcli#1247 on a DataGrid 2: restating TestApp's Studio Pro-authored dataGrid2_1 with one column caption changed silently turned itemSelectionMethod \"rowClick\" into \"checkbox\", reset onClickTrigger and rebuilt every column's and filter's unmapped properties — the combo-box fix's merge did not apply and REPLACE fell back to the template rebuild.", "cause": "remapTypePointers re-aimed every TypePointer in the grafted columns value, including those of the nested filter widgets, which point into their OWN Type; no counterpart -> merge refused -> plain replace. And even when it applied, the columns list was grafted whole, so one changed caption reset every column's unstated properties.", "fix": "remapTypePointers leaves a nested CustomWidgets$CustomWidget as built; an object-list property whose objects line up one for one with the baseline's is merged object by object (properties indexed by their property type's path), so only the stated property of the stated column changes.", "insight": "A fallback that restores the old behaviour is the old bug: test the merge on the most nested pluggable widget available (DataGrid 2 with filters), not only on a flat one, and count the fallbacks.", "issue": "mendixlabs/mxcli#1247", "file": "mdl/backend/pagemutator/pluggable_replace.go"} -{"date": "2026-10-01", "area": "mdl/backend", "symptom": "mendixlabs/mxcli#698: over --mcp, `call microflow … on error rollback|continue` was created with PED's default error handling; a custom handler's error edge was written as an ordinary sequence flow. No error either way.", "cause": "mapMicroflowAction built every action map without errorHandlingType (only notify carried it), and buildFlowDocContent wrote every flow alike — PED's SequenceFlow constructor has no property for an error-handler flow (ped_get_schema, Studio Pro 11.14).", "file": "mdl/backend/mcp/microflow.go (mapObjectTree, buildFlowDocContent)", "fix": "carryErrorHandlingType writes the action's ErrorHandlingType (read by reflection); Custom/CustomWithoutRollBack and IsErrorHandler flows are refused with a message to run without --mcp.", "test": "TestMapObjectTree_CarriesErrorHandlingType, TestMapObjectTree_RefusesACustomErrorHandler", "insight": "ped_get_schema answers 'can PED express this' in one call; check it before mapping a property, and refuse what it cannot hold."} -{"date": "2026-10-01", "area": "mdl/backend", "symptom": "ako/mxcli#628: `move entity A.Parent to B; move entity A.Child to B;` both report success, then Studio Pro cannot open the project: System.AggregateException: The given key '' was not present in the dictionary.", "cause": "MoveEntity scanned only sourceDM.AssociationsItems(); an existing cross-association (created by the first move) was invisible, so moving its FROM entity left its ParentPointer naming an element absent from the unit, and moving its TO entity left Child naming the old module. Cross-unit damage, so the #1119 write guard cannot see it.", "fix": "association_move_cross.go: own cross-associations (FROM = moved entity) travel to the target, or become a plain DomainModels$Association there when the TO entity is already in it (raw transform: $Type, Child -> ChildPointer binary id, connection points added, GUID passed through); a cross-association in the target naming the entity becomes plain; any other module's cross-association naming it is re-pointed. MovedAssociation.SameModule lets the executor report the plain conversions apart.", "insight": "Assert on the stored documents (every ParentPointer/ChildPointer resolves in its own unit, every cross Child resolves to an entity in the named module); mx check is a weak signal for this class. The four sequences (to-then-from, from-then-to, travel, re-point) are distinct shapes, not one.", "issue": "ako/mxcli#628", "file": "mdl/backend/modelsdk/association_move_cross.go"} -{"date": "2026-10-01", "area": "mdl/backend", "symptom": "ako/mxcli#627: the fluent API's AttributeModifier.Apply() (Backend.UpdateAttribute) on a Studio Pro-authored attribute is refused with \"refusing to write unit …: 1 element(s) kept their $ID but would be written with a different GUID (DomainModels$Attribute)\"; without the #1119 guard it would re-mint the GUID and drop the column on the next deploy.", "cause": "UpdateAttribute rebuilds the attribute with attributeToGen (raw == nil) and carried only the $ID, so the codec's EmitGUID default wrote GUID = $ID. No MDL statement reaches it — every ALTER ENTITY form goes through UpdateEntity, which has the carry — so it was found by enumerating the converter's call sites, not by a repro.", "fix": "UpdateAttribute carries the stored attribute's raw bytes and export level onto the rebuild via carryStoredAttribute, the helper now shared with carryAttributeIdentity. Attribute -> Attribute keeps $Type, so SetRaw suffices.", "insight": "Test on PedApp (GUID != $ID); an mxcli-created attribute re-mints the same value and cannot fail. The unfixed code fails the test via the write guard's refusal, which is the observable symptom on main.", "issue": "ako/mxcli#627", "file": "mdl/backend/modelsdk/domainmodel_alter.go"} -{"date": "2026-10-02", "area": "mdl/backend", "symptom": "Studio Pro 11.15 over --mcp: every write in a module fails 'validation failed for ' with another document's error (e.g. an empty enumeration elsewhere); a nonexistent document checks 'No errors found.'", "cause": "11.15 replaced ped_check_errors' documents[] with filters{documentType,documentNamePrefix}+pagination and made all input schemas additionalProperties-permissive, so the old argument is accepted and IGNORED: the check ran project-wide. The answer became a paged listing ('Listing problems a-b (out of n). Check ID: k', then 'Name' (Type): unit headers).", "fix": "checkDocumentNow (mdl/backend/mcp/check_errors.go) sends filters when SupportsToolArg('ped_check_errors','filters'), fetches every page repeating the filters (a page without them lists the whole project), re-runs on a stale window, and keeps only problems under the exact 'Name' (Type): header (documentNamePrefix is a prefix). An unscoped listing from the fallback form is scoped the same way.", "insight": "A permissive schema turns an argument rename into a silent no-op, so 'the call succeeded' proves nothing; the live control needs a deliberately broken document AND a clean one checked in the same run (TestLive_CheckErrorsIsScoped). Gate on the newer shape's argument, never on serverInfo.version.", "file": "mdl/backend/mcp/check_errors.go", "issue": ""} -{"date": "2026-10-02", "area": "mdl/backend", "symptom": "Studio Pro 11.15 over --mcp: DESCRIBE of an entity edited this session shows String(unlimited) for String(200) attributes; reconstructed associations have empty type/owner.", "cause": "11.15's ped_read_document omits every property equal to its schema default (StringAttributeType.length=200, Association.type=Reference/owner=Default, NoGeneralization.persistable=true, BooleanConditionOutcome.value=false); readers mapped absent to Go's zero value, and length 0 means unlimited.", "fix": "attributeTypeFromPED reads length as *int and defaults absent to 200; reconstructAssociations defaults absent type/owner to Reference/Default.", "insight": "Diff the READ output of the same element across releases, not just tool schemas: this change is in one sentence of ped_read_document's description. Any new PED reader must treat an absent property as the schema default (ped_get_schema kind:element shows '= default').", "file": "mdl/backend/mcp/read_router.go", "issue": ""} -{"date": "2026-10-02", "area": "mdl/backend", "symptom": "create workflow \u2026 display 'X' over --mcp stores the document name as the workflow title and workflowName (Studio Pro 11.14 and 11.15).", "cause": "The context-shaped Workflows$Workflow constructor ignores the title (11.14 'caption', 11.15 'title') and workflowName it is given; 11.15 also renamed caption->title, which its permissive schema would have dropped silently anyway.", "fix": "workflowConstructorTakesContext's probe records the title key (workflowCtorTitle); workflowCreateLeafOps sets /title and /workflowName/text after the create (set ops both releases accept).", "insight": "Read back what a constructor stored instead of trusting SUCCESS: both releases accepted the key and applied nothing.", "file": "mdl/backend/mcp/workflow.go", "issue": ""} -{"date": "2026-10-02", "area": "mdl/backend", "symptom": "alter page … set (Caption = '…') on tabPage2 fails with `widget \"tabPage2\" not found` although describe page prints `tabpage tabPage2`; a tab caption missing its default-language translation (mxbuild CE4899 after switching DefaultLanguageCode) could only be fixed by re-creating the whole page.", "cause": "findInWidgetChildren (and its twins findNearestDSInChildren and collectWidgetScopeInChildren) walked TabPages[] only to descend into each page's Widgets[], never matching the Forms$TabPage's own Name — a tab page is a named element that lives in a list that is not a widget list.", "fix": "Walk TabPages through findInWidgetArray / findNearestDSInWidgets / collectWidgetScope like any named list; INSERT INTO a tab page appends to its Widgets, and INSERT BEFORE/AFTER, REPLACE and DROP on a tab page are refused (refuseTabPageSiblingEdit) because they would write widgets into TabPages or orphan the control's DefaultPagePointer.", "insight": "Resolving a new kind of node also hands it to every structural op that trusts parentKey/parentArr: making X addressable means deciding, per op, what writing into X's parent list does.", "file": "mdl/backend/pagemutator/mutator.go", "issue": ""} -{"date": "2026-10-02", "area": "mdl/backend", "symptom": "On a project whose default language is de_DE, alter page … set Caption on a tab page (and set Label on any Studio Pro 11 input widget) reports `Altered page` and stores nothing; set Caption on a button writes the German text over the first translation (en_US); set Title overwrites every language with the same string.", "cause": "Four text writers with four rules: setTranslatableText looked for a `Translations` key no Mendix document has, then DSet a `Text` field DSet cannot add (silent no-op on every real Texts$Text); setWidgetLabelMut read only the legacy `Label` key while 10+/11 store LabelTemplate (a Forms$ClientTemplate); setClientTemplateText wrote Items[0] whatever its language; updateTextsTextValue/updateClientTemplateText wrote all translations. A unit test built the fictional `Translations` shape and asserted nothing, so it stayed green.", "fix": "One helper, setTextsTextTranslation: update or append the Texts$Translation for model.AuthoringLanguage() (the project default DESCRIBE shows), keep other languages; setTranslatableText dispatches on $Type (Texts$Text / Forms$ClientTemplate) and refuses anything else; setWidgetLabelMut tries LabelTemplate first; execAlterPage resolves the authoring language before the first mutation.", "insight": "A text setter's test must build the shape read back from a Studio Pro-authored page (bson dump --format ndsl) and assert the stored translation per language; `err == nil` proves nothing for a setter whose miss path is DSet returning false. Verified live: de_DE switch → CE4899 on tabPage2, set Caption → de_DE added, en_US/nl_NL/ar_DZ kept, 0 errors.", "file": "mdl/backend/pagemutator/mutator.go", "issue": ""} -{"date": "2026-10-02", "area": "mdl/backend", "symptom": "create entity over --mcp stores every attribute as String(200) on both Studio Pro 11.14 and 11.15 MCP servers; String lengths never written (also on alter entity add attribute).", "cause": "pedAttribute sent \"$Type\":\"DomainModels$Attribute\" inside the DomainModels$Entity constructor, whose attributes are PlainObject<{name,type,enumerationName}>; with $Type present PED ignores `type` and still answers SUCCESS and a clean check. Neither attribute constructor has a length property.", "fix": "buildEntityValue clears $Type on nested attributes (omitempty); applyAttributeLengths sets /entities/N/attributes/M/type/length after create and after add for every String not at the schema default 200.", "insight": "The same element has two constructor shapes depending on where it is added (nested PlainObject vs standalone constructor) \u2014 read ped_get_schema for the container, not the element. A clean ped_check_errors proves nothing about a dropped property; only a read-back of each type caught it.", "file": "mdl/backend/mcp/domainmodel.go", "issue": "ako/mxcli#923"} -{"date": "2026-10-02", "area": "mdl/backend", "symptom": "TestLive_EntityAccessRuleReject fails on every live Studio Pro MCP server (entityIndex: not found) \u2014 its fixture module ExpenseApproval exists in no test project.", "cause": "The live test read a hand-made fixture (module/entity/role) from one developer's project via env defaults instead of building it.", "fix": "The test creates its own entity Zz_R12_AccessFixture_ in MXCLI_MCP_MODULE, grants MXCLI_MCP_ROLE (default .User) once, then asserts the second grant is rejected with the rule count unchanged; skips with the role to set when the role does not exist (module roles cannot be created over MCP).", "insight": "A live test that depends on state it does not create fails as an environment problem on every other machine, so its real failures read as noise; build the fixture, and skip only on what MCP cannot author.", "file": "mdl/backend/mcp/security_test.go", "issue": "ako/mxcli#924"} -{"area": "mdl/backend", "date": "2026-10-02", "symptom": "A view entity's OQL document that Studio Pro excluded is re-included by any CREATE OR MODIFY of the view entity that changes its query.", "cause": "encodeViewEntitySourceDocument writes Excluded=false as a constant, and WriteViewEntitySourceDocument's update path carried only the stored ExportLevel (#816), not Excluded.", "file": "`mdl/backend/modelsdk/move_view_write.go` (WriteViewEntitySourceDocument), `export_level_carry.go` (keepStoredExcluded)", "insight": "A shared create/update encoder that writes model state as a constant needs a carry on the update path for every such key, not just the one a previous bug named (#816 ExportLevel, #914 Excluded).", "refs": ["ako/mxcli#827", "ako/mxcli#914"]} -{"area": "mdl/backend", "date": "2026-10-02", "issue": "ako/mxcli#803", "symptom": "`create association A.X from A.E to B.F on delete restrict` (TO entity in another module) stores ChildDeleteBehavior DeleteMeIfNoReferences with a null ChildErrorMessage — the shape that stops the runtime starting (CapTrackV2 §1). The same statement within one module, and ALTER on the cross-module one, were fine.", "cause": "crossAssocToGen built the delete behaviour without the restrict message; #795 added patchCrossDeleteErrorMessage only at one call site (patchCrossAssociations' new-element arm), not in the converter, so CreateCrossAssociation still wrote null.", "file": "`mdl/backend/modelsdk/association_move_write.go` (crossAssocToGen)", "fix": "Call patchCrossDeleteErrorMessage inside crossAssocToGen, as assocToGen does inline; drop the now-redundant call site.", "insight": "A property fix belongs in the converter, not at the call site that was reported — enumerate the converter's callers. Test: mdl/backend/modelsdk/issue803_cross_assoc_restrict_test.go, keep behaviour as the null control."} -{"area": "mdl/backend", "date": "2026-10-02", "symptom": "getput: describe → exec of a combo box with `Editable: ` rewrites ConditionalEditabilitySettings/Conditions[0]: 2 -> 3", "cause": "Forms$ConditionalEditabilitySettings registered Conditions as a plain mandatory list (default marker 3); Studio Pro writes [2], as for visibility", "file": "`mdl/backend/modelsdk/widget_write.go` (RegisterTypeDefaults)", "insight": "measured on both editability settings in TestApp; the ALTER path (pagemutator setWidgetConditionalSettingMut) still hand-writes [3] for both settings", "refs": ["#721"]} -{"area": "mdl/backend", "date": "2026-10-04", "symptom": "ako/mxcli#980 (found adding navigation profiles to the TestApp round-trip harness): describe -> exec of an offline profile rewrote every multi-line sync constraint as the one-line text describe prints", "cause": "navOfflineConfigs wrote the spec's constraint verbatim; describe folds Studio Pro's multi-line layout (singleLine) and the visitor re-normalises it, so the text never matches the stored bytes", "file": "`mdl/backend/modelsdk/navigation_write.go` navOfflineConfigs / xpathWithoutLayout", "insight": "When describe deliberately changes layout, the writer must carry the stored layout for an unchanged value; compare with whitespace outside string literals removed (inside a literal it is part of the value — that is the control)", "refs": ["#980"]} -{"area": "mdl/backend", "date": "2026-10-04", "symptom": "ako/mxcli#980: create or modify navigation on a native profile ignored its menu block, sync block, login/not-found page and on-sync-error while reporting 'updated'; describe printed a native nanoflow home as `home microflow` and bottom bar items without action or icon", "cause": "navPatchNativeProfile only patched home pages; nativeNavProfileFromGen read BottomBarItem.Page only; the grammar had no nanoflow home", "file": "`mdl/backend/modelsdk/navigation_write.go` navPatchNativeProfile, `navigation_read.go` nativeNavProfileFromGen, `mdl/executor/cmd_navigation.go` checkProfileClauses", "insight": "A profile-kind-specific writer must refuse every clause it does not apply (checked in the executor before anything is written, including profile creation), and describe must not print what exec would refuse — list it as comments. No native profile exists in any local fixture, so the reader is pinned with a hand-built gen document", "refs": ["#980"]} -{"area": "mdl/backend", "date": "2026-10-05", "symptom": "On Mendix 11.15 `TestMappingFixtureRoundTrip` fails for the blank app's own FeedbackModule mappings: describe -> exec adds `\"MessageDefinition\": \"\"`; and a mapping `with message definition` built on 11.15 fails `mx check` with CE0270 \"No root element could be found in the schema\"", "cause": "Mendix 11.15 REMOVED the mapping's `MessageDefinition` key (message definitions became `MessageDefinitions$MessageDefinition2` documents; the source moved to `MessageDefinition2` as `Module.MessageName`, measured with `mx convert` 11.15.0). The writer added the key unconditionally and the executor put the source in it, which 11.15 no longer reads", "file": "`model.ImportMapping`/`ExportMapping` (`MessageDefinition *string`, `MessageDefinitionSource()`), `mdl/backend/modelsdk/mapping_read.go` (`optionalStringFromRaw`) + `mapping_write.go`, `mdl/executor/mapping_messagedefinition.go` (`mappingMessageDefinitionKeys`, `messageDefinitionsAreDocuments`), `sdk/versions/mendix-1{0,1}.yaml` (`integration.message_definition_collection` max 11.14.99, `message_definition_document` min 11.15.0)", "insight": "**A key can be REMOVED by a version, not only introduced** \u2014 the #279 carry rule applies in both directions, so `MessageDefinition` is now a pointer like `MessageDefinition2`. The source key follows the stored document's shape on an update (a pre-11.15 document transplanted into an 11.15 project keeps its source in `MessageDefinition`, which is what kept the fixture's AgentCore/Email_Connector mappings green) and the project version only on a create. Caught by the new 11.15 nightly job, not by any fixture: none of the pinned documents was 11.15-shaped until the blank 11.15 app's own FeedbackModule mappings appeared in the base project", "refs": ["ako/mxcli#987", "ako/mxcli#279"]} -{"area": "mdl/backend", "date": "2026-10-06", "symptom": "`describe microflow` prints `-- Unsupported action: Microflows$SendEmailAction` for the Studio Pro 11.13+ Send Email activity, and MDL has no statement to create it", "cause": "No reader case, and no grammar/AST/builder/writer. The vendored gen type `genMf.SendEmailAction`/`EmailMessage` predates 11.12: it binds Subject/MessageBody* as expressions, but 11.13 deleted those and stores SubjectTemplate/MessageBodyPlainTextTemplate/MessageBodyHtmlTemplate (Microflows$StringTemplate) plus a CustomHeaders list the gen type has no field for", "file": "mdl/backend/modelsdk/microflow_send_email.go", "insight": "The gen type existing (the issue cited it as 'the storage side seems known') was a false lead: it is the pre-11.12 shape, so both directions are raw-keyed against a Studio Pro-saved document (ako/TestApp Email.EmailMF) and a key-for-key shape test (TestSendEmailActionToGen_MatchesStudioProShape) pins types and markers — it caught ConnectionTimeout being int64 and the empty CustomHeaders list needing marker 3. Version facts come from mendixmodelsdk 4.116 StructureVersionInfo (templates 11.12.0, plain Subject deleted 11.13.0), hence the 11.13.0 gate. Run negative controls through mxbuild before writing check rules: of the plausible rules, 'check server identity needs SSL' and the header-name rule are ACCEPTED by mxbuild 11.15.0-rc.4 (so warnings), while type faults are CE9528 for address/host/port/user (not the CE0117 exprcheck's E009 used to name — slots can now carry their own Mxbuild code). Attachment is a bare variable name (CE0109 on a wrong one). Shapes MDL cannot restate (auth document, pre-11.13 expression subject, non-SMTP) stay UnsupportedAction. Studio Pro's Test Email tab (TestEmailMessage) is written empty on rewrite, by design. An unedited describe->exec round trip is elided as Unchanged, so it cannot demonstrate loss; author-from-script is the meaningful control (old binary: `mismatched input 'email' expecting REST`).", "refs": ["mendixlabs/mxcli#1315"], "ce": ["CE9528", "CE0117", "CE0720", "CE0109"], "rules": ["MDL-EMAIL01", "MDL-EMAIL02", "MDL-EMAIL03", "E009"]} -{"area": "mdl/backend", "date": "2026-10-07", "symptom": "After `RENAME JAVA ACTION M.JA_Old TO JA_New` (or `mxcli rename java-action`), javasource/m/actions/JA_New.java still declares `public class JA_Old`, its constructor `public JA_Old(` and toString `return \"JA_Old\";` \u2014 javac: `class JA_Old is public, should be declared in a file named JA_Old.java`. `mxcli docker build` reports BUILD SUCCEEDED regardless", "cause": "Backend.RenameJavaSourceFile (mdl/backend/modelsdk/java_write.go) only os.Rename'd the file; nothing rewrote the generated parts that carry the action's name", "file": "mdl/backend/modelsdk/java_write.go, sdk/javaactions/rename.go", "insight": "A green mxbuild does NOT prove the javasource on disk compiles: mxbuild regenerates action stubs in the copy it builds (class, constructor, toString follow the model; user code, extra code and the import list are kept byte-for-byte, a mention of the old name inside them included), so the defect only shows where the on-disk file is compiled directly \u2014 `run --local --watch` hot reload, IDEs. The cheap probe is javac on the one file with runtime/bundles/com.mendix.public-api.jar on the classpath. The oracle for the rewrite is mxbuild itself: run the mxbuild binary on the project IN PLACE (not `docker build`, which uses a temp copy) and diff the javasource file \u2014 it changed exactly three lines; that pair is now testdata/mxbuild/JA_Renamed*. Rewrite those three spots, not a full GenerateSource, which would reformat a Studio Pro-authored file. Control: unfixed binary \u2192 javac error above; fixed \u2192 javac exit 0, docker build BUILD SUCCEEDED", "refs": ["follow-up to mendixlabs/mxcli#1318"]} -{"area": "mdl/backend", "date": "2026-10-06", "symptom": "Opening a change in Studio Pro's Changes panel throws `System.InvalidOperationException: Objects with ID … of type Forms$MicroflowParameterMapping do not have the same properties. baseNames = Expression, Parameter, Variable; newNames = Parameter, Expression` after `alter page … set ('onClickAction': call microflow M.F(P = $P)) on w`. `mx check` is 0 errors; the page opens. Second, silent half: `$P` (a page parameter) is stored as the Expression \"$P\", which Studio Pro does not bind (CE1571, the #1140 form)", "cause": "(1) `bindParameterMappingValue` sets only Expression for an Expression-bound argument; the gen `Variable` Part stays unset and the encoder omits an unset Part on a new element, so the key vanished. Studio Pro writes all three keys, nulling the unused slot, and its merge library compares key sets per $ID — the canon $ID carry makes the new mapping the SAME object as the stored one, so the diff sees two shapes of one element. (2) `convertASTAction` (ALTER PAGE SET Action and named action slots) built its pageBuilder with no paramScope/localVariables, so `classifyFlowArgValue` could not recognise a page/snippet parameter", "file": "`mdl/backend/modelsdk/widget_write.go` (RegisterTypeDefaults NullFields Variable on Forms$MicroflowParameterMapping + Forms$NanoflowParameterMapping), `mdl/executor/cmd_alter_page.go` (convertASTAction seeds ParamScope, storedPageVariables, isSnippet from the mutator)", "insight": "**`mx diff base.mpr new.mpr out.mpr` reproduces Changes-panel crashes headless** — it runs the same MergeLib DifferenceComputer and prints the identical \"do not have the same properties\" line (exit 129). Base = a copy of the project before the write, made by the FIXED build so it has Studio Pro's key set. That turns a Windows-GUI-only symptom into a two-binary A/B on Linux. **The base must share the $ID** — the crash needs a paired object, which the identity carry supplies; a fresh element is only an add and never trips it, so a create-only repro passes on broken code. **Register the null slot, don't set it at the call site**: `Part.Set(nil)` still encodes as omitted, and there are three construction sites (action, nanoflow action, nanoflow data source) — NullFields covers all and any future one. **A builder constructed outside CREATE PAGE is the recurring hole for #1140-style classification**: INSERT/REPLACE already seeded the scope from the mutator, SET did not; grep for `&pageBuilder{` and check each seeds paramScope + localVariables + isSnippet. Measured on mxbuild 11.12.2: buggy mx diff exit 129 with the reported line, fixed exit 0; mx check 0 errors on both", "refs": ["mendixlabs/mxcli#1317", "mendixlabs/mxcli#1180", "mendixlabs/mxcli#1140"], "ce": ["CE1571"]} -{"area": "mdl/backend", "symptom": "alter page … { insert after tpOne { tabpage tpTwo … } } and insert into tabsMain { tabpage … } both fail: `failed to insert: failed to build widgets: failed to build widget tpTwo: tabpage must be a direct child of tabcontainer`; the only route to add a tab was rewriting the page with create or modify", "cause": "applyInsertWidgetMutator built each inserted node with buildWidgetV3, which refuses a lone tabpage (it is only built as a tabcontainer child); and even past the builder, InsertWidget refuses tab-page siblings by design (refuseTabPageSiblingEdit) because a Forms$TabPage is not a widget and lives in the control's TabPages list", "file": "mdl/executor/cmd_alter_page.go (applyInsertWidgetMutator, buildTabPagesFromAST); mdl/backend/pagemutator/tabpages.go (InsertTabPages)", "insight": "A non-widget child (list view template, DataGrid2 column, tab page) needs its own PageMutator method, not a relaxed check in InsertWidget: route by the node type in the executor before the generic builder runs, and let the mutator decide by the TARGET's $Type whether INTO (the container) or BEFORE/AFTER (a sibling of the same kind) is meant. Serialize by wrapping the new pages in a throwaway container and taking its child list back out — no new Deps method, and the shape is exactly CREATE PAGE's. Control that makes `mx check` meaningful: a Forms$TabPage placed in a tab page's Widgets makes mx unable to load the project (\"TabPage does not contain a constructor with a parameter of type TabPage\"), while the TabPages splice checks 0 errors on 11.14.0. Keep the control's DefaultPagePointer on insert; set it only when the control had no pages.", "refs": ["mendixlabs/mxcli#1215"], "date": "2026-10-07"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-05-16-pluggable-widget-selection-bson-value-lowercase-single-multi.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-05-16-pluggable-widget-selection-bson-value-lowercase-single-multi.json new file mode 100644 index 0000000000..2972ec65cc --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-05-16-pluggable-widget-selection-bson-value-lowercase-single-multi.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-05-16", "symptom": "Pluggable widget `Selection` BSON value is lowercase (`single` / `multi` / `none`) but Studio Pro stores PascalCase — contributes to CE0463 drift on stricter widgets and looks wrong in diffs", "cause": "MDL passes the user's typed value verbatim to `SetSelection`; the builder didn't normalise", "file": "`mdl/backend/mpr/widget_builder.go` `SetSelection`", "insight": "Canonicalise via `canonicalSelectionValue` helper (lowercase-keyed switch → `Single` / `Multi` / `None`); unknown values pass through", "ce": ["CE0463"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-19-alter-entity-add-attribute-any-alter-succeeds-describe.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-19-alter-entity-add-attribute-any-alter-succeeds-describe.json new file mode 100644 index 0000000000..9b262f29bf --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-19-alter-entity-add-attribute-any-alter-succeeds-describe.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-19", "symptom": "`ALTER ENTITY ... ADD ATTRIBUTE` (or any ALTER) succeeds and `DESCRIBE ENTITY` is byte-identical before/after, but `mx check` then reports CE1613 \"attribute … no longer exists\" — often on a *different* entity's inherited system member (`createdDate`) referenced by grids/sort-bars in another module", "cause": "The ALTER target is rebuilt from the semantic `domainmodel.Entity` (`entityToGen` → fresh `NewEntity`, `raw==nil`), so the codec's `EmitGUID` default writes `GUID = $ID`, discarding the entity's real on-disk GUID. The GUID is the stable cross-reference identity inheriting entities/pages resolve members through, so changing it dangles those refs. The forgiving reader re-derives DESCRIBE from flags and never shows GUID, so mxcli can't see its own damage. (Legacy `sdk/mpr` had the broader form: it re-serializes the *whole unit* and forces `GUID = $ID` on every sibling too)", "file": "`mdl/backend/modelsdk/domainmodel_alter.go` → `UpdateEntity`", "insight": "Transplant the original element's raw bytes onto the rebuilt target (`ge.SetRaw(orig.Raw())`) so the codec treats it as an EXISTING element: dirty properties re-encode from `ge`, unmodeled fields (GUID, Capabilities, …) pass through verbatim. Siblings already survive via the list-rebuild raw passthrough. When a write \"succeeds\" but `DESCRIBE` is suspiciously byte-identical, suspect an identity (`$ID`/`GUID`) the reader doesn't surface — assert on raw BSON, not DESCRIBE. Issue #657", "refs": ["#657"], "ce": ["CE1613"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-19-page-level-property-can-t-set-alter-page-x-set-popupwidth.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-19-page-level-property-can-t-set-alter-page-x-set-popupwidth.json new file mode 100644 index 0000000000..96f2069bb8 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-19-page-level-property-can-t-set-alter-page-x-set-popupwidth.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-19", "symptom": "A page-level property can't be set — `ALTER PAGE X { SET PopupWidth = 800; }` (or any page-level prop other than Title/Url) fails with `unsupported page-level property: …`", "cause": "The page-level SET handler only special-cased a couple of properties; everything else fell through to the default error", "file": "`mdl/backend/pagemutator/mutator.go` → `applyPageLevelSetMut` (shared by both engines)", "insight": "Add a `case` writing the field at the top level of the Forms$Page doc via `dSetOrAppend` with the on-disk BSON type (int64 for PopupWidth/PopupHeight, bool for PopupResizable — verify against a Studio Pro page with `mxcli bson dump --format bson`). Page-level prop names are **case-sensitive**. For DESCRIBE roundtrip, emit the values back in the CREATE PAGE header. CREATE-time support: add a generic `IDENTIFIER COLON propertyValueV3` to `pageHeaderPropertyV3` (regen grammar), recognize the keys in `parsePageHeaderV3` (`applyGenericPageHeaderProp`, error on unknown), carry `*int`/`*bool` on `CreatePageStmtV3`, default to 600/600/false in `buildPageV3`, and have both writers honour `page.Popup*` (legacy `sdk/mpr/writer_pages.go` int64; codec `mdl/backend/modelsdk/page_write.go` int32 via gen — tolerated by mx check). The MCP backend has its **own** `mcpPageMutator` (pg content tree, not raw BSON) — page-level SET there reaches `SetWidgetProperty(\"\")`; map it or reject honestly (it rejects, pending a `pg_read_page` probe of the pop-up keys). Issue #661", "refs": ["#661"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-19-under-default-modelsdk-engine-show-describe-errors-not.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-19-under-default-modelsdk-engine-show-describe-errors-not.json new file mode 100644 index 0000000000..e0f1676a77 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-19-under-default-modelsdk-engine-show-describe-errors-not.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-19", "raw": "| Under the default modelsdk engine a SHOW/DESCRIBE errors `… not implemented yet — rerun with MXCLI_ENGINE=legacy`, OR the read \"works\" but DESCRIBE drops nested config (return type, parameters, exposed-as info) into a half-shell that's fine under `--engine legacy` | The modelsdk `Backend` never overrode the read method (falls through to the generated `unimplemented` embed), OR a gen-accessor converter read children under the codegen property names while real Studio-Pro BSON stores them under the original storage names — the gen codec decodes those keys into empty lists/parts | `mdl/backend/modelsdk/*_read.go` (add the override) + `mdl/backend/modelsdk/unimplemented_gen.go` (confirm the stub) | First confirm the gap: `comm -23 <(grep -hoP 'errUnimplemented\\(\"\\K[^\"]+' mdl/backend/modelsdk/unimplemented_gen.go \\| sort -u) <(grep -rhoP 'func \\([a-z]* \\*Backend\\) \\K[A-Za-z0-9_]+' mdl/backend/modelsdk/*.go \\| grep -v _test \\| sort -u)`. Then **dump the real raw keys before writing the converter** (a gen-accessor port silently half-shells): iterate `b.ListRawUnitsByType(typePrefix)` + `b.GetRawUnit(id)` and read the fidelity-sensitive children from the empirical keys, not the gen accessors. JS actions store `JavaReturnType`/`Parameters`/`TypeParameters`/`MicroflowActionInfo` (gen wants `ActionReturnType`/`ActionParameters`/`ActionTypeParameters`/`ModelerActionInfo`) — see `javascript_read.go`. Validate with describe-parity vs `--engine legacy` (modelsdk may be *more* correct, e.g. recovers `Enum X` where legacy drops to `Object`). Issue 7 |"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-29-ce0488-no-entity-configured-data-source-data-view-context.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-29-ce0488-no-entity-configured-data-source-data-view-context.json new file mode 100644 index 0000000000..612c632974 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-29-ce0488-no-entity-configured-data-source-data-view-context.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-29", "symptom": "CE0488 \"No entity configured for the data source of this data view\" on a context (page-parameter-bound) data view authored over **MCP** — DESCRIBE looks correct (`dataview dvX (DataSource: $Param)`), but the model has no entity on the source. DataGrids with database sources are fine", "cause": "`mapDataViewSource`'s `*pages.DataViewSource` case used a mutually-exclusive `switch`: when `ParameterName != \"\"` it wrote only `sourceVariable` and never `entityRef`. Studio Pro/pg write **both** entityRef AND sourceVariable for a context source (see `testdata/pg-page-contact-newedit.json`)", "file": "`mdl/backend/mcp/page_widgets.go` → `mapDataViewSource`", "insight": "Drop the exclusive switch: set `entityRef` (DomainModels$DirectEntityRef) whenever `EntityName != \"\"` AND `sourceVariable` (Pages$PageVariable) whenever `ParameterName != \"\"`. The executor already resolves the parameter's entity into `EntityName` (`cmd_pages_builder_v3.go` parameter case), so both are available. MCP-only — the MPR engines were never affected", "ce": ["CE0488"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-create-external-entities-from-module-service-imports.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-create-external-entities-from-module-service-imports.json new file mode 100644 index 0000000000..527ca938e6 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-create-external-entities-from-module-service-imports.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`CREATE EXTERNAL ENTITIES FROM Module.Service` imports external entities as plain **PERSISTED** entities and NPEs **without the \"from Service\" link** (regression on the default modelsdk engine; legacy `--engine legacy` was correct)", "cause": "The modelsdk write adapters dropped all external-entity serialization: `entityToGen` only handled `DomainModels$OqlViewEntitySource`, `attributeToGen` always emitted `StoredValue`, and `assocToGen` forced `Source` null. So `e.Source = \"Rest$ODataRemoteEntitySource\"` (+ RemoteServiceName/EntitySet/KeyParts) was ignored and the entity fell to the plain NoGeneralization path", "file": "`mdl/backend/modelsdk/domainmodel_write.go` (`entityToGen`, `attributeToGen`, `assocToGen`) — the executor (`applyExternalEntityFields` in `mdl/executor/cmd_contract.go`) already stamps the right `domainmodel` fields; only the write adapter was lossy. The legacy serializer `sdk/mpr/writer_domainmodel.go` is the parity reference", "insight": "Port the legacy logic into the codec adapters using the gen `Rest$OData*` types (all present in `modelsdk/gen/rest`): `externalEntitySourceToGen` builds `Rest$ODataRemoteEntitySource`/`…EntityTypeSource`/`…PrimitiveCollectionEntitySource` (+ `ODataKey`/`ODataKeyPart`, with fresh IDs since `assignEntityIDs` only stamps the top source); `attributeToGen` takes an `isExternal` flag and emits `Rest$ODataMappedValue` / `…MappedPrimitiveCollectionValue`; `externalAssociationSourceToGen` emits `Rest$ODataRemoteAssociationSource` / `…PrimitiveCollectionAssociationSource` (the `DomainModels$Association` NullFields:[\"Source\"] default still nulls Source for plain associations because SetSource only emits when set). Tests in `external_entity_write_test.go`. Issue #718", "refs": ["#718"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-drops-database-retrieve-s.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-drops-database-retrieve-s.json new file mode 100644 index 0000000000..ef378851ee --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-drops-database-retrieve-s.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine drops a database retrieve's `sort by …` clause", "cause": "`retrieveSourceFromGen` read Entity/XPath/Range but not the sort columns, which live in the `DatabaseRetrieveSource`'s `NewSortings` child", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`retrieveSourceFromGen`, `sortItemsFromRaw`)", "insight": "Generalize `sortItemsFromRaw` to accept the `NewSortings` wrapper key (it already handled the Sort list-op `Sortings` key); set `s.Sorting = sortItemsFromRaw(g.Raw())`"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-download-file-x.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-download-file-x.json new file mode 100644 index 0000000000..8ec7976f5a --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-download-file-x.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders `download file $X …;` as `-- Empty action`", "cause": "`actionFromGen` lacked a `DownloadFileAction` case", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`)", "insight": "Add the case reading `FileDocumentVariableName()`/`ShowFileInBrowser()`, defaulting an empty `ErrorHandlingType` to Rollback. Note: the storage key is `ShowFileInBrowser` (legacy's `parseDownloadFileAction` reads the wrong `ShowInBrowser` key — a latent legacy bug; the gen reads it correctly)"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-export-mapping.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-export-mapping.json new file mode 100644 index 0000000000..66a43a8943 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-export-mapping.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders `export to mapping …` / `import from mapping …` as `-- Empty action`; and single-object mapping results render `as list of Entity` instead of `as Entity`", "cause": "`actionFromGen` lacked `ExportXmlAction`/`ImportXmlAction` cases; `restResultHandlingFromRaw` set `SingleObject` only from `Range.SingleObject`, ignoring an object var type (REST) and `ForceSingleOccurrence` (XML import)", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`, `restResultHandlingFromRaw`, `readMappingCall`)", "insight": "Add both XML-action cases reading mapping/arg/output from raw `ResultHandling`/`OutputMethod`/`ImportMappingCall`; extract `readMappingCall`; apply the object-type rule (REST) and force fallback (XML import) — mirrors legacy `parseExportXmlAction`/`parseImportXmlAction`/`parseResultHandling`"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-inheritance-split.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-inheritance-split.json new file mode 100644 index 0000000000..fe1f62a345 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-inheritance-split.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders an inheritance split header as `split type $` (empty variable) and drops its `@caption`", "cause": "`flowObjectFromGen` created a bare `InheritanceSplit` without reading `SplitVariableName`/`Caption`/`Documentation` off the gen element", "file": "`mdl/backend/modelsdk/microflow.go` (`flowObjectFromGen`)", "insight": "Populate `VariableName`/`Caption`/`Documentation` from the gen `InheritanceSplit` accessors"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-legacy-soap-call.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-legacy-soap-call.json new file mode 100644 index 0000000000..341f5dbc75 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-legacy-soap-call.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders a legacy SOAP `call web service …` as `-- Empty action`", "cause": "`actionFromGen` lacked a `WebServiceCallAction` case", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`, `webServiceActionRequiresRawBSON`)", "insight": "Add the case: read the structured fields (ImportedService / OperationName / NewResultHandling / RequestHandling) and, when the action carries any field the structured form can't represent, set `RawBSON = a.Raw()` so the renderer emits `call web service raw ''`. Mirror legacy's supported-key set exactly; `canonicalRawBSON` makes both engines' base64 byte-identical"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-loop-break.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-loop-break.json new file mode 100644 index 0000000000..72c2dbfe10 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-loop-break.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders a loop `break;`/`continue;` as `-- Empty action`", "cause": "`flowObjectFromGen` had no cases for `Microflows$BreakEvent` / `Microflows$ContinueEvent`, so they fell through to the ActionActivity default with a nil action", "file": "`mdl/backend/modelsdk/microflow.go` (`flowObjectFromGen`)", "insight": "Add both object cases (renderer already maps them to `break;`/`continue;`)"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-rule-based-split.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-rule-based-split.json new file mode 100644 index 0000000000..4916f4849e --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-modelsdk-engine-renders-rule-based-split.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine renders a rule-based split as `if true then …` instead of the real `Module.Rule_X(args)` condition (body renders, only the condition text is wrong)", "cause": "`splitConditionFromGen` only handled `*ExpressionSplitCondition`; a `RuleSplitCondition` returned nil → renderer fell back to `true`. Even when handled, the rule name lives under the `RuleCall.Microflow` storage key, but gen decodes the property as `Rule` (and `ByNameRef` decode bypasses `fieldAliases`), so `RuleQualifiedName()` is empty", "file": "`mdl/backend/modelsdk/microflow.go` (`splitConditionFromGen`)", "insight": "Reconstruct `*microflows.RuleSplitCondition` from the gen `RuleCall`; read the rule name from `rc.Raw().Lookup(\"Microflow\")` when `RuleQualifiedName()` is empty (same raw-storage-key pattern the action readers use). The existing `formatSplitCondition` renders it"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-nanoflow-modelsdk-engine-omits-grant.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-nanoflow-modelsdk-engine-omits-grant.json new file mode 100644 index 0000000000..e799268188 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-06-30-describe-microflow-nanoflow-modelsdk-engine-omits-grant.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-06-30", "symptom": "`DESCRIBE microflow`/`nanoflow` on the **modelsdk** engine omits the `grant execute on microflow … to ;` line that legacy emits", "cause": "`microflowFromGen`/`nanoflowFromGen` never read the allowed module roles, so `Microflow.AllowedModuleRoles` was empty and the describer skipped the grant block", "file": "`mdl/backend/modelsdk/microflow.go` (`microflowFromGen`/`nanoflowFromGen`)", "insight": "Populate `out.AllowedModuleRoles` from the gen `AllowedModuleRolesQualifiedNames()` (BY_NAME role refs, stored as qualified-name strings)"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-createpage-listview-source-pages-databasesource-not-yet.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-createpage-listview-source-pages-databasesource-not-yet.json new file mode 100644 index 0000000000..1a64708b74 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-createpage-listview-source-pages-databasesource-not-yet.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-01", "symptom": "`CreatePage: ListView source *pages.DatabaseSource not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy` — a native `listview` with a `database`/`database from … where …` datasource fails on the default engine", "cause": "`listViewSourceToGen` only handled `*pages.MicroflowSource`; database (and context/listen/association) sources fell through to the \"not supported\" error. DataView, DataGrid2, and Gallery database sources already worked — only native ListView was missing", "file": "`mdl/backend/modelsdk/widget_write.go` (`listViewSourceToGen`); mirror the `customWidgetDataSourceToGen` database branch and the legacy `serializeListViewDataSource`", "insight": "Add a `*pages.DatabaseSource` case building `genPg.NewListViewXPathSource()` with EntityRef (`DirectEntityRef`), XPathConstraint, a `NewGridSortBar()` (GridSortItem per `d.Sorting`, note gen uses **SortBar** not the legacy `Sort`/`ListViewSort`), and a `NewListViewSearch()` — the codec already has all types. Verify with `mx check` = 0 errors. Tests in `listview_source_write_test.go`"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-describe-microflow-modelsdk-engine-emits-rest-body-mapping-x.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-describe-microflow-modelsdk-engine-emits-rest-body-mapping-x.json new file mode 100644 index 0000000000..a846ebd913 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-describe-microflow-modelsdk-engine-emits-rest-body-mapping-x.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-01", "symptom": "`DESCRIBE microflow` on the **modelsdk** engine emits a REST `body mapping X` line with no `from $var`, which fails to re-parse (`mismatched input … expecting FROM`) — breaks the DESCRIBE roundtrip", "cause": "`restRequestHandlingFromRaw` read the export-mapping source variable from the wrong key `ParameterVariable`; the real storage key is `MappingVariableName` (same key ExportXmlAction uses), so it came back empty and the renderer dropped the grammar-mandatory `from $var`", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`restRequestHandlingFromRaw`)", "insight": "Read `ParameterVariable` from `MappingVariableName` (fallback to `ParameterVariable`). Note this makes modelsdk *more* faithful than legacy, whose `parseRequestHandling` drops the REST request body mapping entirely (\"would be parsed here if needed\")"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-mx-check-reports-ce6621-attribute-x-has-max-length-200-which.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-mx-check-reports-ce6621-attribute-x-has-max-length-200-which.json new file mode 100644 index 0000000000..c775ddb36d --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-mx-check-reports-ce6621-attribute-x-has-max-length-200-which.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-01", "symptom": "`mx check` reports CE6621 \"Attribute 'X' has max length '200' which is not the same as max length in the OData service which is 'unlimited'\" on every unlimited-length string attribute of a `create external entities from …` import — **only under the default modelsdk engine**; `--engine legacy` is clean", "cause": "The codec's encoder only emits *dirty* properties for a new element. `attributeTypeToGen` skipped `SetLength` when `Length == 0` (unlimited), so the `Length` property stayed non-dirty and the field was omitted from the BSON entirely. Studio Pro then applies its own UI default of 200, which contradicts the OData service's \"unlimited\" type. Legacy's `writer_domainmodel.go` always writes `Length: t.Length` (including 0), so it never regressed", "file": "`mdl/backend/modelsdk/domainmodel_write.go` → `attributeTypeToGen` (`*domainmodel.StringAttributeType` case)", "insight": "Always call `g.SetLength(int32(at.Length))` — drop the `if at.Length > 0` guard — so `Length: 0` is emitted explicitly, matching the legacy serializer. General rule for the codec write path: any field whose *zero value is meaningful* (0 = unlimited here) must be Set unconditionally, because an omitted field falls back to Studio Pro's own default, not to zero. Assert on the serialized `NewType` map (not DESCRIBE) — the reader treats a missing Length as unlimited so it hides the damage. Test `TestEntityToGen_ODataRemoteEntitySource` in `external_entity_write_test.go`. Issue #718", "refs": ["#718"], "ce": ["CE6621"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-show-access-page-page-section-show-security-matrix-reports.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-show-access-page-page-section-show-security-matrix-reports.json new file mode 100644 index 0000000000..fd6a3c3905 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-01-show-access-page-page-section-show-security-matrix-reports.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-01", "symptom": "`SHOW ACCESS ON PAGE` / Page section of `SHOW SECURITY MATRIX` reports \"no roles\" for a restricted page on the **modelsdk** engine (legacy is correct) — under-reports page access, a security-audit hazard", "cause": "`pageFromGen` never read the page's allowed module roles into `Page.AllowedRoles`, so it defaulted to empty. (The gen `Page` decode is correct — it has the storage-name override `allowedRoles`↔`AllowedModuleRoles`.) The microflow/nanoflow equivalent was fixed separately", "file": "`mdl/backend/modelsdk/page.go` (`pageFromGen`)", "insight": "Populate `out.AllowedRoles` from the gen `AllowedRolesQualifiedNames()` (mirrors `microflowFromGen`). Fixes both `SHOW ACCESS ON PAGE` and the matrix Page section. Issue #722", "refs": ["#722"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-02-consumed-odata-service-configuration-microflow-has-no-effect.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-02-consumed-odata-service-configuration-microflow-has-no-effect.json new file mode 100644 index 0000000000..102830e900 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-02-consumed-odata-service-configuration-microflow-has-no-effect.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-02", "symptom": "Consumed OData service **Configuration microflow** has no effect in Studio Pro ≥ 11.10 — the \"Configuration source\" dropdown stays on \"Constants only\" even though `describe odata client` shows the microflow", "cause": "Mendix **renamed the BSON storage field** across versions (from the `Rest$ConsumedODataService` reflection metadata): `ConfigurationMicroflow` was introduced 10.12.0 and **DELETED 11.10.0**; `ConfigurationEntityMicroflow` was introduced 11.10.0. mxcli wrote the pre-11.10 key on all versions, so ≥ 11.10 Studio Pro ignored the unknown field. Both engines were affected (identical BSON). The codebase had flip-flopped this key (573→`ConfigurationEntityMicroflow`, then reverted) — the reflection `VersionInfos` Introduced/Deleted is the authority", "file": "`model.ODataConfigMicroflowBSONKey` (new, version-gated), wired into `mdl/backend/modelsdk/odata_write.go` (`consumedODataServiceToGen` via `Backend.configMicroflowKey()`) **and** `sdk/mpr/writer_odata.go` (`serializeConsumedODataService` via `w.reader.ProjectVersion()`)", "insight": "Gate the write key on project major/minor (≥ 11.10 → `ConfigurationEntityMicroflow`). On read, coalesce **all** historical keys (`model.ODataConfigMicroflowBSONKeys()` in the legacy parser; `firstNonEmpty(ConfigurationEntity…, Configuration…, Headers…)` of the gen getters in `integration_read.go`) so a service authored by any version round-trips. When a storage name looks version-dependent, check `modelsdk/gen/*/version.go` `VersionInfos` before guessing. Issue #728", "refs": ["#728"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-02-database-retrieve-s-sort-completely-missing-from-describe.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-02-database-retrieve-s-sort-completely-missing-from-describe.json new file mode 100644 index 0000000000..459cdaaa00 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-02-database-retrieve-s-sort-completely-missing-from-describe.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-02", "symptom": "A database retrieve's `sort by …` is **completely missing** from DESCRIBE after any modelsdk **write** (regression on switching to the modelsdk engine; \"no more sort\") — reproduces on a fresh `create microflow … retrieve … sort by …` round-trip", "cause": "The WRITE counterpart of the row above: `retrieveSourceToGen` set an **empty** `genMf.NewSortItemList()` and never serialized `s.Sorting`, so nothing was stored for the read to render. The generated `SortItemList` type also stores items under `\"Items\"`, but Mendix stores retrieve sort columns under `\"Sortings\"` (like the Sort list-op), so the gen convenience type is the wrong tool", "file": "`mdl/backend/modelsdk/microflow_write.go` (`retrieveSourceToGen`)", "insight": "Build the envelope manually, mirroring the `SortOperation` branch: `newElem(\"Microflows$SortingsList\",\"\")` + `addPartList(sl,\"Sortings\", sortItemToGen(each))` + `g.SetSortItemList(sl)`. Guard with a toGen→encode→decode→fromGen round-trip test asserting the sort items survive. Note \"completely missing\" ⇒ write dropped it (nothing stored); a *blank-attribute* sort line ⇒ a read/name-resolution gap instead. Issue #727", "refs": ["#727"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-03-grant-view-page-module-page-module-role-prints-success-but.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-03-grant-view-page-module-page-module-role-prints-success-but.json new file mode 100644 index 0000000000..bede3e534c --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-03-grant-view-page-module-page-module-role-prints-success-but.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-03", "raw": "| `GRANT VIEW ON PAGE Module.Page TO Module.Role` prints success but the page's allowed roles appear to stay empty: `mxcli lint` still reports `CE0557`/`MPR007` (\"used as home page … but has no allowed roles\") across a freshly rebuilt catalog, while entity-access grants on the same roles verify fine (`describe entity` shows them). Reported on **modelsdk** engine (default), Mendix 11.11 | NOT a write bug — the grant **does** persist (`AllowedModuleRoles: [3, \"Module.Role\"]` on disk; `mx check` = 0 errors; Studio Pro would load clean). The regression was on the **read** path: modelsdk `pageFromGen` didn't populate `Page.AllowedRoles`, and `mxcli lint`'s CE0557 rule counts `len(pg.AllowedRoles)` from `ListPages()` — so it read every page as having zero roles and false-fired. `SHOW ACCESS ON PAGE` / `SHOW SECURITY MATRIX` under-reported the same way. The entity-vs-page asymmetry the reporter saw = different read paths (entity `describe` worked; page `lint` didn't), not different writes | `mdl/backend/modelsdk/page.go` → `pageFromGen` | Already fixed by `88a2d50b1` (issue #722): populate `out.AllowedRoles` from the gen `AllowedRolesQualifiedNames()`, mirroring `microflowFromGen`. Guard for the reporter's exact end-to-end flow: `TestUpdateAllowedRoles_PagePersistsForLintRead` (`mdl/backend/modelsdk/page_roles_test.go`) writes a page role then reopens fresh and asserts the lint read path (`ListPages`→`AllowedRoles`) sees it. **Diagnosis tip**: when a write \"reports success but the value stays empty\" *only in a read/lint/show command*, dump the raw unit (`mxcli bson dump`) — if the bytes are there, it's a read-adapter (`*FromGen`) gap, not a write bug; confirm by reverting the suspected read commit and re-running | ", "refs": ["#722"], "ce": ["CE0557"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-06-move-entity-move-enumeration-cross-module-prints-warning.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-06-move-entity-move-enumeration-cross-module-prints-warning.json new file mode 100644 index 0000000000..04ccec8bde --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-06-move-entity-move-enumeration-cross-module-prints-warning.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-06", "symptom": "`MOVE ENTITY` / `MOVE ENUMERATION` cross-module prints `Warning: Could not update OQL queries` / `Could not update enumeration references: load domain model 00000000-0000-0000-0000-000000000002: open …/mprcontents/00/00/00000000-…002.mxunit: no such file or directory` — **modelsdk engine only**. The move itself succeeds and (today) user modules are still updated, because `ListDomainModels` appends the failing System DM **last** — but the warning is spurious and the ordering dependence is fragile", "cause": "Both reference-update sweeps iterate `ListDomainModels()`, which **injects the virtual System-module domain model** (ID `…002`, `buildSystemDomainModel`) so platform entities resolve. Each sweep then re-loads every listed DM from disk via `loadDomainModelGen(info.ID)` — but the System module isn't stored in `mprcontents`, so its `.mxunit` read fails. Legacy never hit this: it walks in-memory `*domainmodel.DomainModel` objects and never re-reads from disk", "file": "`mdl/backend/modelsdk/association_move_write.go` → `UpdateEnumerationRefsInAllDomainModels`; `mdl/backend/modelsdk/move_view_write.go` → `UpdateOqlQueriesForMovedEntity`", "insight": "Skip the System DM at the top of each DM loop: `if string(info.ID) == meta.SystemDomainModelID { continue }` (import `modelsdk/meta`). It never holds user enum refs or user view-entity OQL, so skipping is correct and removes the disk-read failure regardless of iteration order. **Diagnosis tip**: a \"no such file / not found\" on the fixed ID `…002` (or module `…001`) means code is trying to load the *virtual* System unit from disk — filter it out, don't try to read it. Regression: `TestUpdateRefsForMovedEntity_SkipsSystemDomainModel` (`move_refs_system_test.go`)"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-06-move-entity-x-othermodule-where-view-entity-s-oql-joins.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-06-move-entity-x-othermodule-where-view-entity-s-oql-joins.json new file mode 100644 index 0000000000..437cb74049 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-06-move-entity-x-othermodule-where-view-entity-s-oql-joins.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-06", "symptom": "`MOVE ENTITY X to OtherModule` where a **view entity's OQL** joins through `X`: passes `mxcli check`, but `mx check` on **Mendix 10.x** reports `CE0174 \"…path '…/DmTest.Customer' does not resolve to an entity\"` at the view entity — **only on the modelsdk engine**, only on 10.x (11.x is clean; legacy is clean)", "cause": "Mendix <11 stores a view entity's OQL in **two** places: the top-level `DomainModels$ViewEntitySourceDocument` unit AND inline on the entity's `DomainModels$OqlViewEntitySource` (see `entityToGen`, gated `major < 11`). modelsdk's `UpdateOqlQueriesForMovedEntity` rewrote only the source document, leaving the inline copy pointing at the old module. 11.x doesn't write the inline copy (OQL lives only on the source doc), so it was never affected — hence 10.x-only. Legacy updates the inline copy incidentally by re-serializing the domain model", "file": "`mdl/backend/modelsdk/move_view_write.go` → `UpdateOqlQueriesForMovedEntity`", "insight": "After the ViewEntitySourceDocument sweep, also iterate every domain model (`ListDomainModels` → `loadDomainModelGen`), find each entity whose `Source()` is `*genDm.OqlViewEntitySource` with an `Oql()` containing the old qualified name, `SetOql(strings.ReplaceAll(...))`, and `persistDM`. Mirrors the `UpdateEnumerationRefsInAllDomainModels` walk. **Diagnosis tip**: when a value updates in one storage location but a stale copy elsewhere still breaks `mx check`, dump raw `mprcontents` and grep for the value across `$Type`s — Mendix often duplicates a field (OQL, captions) and a version gate (`major < 11`) can make one copy version-conditional. Repro: `mdl-examples/bug-tests/move-entity-oql-view-inline-ref.mdl`", "ce": ["CE0174"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-mcp-op-reports-mcp-error-32000-request-timed-out-but-page.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-mcp-op-reports-mcp-error-32000-request-timed-out-but-page.json new file mode 100644 index 0000000000..9ee895ab26 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-mcp-op-reports-mcp-error-32000-request-timed-out-but-page.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-07", "symptom": "MCP op reports `MCP error -32000: Request timed out` but the page/document/entity EXISTS in Studio Pro afterwards", "cause": "Studio Pro's ~30s server-side per-call limit fires while the op still applies — a client false failure, not a server rejection", "file": "`mdl/backend/mcp/timeout.go` (`isTimeoutErr`, `timeoutVerifyDelay`, `pedUpdateVerify`, `pedDocumentExists`)", "insight": "Verify-on-timeout, never blind-retry a non-idempotent op: idempotent root-replace (`pgWritePage`) retries once; creates confirm via `ped_find_document`; entity adds confirm via a shallow `/entities` read. Unverified → error with save-before-re-run guidance (a blind re-run from a fresh session duplicates elements)"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-mx-check-ce0117-error-expression-creating-rule-based.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-mx-check-ce0117-error-expression-creating-rule-based.json new file mode 100644 index 0000000000..77fe3b1876 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-mx-check-ce0117-error-expression-creating-rule-based.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-07", "raw": "| `mx check` **CE0117** \"Error in expression\" when creating a rule-based decision (`if Module.SomeRule(...)`) via MDL on the **modelsdk** engine; the decision's subtype is demoted Rule→Expression on every round-trip | modelsdk `*Backend` never implemented `IsRule` → fell back to the embedded `unimplemented` stub (which errors); the builder's `isRule, err := backend.IsRule(...); if err != nil || !isRule` guard treated the error as \"not a rule\" and emitted an invalid `ExpressionSplitCondition` | `mdl/backend/modelsdk/microflow.go` (`IsRule`) | Implement `IsRule` on `*Backend` (list `Microflows$Rule` units, match qualified name via `moduleNameFor`), mirroring the legacy reader; it shadows the generated stub. With the existing builder mock-test this guards the full chain modelsdk.IsRule→RuleSplitCondition. Issue #723 A4 |", "refs": ["#723"], "ce": ["CE0117"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-mx-check-ce4899-concurrent-execution-error-message-microflow.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-mx-check-ce4899-concurrent-execution-error-message-microflow.json new file mode 100644 index 0000000000..14095caec0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-mx-check-ce4899-concurrent-execution-error-message-microflow.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-07", "symptom": "`mx check` **CE4899** \"concurrent execution: error message or microflow required\" after a microflow round-trip (`GetMicroflow` → `UpdateMicroflow`), even though the microflow allowed concurrent execution", "cause": "`microflowFromGen` never read `AllowConcurrentExecution`/`MarkAsUsed` back (the write path set them), so the round-trip reset them to `false` → \"disallow concurrent execution\" with no error message configured", "file": "`mdl/backend/modelsdk/microflow.go` (`microflowFromGen`)", "insight": "Read both flags back from the gen getters (`mf.AllowConcurrentExecution()`, `mf.MarkAsUsed()`) — keep the FromGen converter lossless. Guarded by a `toGen→encode→decode→fromGen` Go test, not MDL (create-or-modify rebuilds the microflow from MDL, so it can't reproduce a read-back bug). Issue #723 A1", "refs": ["#723"], "ce": ["CE4899"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-studio-pro-renders-every-activity-decision-1-px-sliver.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-studio-pro-renders-every-activity-decision-1-px-sliver.json new file mode 100644 index 0000000000..fcedd981cb --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-07-studio-pro-renders-every-activity-decision-1-px-sliver.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-07", "symptom": "Studio Pro renders **every activity/decision as a 1-px sliver** (caption wraps one letter per line) after a modelsdk round-trip; `mx check` reports **NO** error (it ignores box size, so the corruption is silent)", "cause": "`flowObjectFromGen` carried each object's `Position` but not its `Size`, so the round-trip rewrote every node with size `0;0`", "file": "`mdl/backend/modelsdk/microflow.go` (`sizeFromGen`, `splitFlowObjects`)", "insight": "Add `sizeFromGen` (mirrors `pointFromGen`) and apply it at the `splitFlowObjects` call site — covers nested loop bodies via recursion; add `GetSize`/`SetSize` to `BaseMicroflowObject`. Go round-trip test (not MDL, same reason as A1). Issue #723 A2", "refs": ["#723"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-09-dataview-bound-one-referenced-object-over-association-data.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-09-dataview-bound-one-referenced-object-over-association-data.json new file mode 100644 index 0000000000..cee9f7aba7 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-09-dataview-bound-one-referenced-object-over-association-data.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-09", "symptom": "A **DataView bound to a to-one referenced object over an association** (\"data from context\", e.g. an inner DataView showing the referenced Employee's `Name`/`Email` inside an `Expense` DataView) can't be authored/round-tripped: `dataview dvEmp (datasource: $currentObject/Module.Assoc)` was rejected (MDL-WIDGET08), a bare nested `dataview { textbox (attribute: Name) }` bound children to the **parent** entity (CE1613), and `describe` dropped the source → describe/exec **silently strips** the association DataView (round-trip corruption)", "cause": "A DataView over an association is **not** a `Forms$AssociationSource` (that's list-widgets-only; CE6705 on a DataView). It is a `Forms$DataViewSource` whose `EntityRef` is a `DomainModels$IndirectEntityRef` navigating the association (a DataViewSource *is* an `EntityPathSource`, so its EntityRef may be indirect — `generated/metamodel/types.go:558`). mxcli generated the wrong source type, then (Bug 5 v1) wrongly rejected the whole case", "file": "`mdl/backend/modelsdk/widget_write.go` (`dataViewContextAssociationSourceToGen`, wired into `dataViewSourceToGen`'s `*pages.AssociationSource` case) + `mdl/executor/cmd_pages_builder_v3_widgets.go` (removed the `buildDataViewV3` association guard) + `mdl/executor/validate_widgets.go` (removed MDL-WIDGET08) + `mdl/executor/cmd_pages_describe_parse.go` (`Forms$DataViewSource` reads `EntityRef.Steps` via `associationSourcePath`)", "insight": "**modelsdk engine only.** For a DataView association datasource, emit `Forms$DataViewSource` with `EntityRef = IndirectEntityRef{EntityRefStep{Association, DestinationEntity}}` (+ optional page-param `SourceVariable`; `$currentObject` → none) — mirrors `associationSourceToGen` but wraps the IndirectEntityRef in a DataViewSource. Children then bind to the destination entity. DESCRIBE reconstructs `$currentObject/Module.Assoc`. Removes the earlier MDL-WIDGET08 over-rejection. **When two widget kinds share a navigation but MxBuild accepts it on only one, the distinction is the source `$Type` wrapper, not the EntityRef inside it** — check `generated/metamodel/types.go` for the widget's allowed `DataSource` subtypes. mxbuild-validated (`mxcli docker check --no-update-widgets` = 0 errors) + full describe→re-exec round-trip. Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go` (`TestDataViewAssociationSource_Serialized`); bug-test `mdl-examples/bug-tests/dataview-context-association-source.mdl`. Bug 5 (reclassified)", "ce": ["CE1613", "CE6705"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-09-under-default-modelsdk-engine-any-page-widget-association.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-09-under-default-modelsdk-engine-any-page-widget-association.json new file mode 100644 index 0000000000..a20319e196 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-09-under-default-modelsdk-engine-any-page-widget-association.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-09", "symptom": "Under the **default (modelsdk)** engine, any page widget with an **association datasource** (`datasource: $currentObject/Module.Assoc`, the master-detail idiom) fails at exec: `CreatePage: DataView source *pages.AssociationSource not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy`. Same for a nested `listview`", "cause": "`customWidgetDataSourceToGen` (pluggable widgets) already handled `*pages.AssociationSource`, but `dataViewSourceToGen`/`listViewSourceToGen` had no case → hit the \"not yet supported\" default. (Legacy doesn't error but writes a **broken** `Forms$AssociationSource{EntityRef: nil}` in `serializeListViewDataSource`, so legacy is not a valid reference — tracked as **L1** in [`docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md`](../../docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md); do **not** fix legacy, it will be removed when modelsdk matures)", "file": "`mdl/backend/modelsdk/widget_write.go` (`associationSourceToGen`, wired into `dataViewSourceToGen`+`listViewSourceToGen`) + `mdl/executor/cmd_pages_describe.go` (`associationSourcePath`) + `cmd_pages_describe_parse.go` (DataView/ListView datasource cases) + `cmd_pages_describe_output.go` (`associationDataSourceExpr`)", "insight": "**modelsdk engine only.** Extract the proven `AssociationSource` block from `customWidgetDataSourceToGen` into a shared `associationSourceToGen` (Forms$AssociationSource → `IndirectEntityRef` of `EntityRefStep{Association, DestinationEntity}` + optional page-param `SourceVariable`; `$currentObject` → nil source var) and add a `case *pages.AssociationSource` to the DataView and ListView source builders. Matches the canonical legacy `serializeAssociationSource` (writer_widgets.go) and the Studio-Pro-verified `EntityRefStep` structure — NOT the broken `serializeListViewDataSource` stub. DESCRIBE reconstructs `$currentObject/Module.Assoc` from `EntityRef.Steps`. A `Forms$AssociationSource` is valid on **LIST-producing** widgets (listview/datagrid/gallery/templategrid). A plain **DataView** cannot use `Forms$AssociationSource` (MxBuild **CE6705**) — but the DataView *association* case is still valid via a **different** source type; see the \"data from context over association\" row below (an earlier fix wrongly rejected it with MDL-WIDGET08, now removed). Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go`, `mdl/executor/cmd_pages_describe_assoc_source_test.go`; bug-test `mdl-examples/bug-tests/assoc-datasource-modelsdk.mdl` (valid listview)", "ce": ["CE6705"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-10-describe-view-entity-default-modelsdk-engine-omits-oql.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-10-describe-view-entity-default-modelsdk-engine-omits-oql.json new file mode 100644 index 0000000000..41a2f40837 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-10-describe-view-entity-default-modelsdk-engine-omits-oql.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-10", "symptom": "`DESCRIBE` of a view entity on the **default (modelsdk)** engine omits the `as (… OQL …)` clause entirely (legacy renders it) — the describe→exec round-trip silently drops the query, leaving a queryless view. Reproduces on any Mendix **11.x** project", "cause": "On Mendix 11.0+ the OQL is stored **only** in the separate `DomainModels$ViewEntitySourceDocument`; the inline `OqlViewEntitySource.Oql` field was removed (see the writer's `if !pv.IsAtLeast(11,0)` gate in `serializeOqlViewEntitySource`). The modelsdk read (`entityFromGen`: `out.OqlQuery = src.Oql()`) only read the now-empty inline field and never followed the `SourceDocumentRef`. Legacy was correct — `loadViewEntityOqlQueries` in `sdk/mpr/reader_documents.go` already joins the source docs", "file": "`mdl/backend/modelsdk/domainmodel.go` (`populateViewEntityOql`, wired into `ListDomainModels` + `GetDomainModel`)", "insight": "After building the domain models, list `ViewEntitySourceDocument` units (`mprread.ListUnitsWithContainer[*genDm.ViewEntitySourceDocument]`), map `moduleName.docName → Oql()`, and fill `OqlQuery` for any view entity with an empty `OqlQuery` + a `SourceDocumentRef` (mirrors the legacy join; guarded by a cheap pre-check so non-view projects pay nothing). When a view-entity read differs between engines on 11.x, suspect a field the platform moved out-of-line into a source document. Test: `TestGetDomainModel_ViewEntityOqlFromSourceDocument` (round-trips via both read paths)"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-10-describe-workflow-default-modelsdk-engine-renders-some.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-10-describe-workflow-default-modelsdk-engine-renders-some.json new file mode 100644 index 0000000000..50ad9f6bdf --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-10-describe-workflow-default-modelsdk-engine-renders-some.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-10", "symptom": "`DESCRIBE WORKFLOW` on the **default (modelsdk)** engine renders some activities as non-round-trippable comments — `-- [Workflows$StartWorkflowActivity]`, `-- [Workflows$JumpToActivity]`, `-- [Workflows$WaitForTimerActivity]`, `-- [Workflows$WaitForNotificationActivity]` — so `describe → exec` drops those activities and you can't learn their syntax from an existing workflow. User tasks, decisions, splits, call-microflow/workflow round-trip fine (legacy round-trips all of them)", "cause": "The describe **formatter** (`formatWorkflowActivities`) already has typed cases for jump/wait/start/end, but the modelsdk **reader** never produced them: these activities have no `genWf` struct (written via generic `newElem`), so `workflowActivityFromGen`'s type switch fell through to `GenericWorkflowActivity`, which the formatter renders as `-- [$Type]`. Legacy's `parser_workflow.go` had dedicated cases, so legacy was correct — modelsdk-only gap", "file": "`mdl/backend/modelsdk/workflow_read.go` (`workflowActivityFromGen` default → new `workflowSimpleActivityFromGen`)", "insight": "Recognise the untyped simple activities by `el.TypeName()` and rebuild the semantic struct, reading fields from raw BSON via `genWf.RawFieldString` (`TargetActivity` for jump, `Delay` for the timer) — mirrors the legacy parser. Start/end become typed so the formatter **skips** them (implicit in MDL); jump/wait become executable statements. When an engine emits `-- [$Type]` in describe, the *formatter* usually already handles the type — the gap is the *reader* decoding it to `GenericWorkflowActivity`. Test: `TestWorkflowSimpleActivities_ReconstructedTyped`; round-trip validated describe→drop→exec→`docker check` = 0 workflow errors. Bug 11b (11a = the missing `write-workflows.md` authoring skill)"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-11-built-widget-action-actionbutton-container-linkbutton-action.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-11-built-widget-action-actionbutton-container-linkbutton-action.json new file mode 100644 index 0000000000..23a7e67250 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-11-built-widget-action-actionbutton-container-linkbutton-action.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-11", "symptom": "A built-in widget action (`actionbutton`/`container`/`linkbutton` `Action: microflow M.Flow(Param: $x)`) **silently drops the argument** on the **default (modelsdk)** engine — `describe` shows `Action: microflow M.Flow` (no args), the flow runs with no parameter, and a required-param flow never fires so the button/row no-ops at runtime. `mxcli check`/`docker check`/`docker build` all pass (0 errors) — silent break. Legacy persists it (the workaround)", "cause": "`clientActionToGen`'s `MicroflowClientAction` case built the `Forms$MicroflowSettings` from the microflow **name only** (`microflowSettingsToGen(x.MicroflowName)`), never serializing `x.ParameterMappings` — the executor builds them correctly into the model, the writer threw them away. (NOT the same as `custom-widgets.md`'s \"action mapping too complex for auto-mapping\" — that's about *pluggable-widget action-typed properties* in def.json generation, a different path.)", "file": "`mdl/backend/modelsdk/widget_write.go` (`microflowSettingsToGen`)", "insight": "Give `microflowSettingsToGen` the mappings param; emit one `genPg.MicroflowParameterMapping` each — `SetParameterQualifiedName(\".\")` (BY_NAME) + `SetExpression(variable-or-expression)` — mirroring the legacy `writer_widgets_action.go` BSON. The client-action caller passes `x.ParameterMappings`; the microflow-datasource callers pass `nil`. Verified: default-engine describe now keeps `(Param: $x)`; a type-correct page = 0 mxbuild errors (both engines give the *same* CE0117 on a type-mismatched arg — that's a model error, not the writer). Tests: `TestClientActionToGen_MicroflowParameterMappings`; bug-test `mdl-examples/bug-tests/bug1-widget-action-param-mapping.mdl`. Nanoflow widget actions were a separate gap (the whole `NanoflowClientAction` case was missing) — now fixed in Bug 2 below. Bug 1", "ce": ["CE0117"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-11-create-page-widget-action-nanoflow-fails-default-modelsdk.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-11-create-page-widget-action-nanoflow-fails-default-modelsdk.json new file mode 100644 index 0000000000..b6cfe7dcbc --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-11-create-page-widget-action-nanoflow-fails-default-modelsdk.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-11", "symptom": "`CREATE PAGE` with a widget `Action: nanoflow …` fails on the **default (modelsdk)** engine: `client action *pages.NanoflowClientAction not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy` (legacy works). Blocks any nanoflow-triggered button/link written via MDL", "cause": "`clientActionToGen` had cases for save/cancel/close/delete/page/microflow/create-object client actions but **no `*pages.NanoflowClientAction` case** → hit the `default` refuse-loudly branch. (The sibling microflow-arg drop was the separate Bug 1, already fixed on `microflowSettingsToGen`.)", "file": "`mdl/backend/modelsdk/widget_write.go` → `clientActionToGen`", "insight": "Add a `*pages.NanoflowClientAction` case building a gen `CallNanoflowClientAction`. Unlike the microflow action (which nests a `Forms$MicroflowSettings`) it carries the Nanoflow name + `ParameterMappings` **directly**: `SetNanoflowQualifiedName`, `SetProgressBar(\"None\")`, `SetDisabledDuringExecution(true)`, and one `NanoflowParameterMapping` per arg (`SetParameterQualifiedName(nanoflow+\".\"+param)` BY_NAME + `SetExpression($var)`); register `Forms$CallNanoflowClientAction` TypeDefaults (`MandatoryLists:[ParameterMappings]`, `NullFields:[ProgressMessage,ConfirmationInfo]`). Mirror the legacy `writer_widgets_action.go`. The modelsdk **read** already round-trips it (describe shows `nanoflow M.NF(Form: $Form)`). Verify both engines with `mx check`=0. Bug-test `bug2-modelsdk-nanoflow-widget-action.mdl`; tests `TestNanoflowClientAction_Serialized` (+ `TestMicroflowClientAction_PersistsParameters` guards Bug 1's mapping stays wired through the shared helper). Bug 2"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-11-under-default-modelsdk-engine-any-page-widget-association.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-11-under-default-modelsdk-engine-any-page-widget-association.json new file mode 100644 index 0000000000..0050696acb --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-11-under-default-modelsdk-engine-any-page-widget-association.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-11", "symptom": "Under the **default (modelsdk)** engine, any page widget with an **association datasource** (`datasource: $currentObject/Module.Assoc`, the master-detail idiom) fails at exec: `CreatePage: DataView source *pages.AssociationSource not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy`. Same for a nested `listview`", "cause": "`customWidgetDataSourceToGen` (pluggable widgets) already handled `*pages.AssociationSource`, but `dataViewSourceToGen`/`listViewSourceToGen` had no case → hit the \"not yet supported\" default. (Legacy doesn't error but writes a **broken** `Forms$AssociationSource{EntityRef: nil}` in `serializeListViewDataSource`, so legacy is not a valid reference — tracked as **L1** in [`docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md`](../../docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md); do **not** fix legacy, it will be removed when modelsdk matures)", "file": "`mdl/backend/modelsdk/widget_write.go` (`associationSourceToGen`, wired into `dataViewSourceToGen`+`listViewSourceToGen`) + `mdl/executor/cmd_pages_describe.go` (`associationSourcePath`) + `cmd_pages_describe_parse.go` (DataView/ListView datasource cases) + `cmd_pages_describe_output.go` (`associationDataSourceExpr`)", "insight": "**modelsdk engine only.** Extract the proven `AssociationSource` block from `customWidgetDataSourceToGen` into a shared `associationSourceToGen` (Forms$AssociationSource → `IndirectEntityRef` of `EntityRefStep{Association, DestinationEntity}` + optional page-param `SourceVariable`; `$currentObject` → nil source var) and add a `case *pages.AssociationSource` to the DataView and ListView source builders. Matches the canonical legacy `serializeAssociationSource` (writer_widgets.go) and the Studio-Pro-verified `EntityRefStep` structure — NOT the broken `serializeListViewDataSource` stub. DESCRIBE reconstructs `$currentObject/Module.Assoc` from `EntityRef.Steps`. **An association source is valid only on LIST-producing widgets (listview/datagrid/gallery/templategrid), NOT a plain DataView** — Studio Pro rejects a DataView association source (\"cannot have a data source of type association\"); the widget still exec-creates (valid BSON), so `check` flags it with **MDL-WIDGET08** (`validateStaticWidget`). Tests: `mdl/backend/modelsdk/widget_assoc_source_test.go`, `mdl/executor/cmd_pages_describe_assoc_source_test.go`, `validate_widgets_test.go`; bug-tests `mdl-examples/bug-tests/assoc-datasource-modelsdk.mdl` (valid listview) + `dataview-association-source-rejected.fail.mdl` (MDL-WIDGET08)"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-22-page-native-listview-over-database-from-xpath-datasource.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-22-page-native-listview-over-database-from-xpath-datasource.json new file mode 100644 index 0000000000..47cafedd1f --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-22-page-native-listview-over-database-from-xpath-datasource.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-22", "symptom": "A page with a native **LISTVIEW** over a `database from` (XPath) datasource passes `mx check` but **crashes the browser client at runtime** and redirects to login: `TypeError: Cannot read properties of undefined (reading 'length')` at `processResult` → `retrieveByXPath`. Pluggable Gallery/DataGrid2 database sources are fine", "cause": "The serialized `Forms$ListViewXPathSource` omitted the arrays the client reads `.length` of. **Codec (default):** `Forms$ListViewSearch` emitted without its `SearchRefs` list — the encoder drops an empty, never-`Set` PartList unless the `$Type` is in `RegisterTypeDefaults` (GridSortBar.SortItems was registered; ListViewSearch was not). **Legacy:** wrote a bogus `Forms$ListViewSort` + a `Paths` key (renamed `SearchRefs` in 7.11.0) and no `Forms$GridSortBar`. NOT a `SortItems` marker issue — an empty `[2]` compiles fine when search is off; the crash is the absent `SearchRefs`. Diagnose by building a Deploy target and reading the compiled `deployment/web/pages/.js` (the client model) + `mxcli bson dump --type page` on the source", "file": "`mdl/backend/modelsdk/widget_write.go` (`init` RegisterTypeDefaults) + `sdk/mpr/writer_widgets_display.go` (`serializeListViewDataSource`, `emptyListViewXPathSource`)", "insight": "Codec: `RegisterTypeDefaults(\"Forms$ListViewSearch\", {MandatoryLists: []string{\"SearchRefs\"}})` (emits empty marker-3 list). Legacy: emit `Forms$GridSortBar`/`SortItems` (mirror `SerializeCustomWidgetDataSource`) + `Forms$ListViewSearch`/`SearchRefs` + `ForceFullObjects`; drop the bogus `Sort`/`Paths`. Tests: `TestListViewSourceToGen_SearchRefsEmitted` (codec, encode-level), `TestSerializeListViewDataSource_Database` + `TestEmptyListViewXPathSource_Shape` (legacy). Repro `mdl-examples/bug-tests/listview-database-source-searchrefs.mdl`"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-27-alter-page-p-set-class-x-builtin-lowercase-property-hard.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-27-alter-page-p-set-class-x-builtin-lowercase-property-hard.json new file mode 100644 index 0000000000..f1a05b07f6 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-27-alter-page-p-set-class-x-builtin-lowercase-property-hard.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-27", "symptom": "`alter page P { set class = 'x' on }` (lowercase property) hard-errors on EVERY built-in widget: `property \"class\" not found (widget has no pluggable Object)` — but `create page` accepts the same lowercase prop, and these aren't pluggable widgets", "cause": "The MPR page mutator dispatched first-class props via a case-SENSITIVE switch on canonical casing (\"Class\"/\"Caption\"/\"DynamicClasses\"); a lowercase name missed every case and fell through to the pluggable-Object path. `create page` was fine (WidgetV3.GetStringProp is case-insensitive); the sibling MCP mutator already lowercased", "file": "`mdl/backend/pagemutator/mutator.go` (`setRawWidgetPropertyMut`)", "insight": "`switch strings.ToLower(propName)` with lowercase case labels; keep the pluggable fallback (default) on the original casing (template keys are case-sensitive). Test `TestSetWidgetProperty_LowercaseFirstClassProps`; repro `mdl-examples/bug-tests/alter-page-lowercase-set-on-builtin.mdl`. Findings #1", "refs": ["#1"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-27-call-javascript-action-e-g-nanoflowcommons-openurl-passes.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-27-call-javascript-action-e-g-nanoflowcommons-openurl-passes.json new file mode 100644 index 0000000000..9313298ceb --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-27-call-javascript-action-e-g-nanoflowcommons-openurl-passes.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-27", "symptom": "`call javascript action` (e.g. NanoflowCommons.OpenURL) passes `check --references` + `exec` but persists as `-- Empty action`; `mx check` fails CE0008 \"No action defined.\" + CE0109 on the output variable. Affects microflows AND nanoflows", "cause": "The default `modelsdk` engine's `microflowActionToGen` had no case for `*microflows.JavaScriptActionCallAction` → returned nil → ActionActivity written with no Action. Read path + legacy engine both handled it (hence check/exec looked green); nanoflows reuse the same converter", "file": "`mdl/backend/modelsdk/microflow_write.go` (`microflowActionToGen`)", "insight": "Add the `JavaScriptActionCallAction` case, built directly to mirror the legacy serializer (storage keys JavaScriptAction/OutputVariableName; JS parameter mappings use the `ParameterValue` key, not the Java-style `Value`). Test `TestMicroflowActionToGen_JavaScriptActionCall`; repro `mdl-examples/bug-tests/javascript-action-call-persist.mdl`. Findings #11", "refs": ["#11"], "ce": ["CE0008", "CE0109"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-29-runtime-failed-load-model-class-workflows-callmicroflowtask.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-29-runtime-failed-load-model-class-workflows-callmicroflowtask.json new file mode 100644 index 0000000000..9654a8a674 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-29-runtime-failed-load-model-class-workflows-callmicroflowtask.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-29", "symptom": "Runtime `Failed to load model: ... Class 'Workflows$CallMicroflowTask' could not be found` — the WHOLE app won't boot, yet `mxcli check` ✓ and `mx check` → 0 errors", "cause": "Mendix 11.9 (WOR-2802) split MicroflowBasedActivity into CallMicroflowActivity + AIAgentTaskActivity, renaming the workflow call-microflow on-disk `$Type` from the pre-11.9 `CallMicroflowTask` to `CallMicroflowActivity`. Writing the old name to an 11.9+ project is fatal at boot only. Evidence: 11.6.3 modeler = only Task; 11.10 modeler = both (Task marked \"Removed ... WOR-2802\" + a conversion routine); 11.10+ runtime jars = only Activity", "file": "`mdl/backend/modelsdk/workflow_write.go` (`applyCallMicroflowStorageName`, `useCallMicroflowActivityName`) + legacy `sdk/mpr/writer_workflow.go` (`renameCallMicroflowTypeBSON`)", "insight": "Version-gate the emitted `$Type` at 11.9 (same boundary as the `HasOwner`→`HasOwnerAttr` gate): build with the legacy name, rewrite the tree to `CallMicroflowActivity` for `pv.IsAtLeast(11,9)`; register codec TypeDefaults + list-marker under BOTH names; read path already folds both into one semantic type. Repro `mdl-examples/bug-tests/263-workflow-callmicroflow-storage-name.mdl`. FINDINGS #39", "refs": ["#39"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-alter-page-insert-replace-into-list-bound-from-association.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-alter-page-insert-replace-into-list-bound-from-association.json new file mode 100644 index 0000000000..8227784828 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-alter-page-insert-replace-into-list-bound-from-association.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-30", "symptom": "`ALTER PAGE INSERT`/`REPLACE` into a list bound `from association` **or** `datasource: microflow/nanoflow` produces a widget whose Attribute binds to the WRONG entity (the outer data view's) or nothing — `mxcli check` ✓ but `mx check` fails **CE1613** (\"attribute no longer exists\") or **CE0402** (\"No value specified\"); DESCRIBE masks it by printing only the short attribute name", "cause": "The ALTER mutator read the enclosing entity from `DataSource.EntityRef.Entity`, only set for a DIRECT ref (database). An `AssociationSource` stores its destination on the last `DomainModels$EntityRefStep` of an `IndirectEntityRef`; a `MicroflowSource`/`NanoflowSource` stores NO entity at all (its entity is the flow's RETURN type, in the flow document). Either way the list reported no entity, so the context stayed at the outer data view (or empty)", "file": "`mdl/backend/pagemutator/mutator.go` (`extractEntityFromDataSource`+`lastStepDestinationEntity` for association; `EnclosingDataSourceFlow`+`findNearestDataSourceDoc` for flows) + `mdl/executor/cmd_alter_page.go` (`resolveDataSourceFlowEntity`)", "insight": "Association: read the `IndirectEntityRef`'s last `EntityRefStep.DestinationEntity`. Microflow/nanoflow: the mutator returns the nearest-enclosing (or own, for INTO) flow QN — a nearer non-flow source shadows an outer flow — and the executor resolves its return entity via `getMicroflowReturnEntityName`/`getNanoflowReturnEntityName`. Guards `TestEnclosingEntity_AssociationSource`, `TestEnclosingDataSourceFlow`; repro `mdl-examples/bug-tests/55-alter-page-insert-assoc-binding.mdl`. FINDINGS #55", "refs": ["#55"], "ce": ["CE0402", "CE1613"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-describe-microflow-prints-empty-action-set-task-outcome-open.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-describe-microflow-prints-empty-action-set-task-outcome-open.json new file mode 100644 index 0000000000..e1c697cf28 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-describe-microflow-prints-empty-action-set-task-outcome-open.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-30", "symptom": "`describe microflow` prints `-- Empty action` for a `set task outcome` / `open user task` / `notify workflow` statement (default engine); a describe→drop→exec round-trip silently drops it. Legacy engine (`MXCLI_ENGINE=legacy`) describes it fine", "cause": "The modelsdk read path (`actionFromGen`) had no case for the workflow microflow actions, so they read back as nil → \"Empty action\". The write path + DESCRIBE formatter already handled them; only the modelsdk read case was missing", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`actionFromGen`)", "insight": "Add cases for `genMf.SetTaskOutcomeAction` / `OpenUserTaskAction` / `NotifyWorkflowAction`, mirroring the legacy parsers. Test `TestActionFromGen_WorkflowActions`; repro `mdl-examples/bug-tests/54-describe-set-task-outcome.mdl`. FINDINGS #54", "refs": ["#54"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-external-odata-entity-loses-remote-settings-any-read-modify.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-external-odata-entity-loses-remote-settings-any-read-modify.json new file mode 100644 index 0000000000..90eb8778e3 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-external-odata-entity-loses-remote-settings-any-read-modify.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-30", "symptom": "An **external (OData) entity** loses its remote settings on any read-modify-write — `describe external entity` says `is not an external entity (source: )`, and `alter entity … set allow_create_change_locally = true` reports success but the flag stays off. Works under `--engine legacy`", "cause": "`entityFromGen` recognised only `DomainModels$OqlViewEntitySource`, so the three `Rest$OData*` sources read back as no source at all: `Source` empty, every remote field zeroed. The write path was fine — it switches on `e.Source`, which the read never populated", "file": "`mdl/backend/modelsdk/domainmodel.go` (`entityFromGen`'s source switch, `odataKeyFromGen`)", "insight": "Mirror the legacy parser (`sdk/mpr/parser_domainmodel.go`) for all three flavours: RemoteEntitySource (capabilities + CreateChangeLocally + key), EntityTypeSource (type name + IsOpen + key), PrimitiveCollectionEntitySource (service only). `Updatable` has no gen accessor and the writer does not emit it — leaving it zero is symmetric. **Check the read side first when a write-path field \"does not stick\"**: a switch on a field the read never fills looks like a write bug. Repro `mdl-examples/bug-tests/782-external-entity-create-change-locally.mdl`. Issue #782", "refs": ["#782"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-pluggable-widget-s-datasource-child-widget-client-action.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-pluggable-widget-s-datasource-child-widget-client-action.json new file mode 100644 index 0000000000..ca3cb9e701 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-30-pluggable-widget-s-datasource-child-widget-client-action.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-30", "symptom": "A pluggable widget's datasource (or child widget, or client action) is **silently dropped at write time** — `exec` prints `Created page` but the widget lands with the piece missing, and only a `log` line mentions `not yet supported — rerun with MXCLI_ENGINE=legacy`", "cause": "The converter *does* return an error, but it travels through `widgetobj.ChildSerializer`, whose methods return BSON with no error channel (the `TODO(shared-types)` in `mdl/backend/widgetobj/builder.go`), so the caller logged it and returned nil", "file": "`mdl/backend/modelsdk/widget_pluggable_write.go` (`recordChildSerializeErr`, `takeChildSerializeErr`) + the drains in `page_write.go` / `snippet_write.go`", "insight": "Record the failure in a package-level accumulator and drain it at every page/snippet write entry point, so the statement fails instead of the write succeeding with data missing (ADR-0004: refuse, don't drop). The real fix is the deferred `ChildSerializer` interface change; until then, any **new** write entry point that builds pluggable widgets must drain too. Repro `mdl-examples/bug-tests/795b-flow-datasource-context-entity.mdl`"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-07-31-microflow-writes-mxcli-reports-success-but-studio-pro-cannot.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-31-microflow-writes-mxcli-reports-success-but-studio-pro-cannot.json new file mode 100644 index 0000000000..a71e6971a3 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-07-31-microflow-writes-mxcli-reports-success-but-studio-pro-cannot.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-07-31", "symptom": "A microflow writes and `mxcli` reports success, but Studio Pro cannot open the project: `System.Collections.Generic.KeyNotFoundException: The given key '' was not present in the dictionary`. Triggered by a `break`/`continue` inside an if/case within a loop", "cause": "`microflowObjectToGen`'s `default:` branch returns nil for an unhandled object type, so the event object was dropped at serialization while the SequenceFlow pointing at it was written — a DestinationPointer to a GUID that exists nowhere. Exactly the shape already fixed once for `Microflows$ErrorEvent`", "file": "`mdl/backend/modelsdk/microflow_write.go` (`microflowObjectToGen`) — compare against `sdk/mpr/writer_microflow.go`, which handles the full set", "insight": "Add the missing `case *microflows.XxxEvent:` returning the gen element with ID, position and size. **Diagnose without Studio Pro**: dump the microflow (`mxcli bson dump --type microflow --object M.F`), collect every `$ID`, and check every key ending in `Pointer` resolves — a dangling pointer is this bug. When a check-time rule exists only as a stopgap for a write-path bug (MDL051 was), remove it with the real fix, and check whether it covered every affected keyword (MDL051 covered `break`, not `continue`, which is how #791 reached users). Repro `mdl-examples/bug-tests/791-loop-continue-dangling-flow.mdl`. Issue #791, ledger #52", "refs": ["#52", "#791"], "rules": ["MDL051"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-05-alter-page-col-targeting-datagrid2-column-authored-mdl.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-05-alter-page-col-targeting-datagrid2-column-authored-mdl.json new file mode 100644 index 0000000000..3dcf622da3 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-05-alter-page-col-targeting-datagrid2-column-authored-mdl.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-05", "symptom": "`ALTER PAGE … ON ` targeting a DataGrid2 column: (a) the authored MDL column name (`column colFoo`) fails with a bare `widget \"colFoo\" not found`, and (b) a name shared by two columns (duplicate captions) **silently mutates the first** and reports `Altered page`, leaving the second unreachable", "cause": "DataGrid2 columns carry **no stored name** in the Mendix model (their WidgetObject has no `Name` property), so the authored name is dropped on write and mxcli addresses a column by a *derived* name (attribute leaf, else caption, else `col{N}`). The bare-name resolver `findInWidgetArray` returned on the **first** derived-name match, so duplicates collided silently; the miss path emitted a generic \"widget not found\" with no hint that columns use a derived name", "file": "`mdl/backend/pagemutator/mutator.go` — the column loop in `findInWidgetArray` (now counts matches → `bsonWidgetResult.matchCount`), `findBsonColumn` (returns `(result, error)`), and the mutation entry points `SetWidgetProperty`/`DropWidget`/`InsertWidget`/`ReplaceWidget` (reject `matchCount > 1`, use `widgetNotFoundError` which lists derived names)", "insight": "Persisting the authored name is **not possible** — columns have no name slot, and inventing one is the Studio-Pro-won't-open hazard (ADR-0005 guard-don't-drop). So implement the finding's two fallbacks: reject an ambiguous `ON ` with an actionable error instead of silently taking the first (a *data hazard*, not just a wart), and on a miss list the addressable derived names + explain the derived-name model (run DESCRIBE PAGE). **Generalisable**: a resolver that returns the first of N matches hides ambiguity — count matches and reject >1 at every *mutating* entry point, not just the read path. Repro `mdl-examples/bug-tests/ledger-78-datagrid-column-addressing.mdl`; A/B: pre-fix binary reports `Altered page` for the duplicate-caption `ON \"Amount\"`, fixed errors with \"ambiguous\". Ledger #78", "refs": ["#78"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-06-create-javascript-action-succeeds-mxcli-check-passes-build.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-06-create-javascript-action-succeeds-mxcli-check-passes-build.json new file mode 100644 index 0000000000..80f5760133 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-06-create-javascript-action-succeeds-mxcli-check-passes-build.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-06", "symptom": "A **`CREATE JAVASCRIPT ACTION`** succeeds, `mxcli check` passes and the build is clean, but calling the action in the running app throws **`JavaScript action was not implemented`** and the nanoflow aborts", "cause": "mxcli wrote the source to `javascriptsource//actions/` using the module's own casing. Mendix reads a **lowercased** directory — a blank Mendix 11 app ships `javascriptsource/nanoflowcommons/`, `/datawidgets/`, `/webactions/` for modules named `NanoflowCommons`, `DataWidgets`, `WebActions`. Finding no source at the path it reads, mxbuild generates a stub whose body is `throw new Error(\"JavaScript action was not implemented\")` and bundles that. Only reproduces on a **case-sensitive filesystem**, which is why it survived: on macOS and Windows the two spellings are the same directory", "file": "`mdl/backend/modelsdk/javascript_write.go` and `sdk/mpr/writer_javascriptactions.go` (`jsActionSourceDir`)", "insight": "`strings.ToLower(moduleName)` in both writers — the comment in each previously asserted the opposite (\"unlike javasource, which is lowercased\"), so the belief was documented, not tested. **Generalisable — the shape to look for**: when generated *source files* pair with model units, the model unit is not evidence the file is found; the filesystem path is a separate contract, and a case-only mismatch is invisible on the developer's own machine. Check against the directories a blank project already ships rather than against what the code says. Nothing short of running the app catches it: parse, check and build all pass. Test `mdl/backend/modelsdk/javascript_write_dir_test.go`; verified end-to-end (button click flips the theme instead of throwing)"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-07-alter-page-set-editable-expr-set-visible-writes-project.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-07-alter-page-set-editable-expr-set-visible-writes-project.json new file mode 100644 index 0000000000..0599ea021f --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-07-alter-page-set-editable-expr-set-visible-writes-project.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-07", "symptom": "`alter page … set Editable = [expr]` (or `set Visible`) writes a project Studio Pro refuses to open: `StorageLoadException: Conditional editability settings has an invalid value '' for property Attribute`. `mxcli check` ✓ and `mx check` ✓ — neither inspects the stored value. The identical settings written by `create page` load fine", "cause": "The ALTER path builds the `Forms$Conditional{Visibility,Editability}Settings` node by hand and wrote `Attribute: null`. `Attribute` is a **BY_NAME** `AttributeIdentifier`, so its unset value is the empty string, not null — exactly what the CREATE path already encodes via `codec.RegisterTypeDefaults(..., EmptyStringFields: []string{\"Attribute\"})`, whose comment records this same StorageLoadException from #627. Only the hand-built ALTER node missed it", "file": "`mdl/backend/pagemutator/mutator.go` (`setWidgetConditionalSettingMut`)", "insight": "Write `{Key: \"Attribute\", Value: \"\"}`, not `nil`. **General rule: when one path hand-builds BSON that another path builds through the codec, diff the two encodings rather than eyeballing the hand-built one** — `mxcli bson dump --type page --object M.P` on a CREATE-authored and an ALTER-authored widget makes the divergence a one-line diff (key sets and values were otherwise identical). `SourceVariable` stays `nil`: it is BY_ID, where null *is* the absent value, so \"null is wrong\" is per-field, not a blanket rule. Test `TestSetWidgetConditionalSetting_AttributeIsEmptyString`; repro `mdl-examples/bug-tests/851-alter-page-conditional-attribute.mdl`. Issue #851", "refs": ["#627", "#851"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-07-download-file-doc-accepted-mxcli-check-mxcli-exec-created.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-07-download-file-doc-accepted-mxcli-check-mxcli-exec-created.json new file mode 100644 index 0000000000..70832f4a2b --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-07-download-file-doc-accepted-mxcli-check-mxcli-exec-created.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-07", "symptom": "`download file $Doc;` is accepted by `mxcli check` and `mxcli exec` (\"Created microflow\") but the activity lands with **no action at all** — `describe` renders `-- Empty action` and `mx check` fails `[CE0008] \"No action defined.\"`. Same for `download file $Doc show in browser;`", "cause": "`microflowActionToGen` (the modelsdk write path) had no `*microflows.DownloadFileAction` case, so it hit `default: return nil` and the enclosing ActionActivity was serialized with a nil Action. Grammar, visitor, flow builder, read path and DESCRIBE formatter were all already in place, so the statement passed every stage that reports anything and vanished at the one that does not", "file": "`mdl/backend/modelsdk/microflow_write.go` (`microflowActionToGen`)", "insight": "Add the case, setting `FileDocumentVariableName`, `ShowFileInBrowser` and `ErrorHandlingType` (Rollback default). **The storage key is `ShowFileInBrowser`, not `ShowInBrowser`** — the gen setter binds the right one; legacy's `parseDownloadFileAction` reads the wrong key. **Test at the round trip, not the reader**: a reader-only test starts from BSON the writer never had to produce, so `TestActionFromGen_DownloadFile` was green throughout. `roundTripMicroflow` (model→gen→codec→model) is the harness; assert the ActionActivity's `Action` is non-nil, which is the CE0008 shape itself. This is the same silent-drop mechanism as the `microflowObjectToGen` default branch (#791) — when auditing, diff the write switch's cases against `sdk/mpr/writer_microflow_actions.go`. Test `TestMicroflowRoundTrip_DownloadFile`; repro `mdl-examples/bug-tests/850-download-file-action.mdl`. Issue #850", "refs": ["#791", "#850"], "ce": ["CE0008"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-alter-page-set-caption-actionbutton-fails-widget-has-no.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-alter-page-set-caption-actionbutton-fails-widget-has-no.json new file mode 100644 index 0000000000..cca9347838 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-alter-page-set-caption-actionbutton-fails-widget-has-no.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-08", "symptom": "`alter page … set Caption = '…' on ` fails with `widget has no Caption property` — for EVERY action button, nested or top-level", "cause": "An ActionButton has no `Caption` document: its caption is a `Forms$ClientTemplate` under **`CaptionTemplate`** (Template → Items[] → Translation.Text), the same shape `setWidgetContentMut` already walked for `Content`. `setWidgetCaptionMut` only looked for `Caption`", "file": "`mdl/backend/pagemutator/mutator.go` (`setWidgetCaptionMut`, `setClientTemplateText`)", "insight": "Fall back to `CaptionTemplate` via a shared `setClientTemplateText` helper, which `setWidgetContentMut` now uses too. **Found while fixing #834 — check whether a symptom reproduces OUTSIDE the reported context before attributing it**: this failed on a top-level button as well, so it was a second, independent defect and the #834 finder fix alone would not have made the reporter's command work. When a widget property will not set, dump the widget's key list (`mxcli bson dump --type page`) before assuming the setter is wired — the field is often stored under a different, template-shaped key", "refs": ["#834"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-alter-page-set-prop-widget-reports-widget-x-not-found-widget.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-alter-page-set-prop-widget-reports-widget-x-not-found-widget.json new file mode 100644 index 0000000000..b5ff20ebc2 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-alter-page-set-prop-widget-reports-widget-x-not-found-widget.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-08", "symptom": "`alter page … set on ` reports `widget \"X\" not found` when the widget sits inside a datagrid column rendered as `customContent`; only CREATE OR REPLACE PAGE can touch it", "cause": "`findInWidgetChildren`'s pluggable branch searched the grid's own `Object.Properties[].Value.Widgets` and matched columns by derived name, but never descended into a COLUMN's own content. Columns live at `Object.Properties[columns].Value.Objects[]`; their widgets are one level deeper at `Properties[content].Value.Widgets[]`", "file": "`mdl/backend/pagemutator/mutator.go` (`findInWidgetChildren`, the columns loop)", "insight": "Descend into each column's `Properties[].Value` and reuse `findInWidgetArray(…, \"Widgets\", name)`. **Address by the nested widget's OWN name, not a `grid.column.widget` path**: DataGrid2 columns carry no stored name in the MPR (see `findBsonColumn`), so a column segment could only be a derived name that changes when the caption is edited — a path that goes silently stale. Keep the existing column-by-derived-name lookup working (a test pins it, or the descent could shadow it). Test `TestFindBsonWidget_InsideCustomContentColumn`; repro `mdl-examples/bug-tests/834-alter-customcontent-column-widget.mdl`. Issue #834", "refs": ["#834"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-create-modify-external-entity-mod-e-countable-false-touching.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-create-modify-external-entity-mod-e-countable-false-touching.json new file mode 100644 index 0000000000..b62d9f12a0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-create-modify-external-entity-mod-e-countable-false-touching.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-08", "symptom": "`create or modify external entity Mod.E (… Countable: false)` — touching only an entity-level property — detonates every attribute: `[CE6612] \"Attribute 'circuitId' of external entity 'Stg_Circuit' is not supported.\"`, one per attribute, leaving a project that cannot build", "cause": "Not the executor: it already preserves attributes it was not asked to change (`if len(attrs) > 0`). One layer down, `attributeFromGen` handled `StoredValue` and `OqlViewValue` but **not** `Rest$ODataMappedValue`, so every attribute of an external entity read back with no `RemoteName`, and the writer's `isExternal && a.RemoteName != \"\"` arm then fell through to a plain StoredValue on the next read-modify-write", "file": "`mdl/backend/modelsdk/domainmodel.go` (`attributeFromGen` gains the `ODataMappedValue` / `ODataMappedPrimitiveCollectionValue` arms)", "insight": "**The attribute-level half of #782**, which fixed the entity level and stopped there — when a read-modify-write loses data, check every *nesting level* of the read, not just the one named in the report. A polymorphic `Value` switch that silently ignores a variant is the shape to look for: it compiles, it reads, and it drops. Reproduce with a **local metadata file** (`MetadataUrl: './contract.xml'`) — no server needed, and the import is the same code path. Also learned here: the reported attribute *rename* (`name` → `Stg_Circuitname`) is a different thing entirely — it happens at import, from `reservedEntityAttrNames`, and `name` is **not** actually reserved (verified: Mendix builds an external entity with an attribute literally named `name`). Tests `external_entity_read_test.go`. mxcli-formula1 #25", "refs": ["#25", "#782"], "ce": ["CE6612"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-microflow-using-split-type-writes-project-mxbuild-cannot.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-microflow-using-split-type-writes-project-mxbuild-cannot.json new file mode 100644 index 0000000000..4021edc3dd --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-08-microflow-using-split-type-writes-project-mxbuild-cannot.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-08", "symptom": "A microflow using `split type` writes a project mxbuild cannot **load**: `KeyNotFoundException: The given key '' was not present in the dictionary` at `StreamingBsonUnitReader.ResolvePostponedProperties`. `mxcli check` ✓ and `mxcli exec` ✓; reproduced on 11.6.6 and 11.13.0", "cause": "Two gaps in the modelsdk writer, both the #791 shape. (1) `microflowObjectToGen` had no `*microflows.InheritanceSplit` case → `default: return nil`, so the split was dropped while three sequence flows kept pointing at its `$ID`. (2) `caseValueToGen` had no `InheritanceCase` case → every branch degraded to a bare `Microflows$NoCase`, losing the entity it selects on. Its value-receiver normalisation also omitted the type, so a pointer-only fix would still miss half the calls", "file": "`mdl/backend/modelsdk/microflow_write.go` (`microflowObjectToGen`, `caseValueToGen`) — mirror `sdk/mpr/writer_microflow.go`", "insight": "Add both cases. **Diagnose with the #791 recipe**: `mxcli bson dump --type microflow`, collect every `$ID`, check each key ending in `Pointer` resolves (before: 27 objects / 3 dangling; after: 28 / 0). **Take field lists from the GENERATED type, not from legacy** — legacy writes `ErrorHandlingType` on the split but `initInheritanceSplit` has no such property, i.e. legacy writes a field Mendix does not define. **When adding a case-value type, update the value-receiver normalisation too.** Modelling rules confirmed on both versions while verifying: a type split needs an outgoing flow for every type INCLUDING the base (CE0090), and an `else` does NOT substitute for the base-type case. Tests `TestMicroflowRoundTrip_InheritanceSplit`, `TestCaseValueToGen_InheritanceCase{,ValueReceiver}`; repro `mdl-examples/bug-tests/split-type-dangling-pointer.mdl`", "refs": ["#791"], "ce": ["CE0090"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-09-re-running-create-external-entities-from-adds-two-more.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-09-re-running-create-external-entities-from-adds-two-more.json new file mode 100644 index 0000000000..13715baad3 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-09-re-running-create-external-entities-from-adds-two-more.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-09", "symptom": "Re-running `create external entities from` adds two more associations every time — `season_2`, `season_3`, then `season_4`, `season_5`, … unbounded. `mx check` is clean, every test passes, and the only symptom is duplicate links in Studio Pro's domain model", "cause": "The re-import dedup matches an existing association by the OData **nav-property** name, but association names are unique per MODULE, so the second entity with a `season` nav property is stored as `season_2` — which can never match `season`. The nav-property index that would have matched it is keyed on `RemoteParentNavigationProperty`, and the **modelsdk reader never read the OData source back**, so that index was always empty under the default engine. The legacy reader did read it, which is why this only bit the default path", "file": "`mdl/backend/modelsdk/domainmodel.go` (`assocFromGen` now reads `*genRest.ODataRemoteAssociationSource`), `mdl/executor/cmd_contract.go` (`indexExistingAssociations`)", "insight": "**A field that the write path sets and the read path drops survives exactly one save.** Grep both directions when a round-trip matters — `RemoteParentNavigationProperty` appeared in `domainmodel_write.go`, in gen, and in the legacy parser, and was missing only from the modelsdk read, which is invisible if you only grep for the identifier. **Do not \"fix\" this by stripping a trailing `_` from names**: that heuristic also swallows a user's genuine `season_2`, and the model already records the true origin. Damage is cumulative and pre-existing: a project that ran the import N times carries 2N spurious associations, and they cannot simply be deleted — external-entity access rules reference them, so removing one leaves CE1613 \"The selected association no longer exists\". Tests `mdl/backend/modelsdk/association_odata_source_test.go` (round-trip, plus a control that a plain association is not stamped as external) and `mdl/executor/cmd_contract_reimport_test.go` (the index, incl. the assertion that the suffixed ones match via the nav index and NOT the name index — otherwise the guard passes for the wrong reason). mxcli-formula1 §50", "ce": ["CE1613"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-10-describe-renders-activity-empty-action-entire-error-branch.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-10-describe-renders-activity-empty-action-entire-error-branch.json new file mode 100644 index 0000000000..c1d4a53d60 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-10-describe-renders-activity-empty-action-entire-error-branch.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-10", "symptom": "DESCRIBE renders an activity as `-- Empty action` and its entire error branch is missing from the output — the MDL stays syntactically valid, nothing is printed to stderr, and a describe→edit→exec cycle deletes both", "cause": "Two unrelated defects stacked. (a) The modelsdk reader — the DEFAULT engine — had no case for the action's `$Type`, so `ActionActivity.Action` was nil and `formatAction(nil)` returned the anonymous comment; 27 of 58 `MicroflowAction` types were in that state, 12 of them ones the legacy parser handles. (b) `emitActivityStatement` returned as soon as the statement started with `--`, *before* consulting `errorHandlerFlow`, so the branch was never traversed — for **every** unmapped action, not just the reported one", "file": "`mdl/backend/modelsdk/microflow.go` (nil → `UnsupportedAction` stand-in carrying `$Type` + `ErrorHandlingType`), `mdl/backend/modelsdk/microflow_read_actions.go` (`errorHandlingTypeOf` reads the field by property name), `mdl/executor/cmd_microflows_show_helpers.go` (`emitCommentedErrorHandler`), `mdl/executor/cmd_microflows_format_action.go`", "insight": "**Count the gap before fixing the instance** — decoding each `$Type` through the real reader turned \"SynchronizeAction is unsupported\" into \"27 types are, and `ExecuteDatabaseQueryAction` is a shipped feature you cannot describe back\". **The early return is load-bearing**: annotations before a line comment make exec fail `no viable alternative at input '@position...'`, so keep it and emit the branch **commented-out** — emitting it live is worse than dropping it, since the next exec runs those activities unconditionally in the main flow instead of on failure. **Never take an enum from the UI wording**: Studio Pro says \"Selected object(s)\", the model stores `Specific` (Model SDK `SynchronizationType`, fetched via `npm pack mendixmodelsdk` — authoritative when `reference/` is not vendored). **A Mendix array's element 0 is a version marker, not data**: `property.EnumList.BSONValue()` returns a bare `[]string`, so a one-element list read back empty and mxbuild said CE2004 \"No variables to synchronize have been selected.\" with the value plainly in the dump. Also CE0011 if a custom error handler has no outgoing error flow — call `finishCustomErrorHandler`. Tests `cmd_microflows_unsupported_action_test.go`, `microflow_synchronize_test.go`; example `mdl-examples/bug-tests/863-synchronize-and-unsupported-actions.mdl`. upstream #863", "refs": ["#863"], "ce": ["CE0011", "CE2004"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-10-execute-database-query-written-correctly-builds-but-describe.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-10-execute-database-query-written-correctly-builds-but-describe.json new file mode 100644 index 0000000000..a06656046f --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-10-execute-database-query-written-correctly-builds-but-describe.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-10", "symptom": "`execute database query` is written correctly and builds, but DESCRIBE renders it as `-- Empty action` (or, after #863, `-- Unsupported action: DatabaseConnector$ExecuteDatabaseQueryAction`) — a shipped feature you can author and never get back", "cause": "The modelsdk reader (the DEFAULT engine) had no case for it. Both the MDL grammar and the DESCRIBE formatter were already there; only `actionFromGen` was missing, so `ActionActivity.Action` came back nil", "file": "`mdl/backend/modelsdk/microflow_read_actions.go` (`case *genDb.ExecuteDatabaseQueryAction`, plus `rawDocElements` for versioned arrays)", "insight": "**The `$Type` is `DatabaseConnector$ExecuteDatabaseQueryAction`, not `Microflows$`** — several actions live in their own sub-metamodel, so a probe under the wrong prefix proves nothing either way and a `case \\*genMf\\.` grep under-counts what is mapped. **Read from raw against the keys the WRITER builds** (`Query` / `DynamicQuery` / `OutputVariableName` / the two mapping lists): the writer constructs the element directly rather than through gen setters, and gen exposes the named query as `QueryQualifiedName`, so pinning reader and writer to one key set is what makes the round trip mean anything. **Element 0 of a versioned array is the marker, not a mapping** — skip non-documents rather than slicing `[1:]`. **A text-stable round trip is not a valid model**: `describe→exec→describe` diffed clean while `mx check` died at load, because the fixture's `create database connection … username 'app'` wrote a literal into a ConstantIdentifier property (`StorageLoadException`, still-open defect, same class as #854) — always run the build, not just the diff. Tests `microflow_dbquery_test.go`, example `mdl-examples/bug-tests/863-execute-database-query-describe.mdl`. upstream #863 follow-up", "refs": ["#854", "#863"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-alter-page-over-mcp-fails-against-studio-pro-11-13-pg-patch.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-alter-page-over-mcp-fails-against-studio-pro-11-13-pg-patch.json new file mode 100644 index 0000000000..d940e20344 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-alter-page-over-mcp-fails-against-studio-pro-11-13-pg-patch.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-11", "symptom": "`ALTER PAGE` over `--mcp` fails against Studio Pro **11.13** with `pg_patch_page: … PROP_NOT_PRIMITIVE: Property 'widgets' is not a primitive property`. `CREATE PAGE` is fine; the page itself is left intact", "cause": "11.13 gave `pg_read_page` a **`depth` argument defaulting to 4**, replacing anything deeper with the literal string `\"...\"`. ALTER PAGE is read-modify-**replace-whole-page**, so the truncated read went straight back as the new page body. Measured live: `Administration.Account_Overview` read 32,594 bytes at full depth but **1,052 bytes** at the default, its entire tree reduced to `{\"widgets\":[\"...\",\"...\"]}`. Every ordinary page truncates — three of three PgTest pages did", "file": "`mdl/backend/mcp/page.go` (`pgReadPage`, `pgReadFullDepth`, `hasTruncationSentinel`), `mdl/backend/mcp/client.go` (`SupportsToolArg`)", "insight": "Request the full depth, and **guard rather than trust it**: refuse a read still carrying the sentinel instead of letting a partial page reach a write (ADR-0005 guard-don't-drop). Two traps. (1) **Do not send `depth` unconditionally** — 11.11/11.12 declare `pg_read_page` `additionalProperties:false` without it, so the whole call fails; gate on a live `tools/list` probe of the tool's input schema, because `serverInfo.version` is frozen at `1.0.0` across 11.11/11.12/11.13 and cannot discriminate releases. (2) **Match the sentinel only as an array element** — a caption or title legitimately reading `\"...\"` is real content, and a naive substring scan rejects valid pages. The release notes announced none of this, exactly as 11.12 silently removed `pg_write_page` (#697): on any Studio Pro upgrade, re-probe `tools/list` and diff the input schemas, not just the tool names. Tests `mdl/backend/mcp/page_depth_test.go`; controls: stub the depth arg (full-depth test fails) and stub the guard (truncation test fails)", "refs": ["#697"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-call-workflow-get-workflow-data-workflows-activity-records.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-call-workflow-get-workflow-data-workflows-activity-records.json new file mode 100644 index 0000000000..931b8376ad --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-call-workflow-get-workflow-data-workflows-activity-records.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-11", "symptom": "`call workflow`, `get workflow data/workflows/activity records`, `open`/`lock`/`unlock workflow` and `workflow operation` all DESCRIBE as a placeholder, so a describe→edit→exec cycle deletes them — while authoring and building them works fine", "cause": "The modelsdk reader (the DEFAULT engine) had no case for any of the eight; each read back with a nil Action. The formatters and grammar were already there, so only `actionFromGen` was missing", "file": "New `mdl/backend/modelsdk/microflow_workflow_read.go` (+ dispatch in `microflow_read_actions.go`), `mdl/grammar/domains/MDLMicroflow.g4` + `mdl/visitor/visitor_microflow_workflow.go` (positional `call workflow` form), `mdl/executor/cmd_microflows_builder_workflow.go` (abort reason)", "insight": "**Read against the WRITER's keys, not gen's** — gen binds every \"get\" action's result as `VariableName` while the model stores `OutputVariableName`, so an accessor-based reader silently drops the variable; round-trip tests (not reader-only tests on hand-written BSON) are what catch it. **`WorkflowSelection` has two variants and the object form uses `WorkflowDefinitionVariable`**, not `WorkflowVariable`; its ABSENCE is meaningful (the all-workflows case), so do not synthesise one. **Dispatch the operation on `$Type` before reading fields** — only Abort carries a Reason. **Finishing the reader exposes what could not be seen while nothing rendered**: `call workflow` described to a positional form the grammar rejected (the model never stores the parameter NAME, so DESCRIBE cannot emit the named form), and the abort reason stored the expression *including its quotes* into a StringTemplate Text, so Mendix rendered the quotes at runtime and every round trip doubled them. **Still open**: `lock/unlock workflow all` writes an activity mxbuild rejects with CE1825 — a lock always needs a specific definition, and supplying an empty selection does not satisfy it. Tests `microflow_workflow_read_test.go`, example `mdl-examples/bug-tests/workflow-actions-describe.mdl`", "ce": ["CE1825"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-describe-import-mapping-m-x-describe-export-mapping-m-x.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-describe-import-mapping-m-x-describe-export-mapping-m-x.json new file mode 100644 index 0000000000..ea418bcde1 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-describe-import-mapping-m-x-describe-export-mapping-m-x.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-11", "symptom": "`describe import mapping M.X` / `describe export mapping M.X` reports \"not found\" for a mapping that exists and that `show` lists. `create or modify json structure` silently creates a duplicate instead of modifying the existing one. All of them work if the document happens to sit directly under the module rather than in a folder", "cause": "`Backend.moduleNameFor` resolved a unit's module by reading its **direct** container (`GetModule(u.ContainerID)`). Studio Pro nests documents in folders — and folders in folders — so for any foldered document the container is a `Projects$Folder`, `GetModule` returns nil, the name resolves to `\"\"`, and every `…ByQualifiedName` lookup built on it misses. In a stock marketplace project this is the *normal* case, not an edge case: 15 of 16 microflows, both JSON structures and both mappings were foldered, while all 8 domain models sat directly under their module — which is exactly why the three domain-model callers worked and hid the defect", "file": "`mdl/backend/modelsdk/domainmodel_write.go` (`moduleNameFor`); callers in `mapping_write.go`, `json_write.go`, `microflow.go`, `domainmodel_alter.go`", "insight": "Walk the containment chain to the first enclosing module instead of reading one level. Build the parent map once (the old code re-scanned `ListUnits()` per call, inside a loop over every document — O(n²)), and **guard the walk against cycles**: the project root is its own container, so an unguarded loop hangs. Behaviour is unchanged for direct children, so the domain-model callers are unaffected. **Generalisable — the shape to look for**: a containment lookup that reads one level where the model allows arbitrary nesting. It works on every fixture built by `create module` + `create X` (nothing is foldered) and fails on every real project, so unit tests pass while users see \"not found\". **Diagnostic trap**: `describeImportMapping` wraps any error containing \"not found\" in `mdlerrors.NewNotFound`, so the surfaced message is identical whichever layer failed and cannot be used to identify the engine — call the reader and the backend directly to tell them apart. The legacy `sdk/mpr` reader walks the chain correctly (`buildContainerModuleNameMap`), so **the two engines disagreed** and only the default (modelsdk) was broken. Test `mdl/backend/modelsdk/module_resolution_test.go`"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-transform-module-transformer-reports-created-microflow-build.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-transform-module-transformer-reports-created-microflow-build.json new file mode 100644 index 0000000000..7ad51cfa05 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-11-transform-module-transformer-reports-created-microflow-build.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-11", "symptom": "`transform $In with Module.Transformer` reports \"Created microflow\" and the build then fails `[CE0008] \"No action defined.\"` — the activity is in the model with no action inside it", "cause": "The modelsdk writer (the DEFAULT engine) had no `TransformJsonAction` case, so it fell through to `default: return nil`. The grammar, builder, DESCRIBE formatter and the LEGACY writer all handled it, which is why it looked supported. The reader was missing too, so even a correctly written action described as a placeholder", "file": "`mdl/backend/modelsdk/microflow_write.go` (writer case), `mdl/backend/modelsdk/microflow_read_actions.go` (reader cases for `TransformJsonAction`, `CallExternalAction`, `RestOperationCallAction`)", "insight": "**A missing WRITER case and a missing READER case present identically in a coverage audit** — grepping for reader cases found `transform` \"authorable but unreadable\" when it was actually unwritable, a strictly worse bug. Check both directions before sizing the work. **Mirror the legacy serializer for keys** (`InputVariableName`/`OutputVariableName`/`Transformation`); for the other two, note `CallExternalAction` stores its result under `VariableName` (not `ResultVariableName`) and REST's two mapping lists are NOT symmetric — the query list keys its name as `QueryParameter`. **Do not reconstruct `CallExternalAction.ResultDataType`**: it is resolved from the consumed service's cached `$metadata` at write time, so reading it back would let a stale value round-trip as if authored. **CE0008 turning into CE1613 is progress, not a new bug** — the action now exists and the build has moved on to validating its reference. Tests `microflow_integration_actions_test.go`, example `mdl-examples/bug-tests/transform-json-write-and-describe.mdl`", "ce": ["CE0008", "CE1613"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-12-alter-entity-add-attribute-entity-has-regex-range-validation.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-12-alter-entity-add-attribute-entity-has-regex-range-validation.json new file mode 100644 index 0000000000..bb077cc842 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-12-alter-entity-add-attribute-entity-has-regex-range-validation.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-12", "symptom": "`alter entity … add attribute` on an entity that has a **RegEx or Range** validation rule silently turns that rule into a **Required** rule: the regex reference (or the min/max bounds) is gone, the field is merely mandatory, and `mx check` reports nothing — a Required rule is perfectly valid. Happens on BOTH engines", "cause": "A matched pair of fallbacks. The modelsdk READER (`ruleTypeFromGen`) collapsed every RuleInfo except Unique to `\"Required\"`, and BOTH writers (`ruleInfoToGen`, `serializeRuleInfo`) fell back to `RequiredRuleInfo` for any type they did not recognise. ALTER ENTITY round-trips the whole entity through read→write, so the two defaults compose into a silent downgrade", "file": "`mdl/backend/modelsdk/domainmodel.go` (`ruleTypeFromGen` now derives the name from `$Type`), `mdl/backend/modelsdk/domainmodel_write.go` + `sdk/mpr/writer_domainmodel.go` (both rule-info serializers return **nil = refusal**, plus `reproducibleRuleType`/`validationRulesAreReproducible`), guards in both `UpdateEntity` implementations", "insight": "**A read default and a write default are individually defensible and jointly destructive** — neither function looks wrong alone; the corruption only exists in the round trip. When auditing a rewrite path, check the reader's fallback and the writer's fallback TOGETHER. **`mx check` is not a safety net for a type substitution**: both rules are valid, so the build stays green and the only evidence is the stored `$Type`. **A fourth gen-vs-reality key mismatch**: `modelsdk/gen` binds `RegExRuleInfo`'s reference to BSON `RegularExpression`, while the Studio Pro document (Mendix Email Connector 6.4.2) stores `RegExIdentifier` — so RegEx rules cannot be written through gen at all today, which is *why* the fix is a refusal rather than a round trip. There is no `supplements.json` or `cmd/modelsdk-codegen` in this tree, so correcting gen is not currently available. **Injection fixtures need ORDERED BSON**: re-marshalling a unit through `bson.M` randomises key order and Mendix refuses to load it (`Expected '$ID' as the first property of a storage object`) — use `bson.D` throughout, or the fixture's own invalidity masks the thing you are testing. **Required/Unique rules were never affected**, and are authorable today via the `not null error '…'` / `unique error '…'` attribute constraints — only the standalone `create validation rule` statement is dead (grammar with no AST, visitor or handler). Tests `sdk/mpr/writer_validationrule_test.go`, `mdl/backend/modelsdk/validationrule_test.go`"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-12-call-java-action-whose-parameter-microflow-mcpserver-addtool.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-12-call-java-action-whose-parameter-microflow-mcpserver-addtool.json new file mode 100644 index 0000000000..ce5d06c99c --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-12-call-java-action-whose-parameter-microflow-mcpserver-addtool.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-12", "symptom": "A `call java action` whose parameter is a **microflow** (`MCPServer.AddTool`'s `ExecutingMicroflow`, and every other \"register a callback\" action) fails `mx check` with **CE0115** — the microflow's name is stored as a literal string", "cause": "The modelsdk engine's java-action read had no case for the parameter type, so `codeActionBasicFromGen` fell through to its `default` and returned a `StringType`. The microflow builder's `microflowTypeParams` branch was therefore never taken and it authored `Microflows$BasicCodeActionParameterValue{Argument: \"'M.MyFlow'\"}` instead of `Microflows$MicroflowParameterValue{Microflow: \"M.MyFlow\"}`", "file": "`mdl/backend/modelsdk/java_read.go` (`codeActionBasicFromGen`), `java_write.go` (`codeActionParamTypeToGen`), `mdl/executor/cmd_javaactions.go` (`astDataTypeToJavaActionParamType`, `bareDataTypeName`)", "insight": "**A `default:` that returns a plausible type is how a read path lies.** The legacy parser and the executor both handled this correctly for years; only the gen→semantic converter did not, and it degraded to String rather than erroring — so every layer downstream looked correct in isolation. **Fix read and write together**: without the write case an *update* of such an action rewrites the parameter as a String, which is worse than the read bug. **Watch what DESCRIBE starts printing** — the fix made it emit the honest `Microflow`, which then round-tripped into an entity type with an empty module (`.Microflow`) until `astDataTypeToJavaActionParamType` learned the bare word. Probe the stored `$Type` from the marketplace `.mpk` before theorising; MCP Server 5.1.0 stores `JavaActions$MicroflowJavaActionParameterType`. Tests `mdl/backend/modelsdk/java_read_test.go`, `mdl/executor/cmd_javaactions_test.go`, example `mdl-examples/bug-tests/javaaction-microflow-parameter.mdl`; verified 0 errors under `mx check` 11.12.1. Reported in mxcli-chat FINDINGS §36", "ce": ["CE0115"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-13-grant-role-module-specialization-read-write-writes-model.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-13-grant-role-module-specialization-read-write-writes-model.json new file mode 100644 index 0000000000..42adced899 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-13-grant-role-module-specialization-read-write-writes-model.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-13", "symptom": "`GRANT ON Module.Specialization (READ *, WRITE *)` writes a model Mendix rejects with **CE0066** \"Entity access is out of date\", while the same grant on the generalization checks clean. The executor passes 3 MemberAccess entries and storage holds 2", "cause": "`ReconcileMemberAccesses` — which runs after every program and after every `create association` — recomputes each rule's expected member set from the entity's **own** attributes and the module's **FROM-side** associations. An inherited association matched neither, so it was classed stale and deleted, one step after the GRANT that had just written it correctly", "file": "`mdl/backend/modelsdk/domainmodel_security_write.go` (`sameModuleAncestors`, `entitiesByName`, `assocRefBelongsTo`, `ownerIDs` in `ReconcileMemberAccesses`), `mdl/executor/cmd_associations.go` (reconcile after DROP)", "insight": "**The write path was innocent** — `AddEntityAccessRule` stored all 3; the deletion happened in the reconcile that ran next. When a value is written and then absent, instrument the *later* pass before the writer. **Preserve what cannot be checked**, as the attribute branch already did (#758): an association is qualified by the module that DECLARES it, so an ancestor in another module (the reported `OpenAIDeployedModel extends GenAICommons.DeployedModel`) names a domain model that is not loaded here. **The counter-control is the whole test**: a walk that collected every association in the module passes both positive cases and puts an entry on the TO side of an `OWNER Default` association, which is *itself* CE0066 — so assert that the unrelated entity does **not** get one. **Reconcile must add as well as keep**, or a rule written before the ancestor gained the association never catches up. Verifying this surfaced a neighbouring bug: `drop association` never reconciled at all, leaving entries Mendix rejects with CE1613 — on the declaring entity too, so it predates inheritance support. Tests `mdl/backend/modelsdk/reconcile_inherited_assoc_test.go` (5 cases incl. both controls), `mdl/executor/cmd_associations_mock_test.go`; example `mdl-examples/bug-tests/grant-on-specialization.mdl`, measured 1 error → 0 on mxbuild 11.12.1. Reported in mxcli-chat FINDINGS §25", "refs": ["#758"], "ce": ["CE0066", "CE1613"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-14-alter-page-replace-col-insert-after-col-naming-datagrid2.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-14-alter-page-replace-col-insert-after-col-naming-datagrid2.json new file mode 100644 index 0000000000..753c0d38da --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-14-alter-page-replace-col-insert-after-col-naming-datagrid2.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-14", "symptom": "`ALTER PAGE` **`REPLACE `** or **`INSERT AFTER `** naming a DataGrid2 column with a BARE name reports \"Altered page\", but the column vanishes (REPLACE) or nothing changes (INSERT) — and Studio Pro / `mx check` then cannot **LOAD** the project: `System.InvalidCastException: Unable to cast object of type '…LayoutWidgets.DivContainers.DivContainer' to type '…CustomWidgets.WidgetObject'`", "cause": "`ast.WidgetRef` treats only a **dotted** name as a column, so a bare `NextRunAt` gives `Widget=\"NextRunAt\"`, `Column=\"\"`, `IsColumn()==false`. The op skipped the `InsertColumns`/`ReplaceColumn` paths — which work correctly — and fell through to the generic widget path, which built the `COLUMN` as a **layout container** and wrote it into the grid's column list. Nothing refused because `findBsonWidget` **recurses into pluggable-widget internals**, so the bare name *did* resolve — to the column node. `DESCRIBE PAGE` then skipped the malformed node, which is what made REPLACE look like a clean deletion and INSERT like a harmless no-op", "file": "`mdl/backend/pagemutator/mutator.go` (`refuseObjectListItemTarget`, called from `InsertWidget` + `ReplaceWidget`)", "insight": "Discriminate on the resolved node's **`$Type`**: an object-list item (DataGrid2 column, Accordion group, PopupMenu basicItem) is `CustomWidgets$WidgetObject`, a real widget is `Forms$*` / `CustomWidgets$CustomWidget`. Refuse the bare form and name the qualified `grid.column` in the message — do **not** auto-resolve, since guessing which grid was meant is what produced the invalid document. Do not discriminate by *counting* name matches: `columnMatchCount` already existed but only refused when `n > 1`, so the single-match case (the common one) sailed through, and a widget that merely shares a name with some column elsewhere would be a false positive. Guard belongs in the **mutator**, not the executor — `FindWidget` matches columns too, so an executor-level check on it only catches names that are absent entirely, which the mutator already refuses. Repro `mdl-examples/bug-tests/891-alter-page-bare-column-target.fail.mdl`. Issue #891", "refs": ["#891"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-14-authoring-pluggable-widget-property-lives-conditionally.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-14-authoring-pluggable-widget-property-lives-conditionally.json new file mode 100644 index 0000000000..88576fb5a0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-14-authoring-pluggable-widget-property-lives-conditionally.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-14", "symptom": "Authoring a pluggable widget property that lives in a **conditionally shown group** writes a widget Mendix rejects with **CE0463**, while the same widget's other properties take non-default values happily. On ProgressCircle both `showLabel: true` and `labelType: 'percentage'` do it; `showLabel: false` and General-group properties are clean. No DESCRIBE involved", "cause": "Two gaps on the same axis. **Serialization**: #574 nulls the TextTemplate of a HIDDEN conditional property, but left a VISIBLE one null — and Mendix stores an empty `Forms$ClientTemplate` there. Null and empty are each invalid in the other's state, so nulling hidden ones was only half the rule. **Extraction**: the editorConfig reader did not understand a ternary's ELSE branch (`cond ? (…) : hidePropertiesIn([…])`), so the `showLabel` gate was never seen and `labelText` read as visible whenever `labelType` was `\"text\"` — its default", "file": "`mdl/backend/widgetobj/builder.go` (`ApplyVisibilityRules`, `bsonFieldIsNil`), `mdl/executor/editorconfig_extract.go` (`parseGuard` `:` case, `ternaryCondition`, `trailingExpr`)", "insight": "**Fixing one gap alone inverts the bug rather than closing it** — filling visible templates without the missing gate made `showLabel: false` fail where `true` had, because mxcli still thought labelText was visible. Measured both ways round before and after; a fix that moves which case fails is not a fix. **Let Mendix say what the shape should be**: `mx update-widgets` on a COPY of the failing project reconciles the widget, and diffing that against mxcli's output named the single meaningful path (`Object/Properties[N]/Value/TextTemplate` null vs `Forms$ClientTemplate`) out of 969. **The good/bad control does the isolation for free**: authoring the same widget with the boolean both ways gave two documents differing in exactly ONE path, so no other candidate needed testing. **Only CONDITIONAL properties are filled** — Studio Pro's convention for an unset TextTemplate is not uniform (a DataGrid custom-content column stores null for `tooltip` and an empty template for `exportValue`, per `emptyClientTemplateRules`), so filling every unset one would trade this bug for its mirror image. **The extractor's preamble matters**: the ternary is preceded by a whole `switch`, and walking back past the `?` to the function start yields a fragment with an unbalanced `}` that parses to nothing and looks like \"unsupported shape\" — hence `trailingExpr`. Regression signal: the widgetdemo showcase applies with **0 CE0463** (its 4 CE1613 are a pre-existing attribute reference). Tests `mdl/backend/widgetobj/widget_visibility_test.go`, `mdl/executor/editorconfig_extract_test.go`; example `mdl-examples/bug-tests/pluggable-describe-roundtrip.mdl` now exercises the group. Reported in mxcli-ledger FINDINGS #104 follow-on", "refs": ["#104", "#574"], "ce": ["CE0463", "CE1613"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-17-authoring-pluggable-widget-object-list-accordion-group-pop.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-17-authoring-pluggable-widget-object-list-accordion-group-pop.json new file mode 100644 index 0000000000..417dc4c82c --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-17-authoring-pluggable-widget-object-list-accordion-group-pop.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-17", "symptom": "Authoring a pluggable widget with an **object list** (Accordion group, Pop-up menu item, chart series) raises **CE0463** \"the definition of this widget has changed\" on the widget itself, on a widget created seconds ago against the current package", "cause": "An object-list item's **required TextTemplate** that the author left unset was serialized as `null`. `emptyClientTemplateRules` in `widgetobj/builder.go` is a hardcoded per-widget table covering only DataGrid columns, so every other object-list widget fell through it. Required-ness is genuinely absent from the widget XML for these properties — and the widget schema **defaults `required` to true** — so the Accordion's `headerText` is mandatory even though nothing says so", "file": "`mdl/backend/widgetobj/builder.go` (`isUnsetRequiredTextTemplate`, `buildDefaultTextClientTemplateProperty`), plus the entry plumbing in `mdl/types/widget_property_type.go`, `modelsdk/widgets/loader.go`, `sdk/pages/pages_widgets_advanced.go`, `mdl/backend/modelsdk/widget_pluggable_write.go` (`convertPropTypeIDs`)", "insight": "Serialize a required unset TextTemplate with the widget's **shipped ``**, read off the template ValueType's `Translations` beside the `Required` flag. **Both weaker forms were measured and fail**: `null` is CE0463, and an *empty* `Forms$ClientTemplate` is **CE4899** \"Property 'Groups/1/Text' is required\" — so the intuitive \"emit an empty template\" fix only moves the error. Populating is what `mx update-widgets` itself writes (verified: mxcli and the reference now emit the same `'Header'`/`'Koptekst'`). Scope it to **required** properties: filling every TextTemplate is the documented way to take CE0463 from 33 to 127. Note `PropertyTypeIDEntry` exists in THREE places (`mdl/types` — canonical and aliased by `modelsdk/widgets`; `sdk/pages` — what the builder consumes; `sdk/widgets` — the legacy engine's own), so a field added for this must be carried through `convertPropTypeIDs` or it silently never arrives. Repro `mdl-examples/bug-tests/891-accordion-group-nested-widgets.mdl`. Issue #891", "refs": ["#891"], "ce": ["CE0463", "CE4899"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-18-alter-page-set-dynamiccellclass-set-visible-datagrid-2.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-18-alter-page-set-dynamiccellclass-set-visible-datagrid-2.json new file mode 100644 index 0000000000..ce00456f97 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-18-alter-page-set-dynamiccellclass-set-visible-datagrid-2.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-18", "symptom": "`ALTER PAGE SET DynamicCellClass` / `SET Visible` on a DataGrid 2 column reports success and changes nothing visible: the value does not come back from `describe page`, and `mx check` stays at 0 errors", "cause": "`setColumnPropertyMut` wrote every value to `PrimitiveValue`. `columnClass` and `visible` are **Expression**-valued, and a `CustomWidgets$WidgetValue` carries *every* field at once — Expression, PrimitiveValue, TextTemplate, AttributeRef — with the unused ones empty, so key-presence cannot tell you which one to write and the always-primitive default looked plausible. The value landed in a field Mendix does not read", "file": "`mdl/backend/pagemutator/mutator.go` (`columnValueField`, `buildColumnPropKeyMap` now also returns kinds, `bsonWidgetResult.colPropKinds`)", "insight": "Take the kind from the schema — the column's `PropertyTypes[].ValueType.Type` — and write Expression, PrimitiveValue or the TextTemplate path accordingly; refuse the structured kinds (Attribute, DataSource, Action, Widgets) rather than writing a string where a reference belongs. Verified on Mendix 11.13.0: before, the write was invisible to DESCRIBE and `mx check` said 0 errors; after, it round-trips and mxbuild **validates** it — a valid expression builds at 0 errors and a bare identifier is CE0117, matching what CREATE produces for the same input. **Generalisable**: when a container type carries every variant field at once, presence is not a discriminator — the schema is. A silent wrong write is worse than the loud failure it was hiding behind", "ce": ["CE0117"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-18-calculated-module-microflow-attribute-accepted-check-exec.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-18-calculated-module-microflow-attribute-accepted-check-exec.json new file mode 100644 index 0000000000..c8f93a03ce --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-18-calculated-module-microflow-attribute-accepted-check-exec.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-18", "symptom": "`calculated by Module.Microflow` on an attribute is accepted by `check` and by exec (\"Added attribute\"), but the stored document holds a plain `DomainModels$StoredValue` with no calculation link — the microflow name appears nowhere in the domain model unit, `mx check` reports **0 errors**, and the attribute is simply empty at runtime. Both the CREATE (inline) and ALTER (`ADD`/`MODIFY ATTRIBUTE`) paths. A microflow whose signature cannot work is accepted too, masked by the same drop", "cause": "`attributeToGen` in the **modelsdk** writer had arms for OqlViewValue / ODataMappedValue / ODataMappedPrimitiveCollectionValue and a `default:` that emits StoredValue — no `CalculatedValue` arm — so the binding the executor had already resolved fell through and was discarded. The **legacy** writer had the arm all along (`sdk/mpr/writer_domainmodel.go`), which is why the feature read as implemented; modelsdk is the default engine (`--engine`), so everyone hit the broken path. The reader had no `CalculatedValue` case either, so an unrelated ALTER on the same entity destroyed a binding made in Studio Pro", "file": "`mdl/backend/modelsdk/domainmodel_write.go` (`attributeToGen`) + `domainmodel.go` (`attributeFromGen`) + `mdl/executor/calculated_attributes.go` (`resolveCalculatedValue`, called from the three sites in `cmd_entities.go`)", "insight": "Add the write arm (`genDm.NewCalculatedValue`, `SetMicroflowQualifiedName` → the `Microflow` ByNameRef key, `SetPassEntity`) **and** the read arm — a write-only fix leaves the read-modify-write data loss in place, which is the worse half. Derive `PassEntity` from the signature rather than hardcoding it (legacy hardcoded `microflowRef != \"\"`): measured on 11.13.0, an entity-parameter microflow (`PassEntity=true`) and a parameterless one (`PassEntity=false`) BOTH build at 0 errors, so refusing the parameterless form would have been wrong. Signature rules are refused at exec (the #833 placement), and each was checked against mxbuild rather than assumed — wrong entity parameter and wrong return type are both **CE7247**, but the return-type message is *\"should be Integer/Long\"*, so **Integer and Long are one family** and a strict equality check refuses valid MDL (caught only by reading the CE text). To ask mxbuild about a binding mxcli now refuses, stub the check and rebuild — `--engine legacy` does NOT bypass it, because the validation lives in the engine-independent executor. Tests `TestAttributeToGen_CalculatedValue`, `TestAttributeFromGen_CalculatedValue`, `TestResolveCalculatedValue_*` (the backend three fail with `value is *domainmodels.StoredValue` when reverted); fixture `mdl-examples/bug-tests/917-calculated-attribute-binding.mdl`. Issue #917", "refs": ["#833", "#917"], "ce": ["CE7247"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-18-describe-database-connection-omits-returns-map-parameter.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-18-describe-database-connection-omits-returns-map-parameter.json new file mode 100644 index 0000000000..ea1493467e --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-18-describe-database-connection-omits-returns-map-parameter.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-18", "symptom": "`describe database connection` omits `returns` / `map (…)` / `parameter` on the **default (modelsdk) engine** while the legacy engine renders them, so a describe → exec round trip drops the return entity, the column mapping and every query parameter", "cause": "The modelsdk reader populated neither `TableMappings` nor `Parameters`, so the DESCRIBE renderer (which does handle them) only ever saw empty slices — dead code on that engine. The round-trip test existed but was pinned to the legacy backend via `setupTestEnv`, so it passed while the default engine was lossy", "file": "`mdl/backend/modelsdk/integration_read.go` (`tableMappingsFromGen`, `queryParametersFromGen`); test `mdl/executor/roundtrip_dbconnection_test.go`", "insight": "Mirror the legacy reader field for field, and run the round-trip test over `gateEngines` rather than the default backend. **The cross-engine DESCRIBE matrix is the check that finds these**: write with engine A, read with engine B, all four combinations — a gap on one engine is invisible from inside that engine. Fixing one field (TableMappings) and not its sibling (Parameters) is the characteristic mistake; enumerate every child collection of the document before calling it done"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-19-alter-page-dotted-datagrid2-column-target-insert-after-grid1.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-19-alter-page-dotted-datagrid2-column-target-insert-after-grid1.json new file mode 100644 index 0000000000..6f0b6c5327 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-19-alter-page-dotted-datagrid2-column-target-insert-after-grid1.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-19", "symptom": "`ALTER PAGE` at a **dotted** DataGrid2 column target (`insert after grid1.Col { container … }` / `replace grid1.Col with { container … }`) reports \"Altered page\", and `mx check` then aborts before checking anything: `System.InvalidCastException: Unable to cast object of type '…DivContainers.DivContainer' to type '…CustomWidgets.WidgetObject'`. Separately: a widget **inserted into a customContent cell** binds nothing — `ContentParams` naming an association path stores the raw path as the attribute name (**CE1613**) and `describe page` shows the path with its first hop silently gone", "cause": "Two defects. (a) #891's guard covers only the BARE column form; the dotted form is legitimate and skips the guard entirely, so a **non-`column` body** still fell through to the generic widget path and was serialized into the grid's column list. The wrong thing is the *pairing* (widgets at an object-list target), not the target. (b) The entity-context walk descended into a pluggable widget's own widget properties but not into an object-list ITEM's — a column keeps its cell widgets one level deeper at `Objects[].Properties[content].Value.Widgets`, the same descent `findInWidgetChildren` gained in #834 — and never read a **pluggable** widget's datasource at all, so the grid's own entity was invisible too. Empty entity context ⇒ `resolveAssociationAttributePath` bails ⇒ the raw path is stored as the attribute name", "file": "`mdl/backend/pagemutator/mutator.go` (`refuseWidgetsAtColumnTarget` called from `InsertWidget`/`ReplaceWidget`; `findEntityContextInChildren`, `widgetOwnEntity`, `entityFromEntityRef`)", "insight": "(a) Refuse when `columnRef != \"\"` — resolve the column FIRST so a mistyped name still reports not-found with the available names, then refuse and name both alternatives (`column …` body, or address the widget inside the cell by its own name, per #834). (b) Descend into `Value.Objects[].Properties[].Value.Widgets`, keyed on the BSON shape rather than the `columns` property key so Accordion groups and PopupMenu items are covered by the same code; resolve a pluggable datasource through the shared `entityFromEntityRef`, which also gives it the IndirectEntityRef (association) case the plain reader already had. **Note the guard cannot key on the target alone** the way #891's does, and a unit test on the mutator alone under-proves it: reverting only shows a nil-deref in the fixture. The honest control is the CLI + `mx check` — measured on Mendix 10.24.20.105674, exception before / 0 errors after, and CE1613 before / 0 after. Repros `mdl-examples/bug-tests/935-alter-page-widgets-at-column-target.mdl` and `935-customcontent-column-entity-context.mdl`. Issue #935", "refs": ["#834", "#891", "#935"], "ce": ["CE1613"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-19-grant-role-entity-read-shrinks-existing-access-rule-instead.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-19-grant-role-entity-read-shrinks-existing-access-rule-instead.json new file mode 100644 index 0000000000..44067df271 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-19-grant-role-entity-read-shrinks-existing-access-rule-instead.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-19", "symptom": "`GRANT ON (READ (A))` **shrinks** an existing access rule instead of widening it: attributes granted by an earlier GRANT come back `None`, and `(CREATE, DELETE, READ *, WRITE *)` followed by a narrow re-grant loses create, delete and every write right. No error, no warning, no diff. Reported as WHERE-specific and as a \"silent security regression\"", "cause": "Two defects. (a) The codec engine's upsert **cleared the matched rule's MemberAccesses and rebuilt them from the current statement alone**. The executor emits an entry for *every* member of the entity (unnamed ones at the rule's default, normally `None`), so each GRANT reset every member it did not mention; `AllowCreate`/`AllowDelete`/default rights were overwritten the same way. Legacy has merged additively since it shipped (`mergeAccessRule`), so this was an **engine regression**, not a longstanding gap — which is why the documented contract (\"GRANT is additive … never removes permissions\") held on one engine and not the default one. (b) **Both** engines matched a stored rule on its module-role set alone, ignoring `XPathConstraint`, collapsing two legitimate rules into one", "file": "`mdl/backend/modelsdk/domainmodel_security_write.go` (`AddEntityAccessRule`); `sdk/mpr/writer_security.go` (the upsert's match + `mergeAccessRule`); lattice shared via `mdl/types.AccessRightsLevel` / `HigherAccessRights`", "insight": "**Measure the reported trigger before scoping the fix to it.** WHERE was a red herring — the same loss reproduced with no WHERE and with `READ *`, so a fix aimed at the constrained path would have passed the reporter's repro and left most of the bug. Equally, the report understated the damage: only attributes were mentioned, but structural rights went too, so re-derive the blast radius rather than inheriting it. **Run the other engine as the control** — `MXCLI_ENGINE=legacy` merging correctly is what turned \"longstanding bug\" into \"regression\" and handed over the reference semantics (OR create/delete, take the higher default, take the higher rights per member) instead of having to invent them. For (b), **check the platform's own docs before calling multi-rule a user error**: Mendix's refguide says \"Rules are additive … all access rights of those rules are combined\", so one role holding one rule per constraint is the normal way to write row-level security, and `mx check` confirms it at 0 errors. Put the constraint in the match key and treat the empty constraint as a *value*, not a wildcard, so an unconstrained and a constrained rule coexist and re-running a script stays idempotent (ADR-0008). **Fixing (b) breaks anything keyed on roles alone**: `formatAccessRuleResult` then echoed a *different* rule's rights back at the user, so it needed the constraint too (REVOKE passes `anyXPath`, since it narrows every matching rule). Rights merge on `None < ReadOnly < ReadWrite`, so a merge never narrows — that is what keeps GRANT and REVOKE inverses rather than two spellings of \"set\". Issue #936", "refs": ["#936"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-20-alter-styling-set-design-property-value-prints-updated.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-20-alter-styling-set-design-property-value-prints-updated.json new file mode 100644 index 0000000000..36d2805927 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-20-alter-styling-set-design-property-value-prints-updated.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-20", "symptom": "`ALTER STYLING … SET '' = ''` prints `Updated styling on widget \"x\"` and writes **nothing**. The same statement works on a Studio Pro-authored widget, and the inline `DesignProperties: [...]` form works everywhere", "cause": "Two defects composing into a silent no-op. `bsonnav.DSet` takes a `bson.D` **by value**, so it can only overwrite an existing key — it returns `false` and does nothing when the key is absent, and `DSetArray` ignored that result. And the key WAS absent: the codec omits an empty, never-appended `PartList`, so `Forms$Appearance` on an mxcli-authored widget carried no `DesignProperties` at all (Studio Pro writes the empty marker `[3]` on all 16 pages of a blank app)", "file": "`mdl/backend/bsonnav/bsonnav.go` (`DSetArray` now returns bool + refuses a markerless write, new `DSetArrayIn`); `mdl/backend/pagemutator/mutator.go` (`writeDesignProperties`); `mdl/backend/modelsdk/widget_write.go` (`Forms$Appearance` MandatoryLists, `CustomWidgets$CustomWidget` LabelTemplate)", "insight": "**Fixing the append without fixing the marker turns a silent no-op into an unopenable project.** A Mendix array's first entry is its typed-array marker; measured on 11.13, a `DesignProperties` array written bare fails to LOAD (`Type OptionDesignPropertyValue does not contain a constructor with a parameter of type Appearance`) and `create-module-package` dies with \"Unknown export error\", while a doubled marker throws `StorageLoadException`. So `DSetArray` refuses rather than guessing, and the marker is passed explicitly at the one call site that creates the property. **Diff against `mx update-widgets`' own output** to find missing keys: handing an mxcli-authored page to Mendix's tool and diffing the unit it writes back (pointer identity preserved, key order intact) surfaced `DesignProperties`, `LabelTemplate` and a set of int32/int64 width mismatches in one pass. Not changed: `Forms$Page.AllowedModuleRoles` is marker 3 where Studio Pro writes 1 — `NewByNameRefListV3` documents CE0557 as the reason, and nothing currently misbehaves. Issue mendixlabs/mxcli#931", "refs": ["mendixlabs/mxcli#931"], "ce": ["CE0557"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-alter-workflow-over-mcp-inserts-call-microflow-studio-pro.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-alter-workflow-over-mcp-inserts-call-microflow-studio-pro.json new file mode 100644 index 0000000000..90324d2d6f --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-alter-workflow-over-mcp-inserts-call-microflow-studio-pro.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-21", "symptom": "`ALTER WORKFLOW` over `--mcp` inserts a `CALL MICROFLOW` and Studio Pro shows **CE0495 \"Duplicate name\"** on the workflow, while `mxcli`'s own read-back looks fine and the statement reported success", "cause": "A `CALL MICROFLOW` activity is named after its target microflow (`buildCallMicroflowTask`: `task.Name = n.Microflow.Name`), so two calls to the same microflow collide before anything inspects them. The file backends deduplicate in `wfmutator`; the MCP backend had no equivalent and sent the derived name verbatim. Two separate gaps, both needed: PED **accepts** the duplicate with `SUCCESS` and does **not** auto-rename (measured live), and `mcpWorkflowMutator.Save()` early-returned when only activity ops had run, so `ped_check_errors` — the only thing that sees a consistency error — was never called. `CREATE`/`CREATE OR REPLACE` are **not** affected: the executor dedups at `cmd_workflows_write.go` for every backend", "file": "`mdl/backend/wfnames/` (shared policy); `mdl/backend/mcp/workflow.go` (`resolve`/`searchActivities` collect names on the walk, `Save`/`checkSettled`); `mdl/backend/wfmutator/mutator.go` (`serializeAndDedup`, `buildSubFlowBson`)", "insight": "Deduplicate **before the add** — PED refuses `set` on an activity's `/name` (`Element type does not support renaming`), so there is no repair after the fact. Collect the taken-set on the walk `resolve` already does, or the fix costs a PED round-trip per level. **Use a taken-set, not a seen-count**: counting re-collides the moment the workflow already contains the suffixed name (`A, A, A_2` counts its way to `A, A_2, A_2`). Uniqueness is workflow-**wide**, so recurse into the inserted activity's own outcome and boundary-event sub-flows. **`ped_update_document` reporting `SUCCESS` proves nothing about consistency** — it reports op-level failures only; `ped_check_errors` is the only signal, and it reads Studio Pro's **background** error list, which lags the write in BOTH directions (26 samples at a 20ms poll interval: error becomes visible 77-115ms after the write, stops being shown 75-128ms after the fix; one symmetric debounce, no growth under load). Asked too early it answers \"No errors found.\" for a document just broken **and** still reports an error a just-applied op has already cleared — measured against an immediate check as the control, 6 trials each: immediate missed the real error 6/6 and falsely failed on a cleared transient 6/6, settled 0/6 on both. `Backend.pedCheckDocument` owns the pacing for every call site (delay, then re-ask while clean) so a new write path cannot forget it. **Poll at a fine interval when measuring a debounce** — a first pass at 50-100ms reported 170-350ms and \"grows under load\", both pure granularity artifacts. Issue #945", "refs": ["#945"], "ce": ["CE0495"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-alter-workflow-replace-activity-x-activity-named-x-silently.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-alter-workflow-replace-activity-x-activity-named-x-silently.json new file mode 100644 index 0000000000..82a1242cfa --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-alter-workflow-replace-activity-x-activity-named-x-silently.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-21", "symptom": "`ALTER WORKFLOW … REPLACE ACTIVITY X WITH ` silently renames it to `X_2`, and re-running the same script compounds it — `X_2_2`, `X_2_2_2`, … A no-op replace (nothing changed) renames too. Invisible to `mxcli check`, `exec` output, and — for `CALL MICROFLOW` steps, whose DESCRIBE renders only the target microflow — to `DESCRIBE` as well", "cause": "The dedup helper was written for INSERT and reused by REPLACE. `collectAllActivityNames()` walks the still-unmodified tree, so the activity about to be spliced out is still in the name pool when the replacement's name is checked against it — a same-name replace therefore always \"collides\" with itself. The MCP backend had the same shape, propagated deliberately in PR #204 on a **guess** that the file backends' choice was intentional", "file": "`mdl/backend/wfmutator/mutator.go` (`serializeAndDedup` gains `freeName`, `ReplaceActivity` passes the outgoing name); `mdl/backend/mcp/workflow.go` (`activityRefMatch.name` / `wfLocation.name`, `delete(loc.taken, loc.name)`); tests `mdl/backend/wfmutator/replace_samename_test.go`, `mdl/backend/mcp/workflow_dedup_test.go`", "insight": "Free **only the outgoing activity's name**, and read it off the **resolved element** — the reference may be a caption, not the name, so using `activityRef` frees the wrong key (or none). Keep a control proving a replacement colliding with a *surviving* activity still dedupes, or the fix over-corrects into CE0495. **Check the severity claim before believing it**: the report argued renames detach in-flight `System.WorkflowUserTask` records, but Mendix's [workflow-versioning docs](https://docs.mendix.com/refguide/workflow-versioning/) list \"changing names, captions, and titles\" as explicitly **non-conflicting** — the engine matches structurally, not by name. The real harm is unbounded accretion and the ADR-0008 idempotence break. **The fix only half-closes that**: a no-op replace still rewrites the `.mpr` because `addFreshPersistentID` re-mints `PersistentId` on every activity write in both engines and neither reads it back (measured: untouched siblings keep theirs, the replaced one does not). Issue #944", "refs": ["#204", "#944"], "ce": ["CE0495"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-create-replace-workflow-silently-deletes-boundary-event.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-create-replace-workflow-silently-deletes-boundary-event.json new file mode 100644 index 0000000000..5f9ddedba6 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-create-replace-workflow-silently-deletes-boundary-event.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-21", "symptom": "`CREATE OR REPLACE WORKFLOW` silently deletes a boundary event (timer + its whole handler flow) or an event sub-process that the script does not restate — `exec` reports \"Created workflow\" and exit 0, and `mx check` afterwards reports 0 errors because the result is a valid workflow that simply no longer does what it did. `DESCRIBE WORKFLOW` does not show the construct either, so nothing reveals the loss", "cause": "Two separate defects. (1) The **default (modelsdk) engine's workflow reader had no boundary-event support at all** — `grep BoundaryEvent mdl/backend/modelsdk/workflow_read.go` → 0, while `sdk/mpr/parser_workflow.go` → 17. Both engines *write* them, so a boundary event mxcli itself had just written read back as absent. (2) A rewrite rebuilds from the statement, so anything unstated is dropped, and no guard existed — unlike the queued-call and validation-rule paths", "file": "`mdl/backend/modelsdk/workflow_read.go` (`boundaryEventsFromGen`, wired into the 6 gen types with `BoundaryEventsItems`); `mdl/executor/validate_workflow_rewrite.go` (`checkNoDroppedWorkflowConstructs`, called from `cmd_workflows_write.go`); `mdl/executor/cmd_workflows.go` (clause order)", "insight": "**Read the stored side from the RAW unit, not the semantic model** — the reader is what was blind, and a guard sharing its blind spot cannot see what it protects; `GetRawUnit` also covers constructs no engine models yet (event sub-processes exist in `modelsdk/gen` but have no semantic type, reader or writer anywhere). Match the `$Type` by **substring** so all three timer variants and any later one are caught. Authorable constructs get the queue treatment (restate them and the rewrite proceeds); unauthorable ones refuse outright. **Fixing a reader can expose a describer bug**: with boundary events finally readable, `describe → exec` stopped parsing, because the call-microflow describer emitted `boundary event` BEFORE `outcomes` and the grammar requires the reverse — pre-existing on legacy, invisible while the default engine emitted neither. Always re-run a describe→re-exec round trip after widening a reader. Issue #948", "refs": ["#948"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-microflow-reads-if-module-somerule-param-x-then-written-no.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-microflow-reads-if-module-somerule-param-x-then-written-no.json new file mode 100644 index 0000000000..51d25549ad --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-21-microflow-reads-if-module-somerule-param-x-then-written-no.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-21", "symptom": "A microflow that reads `if Module.SomeRule(param = $x) then` is written with **no Condition at all** — `mx check` reports **CE0080** \"The 'Condition' property is required.\" naming the Decision, whose caption still shows the call. `DESCRIBE` renders it as `if true then`, so the round trip looks like valid MDL, and re-applying an identical `CREATE OR MODIFY` over a Studio Pro-authored microflow turns a clean project into a broken one", "cause": "Two defects, one behind the other. (a) `splitConditionToGen` handled only `ExpressionSplitCondition` and fell through to `default: return nil`; the caller then skipped `SetSplitCondition` entirely. The READ side had been implemented for #723, and that asymmetry is what made it silent. (b) `modelsdk/gen` binds `Microflows$RuleCall.Rule` to BSON key `Rule`, where Mendix stores **`Microflow`** (rules share the microflow namespace) — so fixing (a) alone writes a reference Mendix never reads and produces the same CE0080 for a different reason", "file": "`mdl/backend/modelsdk/microflow_write.go` (`splitConditionToGen`, new `ruleCallToGen`, the `Microflows$RuleCall` marker registration); `modelsdk/gen/microflows/types.go` (`initRuleCall` + `RuleCall.InitFromRaw` — STORAGE-NAME OVERRIDE on both sides); strike the row off `modelsdk/gen/keyaudit_test.go`; tests `mdl/backend/modelsdk/microflow_rule_split_write_test.go`; repro `mdl-examples/bug-tests/939-rule-split-condition.mdl`", "insight": "**\"Works on Mendix 10, fails on 11\" was an ENGINE difference, not a version one** — same script, same project, `MXCLI_ENGINE=legacy` is 0 errors and the default (modelsdk) is CE0080. The microflow write path branches only on `major >= 10` / `<= 9`, so 10.24 and 11.12 take identical code; check the engine before believing a version story. **A round-trip test cannot see a wrong storage key** — it passes whichever key is used as long as both sides agree, which is exactly the shape of (b) — so assert the KEY on the encoded document, in a separate test with its own control. **Reconstruct the missing document type when the fixture does not exist**: MDL cannot author rules and no module in a blank Mendix 11 app ships one, so the repro rewrote an mxcli-created microflow's unit as `Microflows$Rule` (keeping only the ten properties `initRule` declares) — `mx check` 0 errors on the synthetic rule is the control that it is a valid stand-in. Verify the fix against the legacy engine's BSON, not just against a green build: the two documents are identical modulo key order. Issue mendixlabs/mxcli#939", "refs": ["#723", "mendixlabs/mxcli#939"], "ce": ["CE0080"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-23-grant-module-entity-over-mcp-fails-failed-read-module.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-23-grant-module-entity-over-mcp-fails-failed-read-module.json new file mode 100644 index 0000000000..507c52ee8b --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-23-grant-module-entity-over-mcp-fails-failed-read-module.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-23", "symptom": "`GRANT … ON Module.Entity` over `--mcp` fails with `failed to read module security for X: GetModuleSecurity: not supported by the MCP backend` — `--mcp-verbose` prints **no PED tool call at all**, and `mxcli mcp capabilities` reports entity access rules as available in the same session. `SHOW MODULE ROLES` / `SHOW USER ROLES` / `SHOW PROJECT SECURITY` fail the same way", "cause": "The MCP backend embeds `unsupportedBackend` and overrides only what it supports; `gen_unsupported.go`'s own header says it delegates \"the reads those [writes] need\" — i.e. a read is added **reactively, per write method**. `AddEntityAccessRule` (#704) shipped and was unit-tested, but the executor calls `validateModuleRole` → `GetModuleSecurity` **before** it, and a pre-check made outside the backend package is invisible from inside it. Three reads were missing, not one", "file": "`mdl/backend/mcp/security.go` (`GetModuleSecurity`, `ListModuleSecurity`, `GetProjectSecurity` → `b.reader`); tests `mdl/backend/mcp/grant_executor_test.go`", "insight": "**Test the statement, not the backend method** — a unit test on the method passes against a completely unreachable feature, which is exactly how this shipped; drive real MDL through `executor.New` + `SetBackendFactory` against `newFakePED` and assert a `ped_update_document` actually happened. **Probe the whole class**: the reported symptom was GRANT, but the same three-line omission had broken every security `SHOW`, none of which writes anything. **The capability report is a hand-written YAML declaration, not a verified claim** — `capabilities.go` says report and behaviour \"cannot drift\", and that holds only for the four *method-gated* keys; `entity_access_rules` is a display-only row, so it can and did contradict the very next command. Routing a read to the local `.mpr` is safe **only when PED is not a writer of that document**: roles are by-name references Studio Pro resolves on apply, so a stale file yields \"module role not found\", never a wrong write — unlike the domain model, which needs the dirty-set router. A module created over PED this session has no security document on disk at all, so answer that case explicitly rather than letting the reader say \"not found for module mcp~module~X\". Issue #900", "refs": ["#704", "#900"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-24-pluggable-widget-s-text-template-property-authored-silently.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-24-pluggable-widget-s-text-template-property-authored-silently.json new file mode 100644 index 0000000000..1132c4b920 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-24-pluggable-widget-s-text-template-property-authored-silently.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-24", "symptom": "A pluggable widget's text-template property is authored and **silently dropped** — the document keeps the enumeration that says the text exists but no text, so `tooltipType: 'customText', tooltip: '…'` builds as *\"'Tooltip' cannot be empty when 'Custom' type is chosen\"*. The tell that makes it findable: `tooltip: 'Value {Score}'` WORKS while `tooltip: 'Plain text'` does not", "cause": "`setTextTemplateValue` only ever **updated an existing** TextTemplate and returned the element untouched when it was nil. A CONDITIONAL template is stored nil while its condition hides it (#574), and the Slider's tooltip is hidden under its own default `tooltipType: \"value\"` — so a statement that turns the condition on and writes the text in one go finds nothing to edit. `ApplyVisibilityRules` then fills the now-visible property with an EMPTY template, which is what reaches disk. The `{AttrName}` spelling escapes it only because `SetTextTemplateWithParams` **replaces** the whole template instead of editing it", "file": "`mdl/backend/widgetobj/builder.go` (`setTextTemplateValue`); the params gap it exposed is `mdl/executor/widget_engine.go` (`applyOperation` \"texttemplate\" + `resolveMapping` \"TextTemplate\", `BuildContext.ClientParams`)", "insight": "Build the envelope when the slot is nil (`updateTemplateText(BuildEmptyClientTemplate(), text)`) rather than special-casing visibility — `ApplyVisibilityRules` still runs afterwards and re-nulls a genuinely pruned property, so authored text can never survive into a slot the editor hides. **Studio Pro is the tool that localises this**: `pg_read_page` shows the stored template as a well-formed, EMPTY `Pages$ClientTemplate`, which distinguishes \"no text was written\" from \"the template is malformed\" — the BSON cannot. Two traps. Fixing the drop makes the **`{1}` spelling reachable**, and the MAPPING pass ignored `contentparams:` where the explicit-property pass has routed it since #928, so the newly-landing text hit CE0720 until both passes agreed. And **DESCRIBE still does not emit any text-template property for a pluggable widget** (`extractExplicitProperties` reads AttributeRef and PrimitiveValue only), so a tooltip survives a write and is lost on the next describe→exec — measured on the Timeline control too, i.e. every widget, tracked separately. Repro `mdl-examples/bug-tests/254-slider-custom-tooltip.mdl`. Issue #254", "refs": ["#254", "#574", "#928"], "ce": ["CE0720"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-25-describe-microflow-prints-range-list-range-stores-offset.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-25-describe-microflow-prints-range-list-range-stores-offset.json new file mode 100644 index 0000000000..75ac5e0e40 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-25-describe-microflow-prints-range-list-range-stores-offset.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-25", "symptom": "`describe microflow` prints `range($List)` for a range that stores an offset and an amount — both expressions dropped, no warning, exit 0, and `mxcli check` clean on the truncated output, which re-executes as an *unpaged* range. Mirror-image on the write side: `range($L, $o, $a)` authored through mxcli builds as **CE6520** *\"Amount and offset are not specified\"*, and on `--engine legacy` the project will not **load** at all (*\"Expected '$ID' as the first property of a storage object, but got 'NewOperation'\"*)", "cause": "Mendix nests the bounds one level below where mxcli looked: `Microflows$ListRange{ListName, CustomRange}` → `Microflows$CustomRange{LimitExpression, OffsetExpression}`. The modelsdk engine had them **flat on both sides** — `listOperationToGen` wrote them onto the ListRange and `listOperationFromRaw` read them from there — so mxcli round-tripped its own documents perfectly and only Mendix disagreed. The legacy engine had no `ListRangeOperation` case at all, fell through to `return nil`, and `serializeListOperationAction` appended that nil under `NewOperation`, writing an **empty sub-document** that Mendix's loader rejects outright", "file": "`mdl/backend/modelsdk/microflow_write.go` (`listOperationToGen`), `mdl/backend/modelsdk/microflow_read_actions.go` (`listOperationFromRaw`), `sdk/mpr/writer_microflow_actions.go` (`serializeListOperation` + the nil guard in `serializeListOperationAction`), `mdl/executor/validate_microflow.go` (`checkRangeHasABound`, MDL068); tests `mdl/backend/modelsdk/microflow_listrange_test.go`, `sdk/mpr/writer_listoperation_test.go`, `mdl/executor/validate_range_bounds_test.go`; repro `mdl-examples/bug-tests/list-range-custom-bounds.mdl`", "insight": "**A clean round trip proves nothing when one engine wrote the document.** Both sides being wrong in the same direction is what hid this for a release: the reporter concluded \"the authoring side works correctly\" from exactly that evidence. Read a document Studio Pro wrote, or cross-read with the *other* engine — here the legacy PARSER had been right all along, so `--engine legacy` on a default-engine project was a free control. **`generated/metamodel` settles the nesting**: a `*Microflows$X` pointer field is a child element, a plain string is a property. When a writer's `default:` returns nil, check what the CALLER does with it — appending nil is how an unloadable file gets written, and omitting the key (CE0008, which names the activity) is always the better failure. The reader keeps a **flat fallback** so a project written by the broken writer still describes its bounds and repairs itself on the next `exec` — verified: CE6520 → describe → exec → 0 errors", "ce": ["CE0008", "CE6520"], "rules": ["MDL068"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-26-alter-page-set-layout-alter-layout-cannot-find-any-widget.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-26-alter-page-set-layout-alter-layout-cannot-find-any-widget.json new file mode 100644 index 0000000000..74ca1803e0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-26-alter-page-set-layout-alter-layout-cannot-find-any-widget.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-26", "symptom": "`ALTER PAGE … SET Layout` / `ALTER LAYOUT` cannot find any widget in a layout — \"widget X not found\" for a widget DESCRIBE LAYOUT clearly shows", "cause": "Two finder gaps compounding. `findBsonWidget` starts from the page's `FormCall`, which a `Forms$Layout` does not have (its tree hangs off `Content`), so every widget in every layout was unreachable; and `findInWidgetChildren` never descended into a `ScrollContainer`'s five named slots, so a layout's topbar (`Top`) and navigation (`Left`) were out of reach even once the root was found — as was anything inside a scroll container a *page* places", "file": "`mdl/backend/pagemutator/mutator.go` (`findBsonWidgetInLayout`, region descent in `findInWidgetChildren`), `mdl/backend/pagemutator/scrollregion.go`", "insight": "**A region is addressed by slot, not by name** — it has no `Name`, so `INSERT INTO layoutContainer.top` reuses the dotted widgetRef that also serves DataGrid2 columns, and which one it means is decided by the named widget's `$Type`. Only `INSERT INTO` takes a region: BEFORE/AFTER position a widget among siblings, and silently treating them as INTO would put widgets somewhere the script did not ask for. The MDL slot name is `center`; the stored key is `CenterRegion`. Controlled by emptying the slot list and by respelling the centre key"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-26-every-layout-reports-layout-type-responsive-including-phone.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-26-every-layout-reports-layout-type-responsive-including-phone.json new file mode 100644 index 0000000000..c30dd7d1eb --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-26-every-layout-reports-layout-type-responsive-including-phone.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-26", "symptom": "Every layout reports `Layout Type: Responsive` — including the Phone, Tablet, ModalPopup and native ones, 19 of the 22 Atlas ships", "cause": "Two faults compounding. `LayoutType` is **not on `Forms$Layout`**; it is on the content wrapper. `modelsdk/gen` nonetheless exposes `Layout.LayoutType()`, bound to a key the document does not carry, so it reads `\"\"` — a phantom property, not a wrong key, which is why `keyaudit_test.go` does not list it. `DESCRIBE LAYOUT` then defaulted `\"\"` to `\"Responsive\"`, rendering a failed read as a fact", "file": "`mdl/backend/modelsdk/page.go` (`layoutTypeOf` reads the wrapper), `mdl/executor/cmd_pages_describe.go` (no fallback)", "insight": "**Never default a value every real document carries** — an empty one means the read failed, and a plausible default is what hides it. `generated/metamodel` was right about the location all along (property of `PagesWebLayoutContent`/`PagesNativeLayoutContent`), so the arbiter rule held; what no source got right was the *value set*, six values splitting by platform (web: Responsive/Phone/Tablet/ModalPopup, native: Default/Popup) against two declared in gen and in the metamodel enum. Measured across all 22"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-26-layout-mxcli-wrote-builds-cleanly-mx-check-0-errors-but.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-26-layout-mxcli-wrote-builds-cleanly-mx-check-0-errors-but.json new file mode 100644 index 0000000000..cef756f5da --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-26-layout-mxcli-wrote-builds-cleanly-mx-check-0-errors-but.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-26", "symptom": "A layout mxcli wrote builds cleanly (`mx check`: 0 errors) but Studio Pro refuses to open the project: `System.InvalidOperationException: Sequence contains no matching element` at `MprProperty.cs`", "cause": "The layout carried `MainPlaceholderName`. `modelsdk/gen` declares it on `Layout` so the setter compiles, and mxbuild tolerates unknown properties — but `generated/metamodel`'s `PagesLayout` declares only eight properties and none of the 22 layouts Atlas ships on 11.13.0 carries the key. Studio Pro resolves every stored property against the type's list", "file": "`mdl/backend/modelsdk/layout_write.go` (`layoutToGen`), `mdl/executor/cmd_pages_layout_v3.go` (the header refuses any property but `layouttype`)", "insight": "**A `Forms$Layout` has exactly ten keys** — `$ID`, `$Type`, `Appearance`, `CanvasHeight`, `CanvasWidth`, `Content`, `Documentation`, `Excluded`, `ExportLevel`, `Name` — measured identical across all 22. Which placeholder is \"main\" is a *naming convention*: every Atlas layout names one `Main`, and a page binds to a placeholder by qualified name anyway, so the document has nothing to say. gen's whole placeholder family (`MainPlaceholder`, `AcceptButtonPlaceholder`, `UseMainPlaceholderForPopups`, …) is phantom here. `mx check` is not the control for this class — the key set is. Pinned by `TestLayoutToGen_WritesExactlyTheKeysStudioProWrites`"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-27-no-export-mapping-mxcli-writes-matches-studio-pro-original.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-27-no-export-mapping-mxcli-writes-matches-studio-pro-original.json new file mode 100644 index 0000000000..d110aa555f --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-27-no-export-mapping-mxcli-writes-matches-studio-pro-original.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-27", "symptom": "No export mapping mxcli writes matches its Studio Pro original, so every one stays in #260's silent-loss set even once its source kind is authorable", "cause": "The export writers hardcoded three properties the IMPORT twin already read off the element: `MinOccurs 0` (a schema root has **1**), `MaxLength 0`, and `IsKey` not written at all. `MaxLength` is the one that cannot be a constant — Studio Pro stores **0 for a string element and -1 for a numeric one**, mirroring the bound schema element exactly as `MaxOccurs` already did", "file": "`mdl/backend/modelsdk/mapping_write.go` (`exportValueElementToGen`, `exportObjectElementToGen`) + `mapping_read.go`, `sdk/mpr/writer_export_mapping.go`, `modelsdk/mpr/serialize_mappings.go`, `mdl/executor/cmd_export_mappings.go`, `model.ExportMappingElement`", "insight": "The export element model had **none** of these fields and the reader populated none of them, so this is four layers (model, reader, builder, three writers), not a writer patch — and a writer patch alone would have written zeros from an empty model. Clone them from the schema element where the builder already clones `MaxOccurs`. Check the import twin first when an export property looks wrong: this was a divergence between the two writers, not a considered decision, and the import side is the correct reference. Repro `mdl-examples/bug-tests/mapping-277-export-property-set.mdl`, tests `sdk/mpr/writer_export_mapping_properties_test.go`. Issue ako/mxcli#277", "refs": ["#260", "ako/mxcli#277"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-08-27-rebuilt-mapping-drops-messagedefinition2-so-document-written.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-27-rebuilt-mapping-drops-messagedefinition2-so-document-written.json new file mode 100644 index 0000000000..b694ba1274 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-08-27-rebuilt-mapping-drops-messagedefinition2-so-document-written.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-08-27", "symptom": "A rebuilt mapping drops `MessageDefinition2`, so a document written by Studio Pro 11.10+ never round-trips", "cause": "The key is version-introduced — `modelsdk/gen/mappings/version.go` records `messageDefinition2` as `Introduced: \"11.10.0\"` — and gen generates **no accessor** for it, so nothing read or wrote it. A blank 11.13 app's own mappings carry it as `\"\"`; none of the older pinned fixtures has it at all", "file": "`model.ImportMapping`/`ExportMapping` (`MessageDefinition2 *string`), `mdl/backend/modelsdk/mapping_read.go` (`messageDefinition2FromRaw`) + `mapping_write.go`, `sdk/mpr/parser_*_mapping.go` + `writer_*_mapping.go`, `mdl/executor/cmd_import_mappings.go` + `cmd_export_mappings.go`", "insight": "**Carry it, do not derive it.** A pointer, because nil (absent) is NOT the same as present-and-empty — writing the key onto a pre-11.10 document is the overlay-rule mistake CLAUDE.md warns about, which mxbuild tolerates and Studio Pro refuses to open. The executor decides: carry `existing.MessageDefinition2` on an update, apply the version gate only on a CREATE where there is nothing to read it off. A plain version gate in the writer looks equivalent and is not — it re-adds the key to every older document a rewrite touches, which turned four previously-clean fixtures red. gen having no accessor means the read goes to raw BSON, the route `parameterEntityFromRaw` takes. **`MappingSourceReference` is the same family and deliberately NOT carried**: its gate (10.16) predates every project in the field, and the codec emits it through a package-level `TypeDefaults` registration, so making it conditional would touch every mapping write to preserve one pre-10.16 fixture. Issue ako/mxcli#279", "refs": ["ako/mxcli#279"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-01-describe-navigation-prints-home-page-menu-but-silently-omits.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-01-describe-navigation-prints-home-page-menu-but-silently-omits.json new file mode 100644 index 0000000000..bd5c132f97 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-01-describe-navigation-prints-home-page-menu-but-silently-omits.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "cause": "The **reader**, not the writer: `mdl/backend/modelsdk/navigation_read.go` type-asserted only the `$Type`s `modelsdk/gen` declares for those two slots, and neither is what the documents carry \u2014 `LoginPageSettings` is stored as `Forms$FormSettings` with the page under `Form` (gen expects `Navigation$NavigationProfileLoginFormSettings` / `LoginPage`), and `NotFoundHomepage` as `Navigation$HomePage` (gen and `generated/metamodel` both expect `Navigation$NotFoundHomePage`). A failed type assertion leaves the field empty, so the loss is silent", "date": "2026-09-01", "file": "`mdl/backend/modelsdk/navigation_read.go` (`navLoginPageOf`, `navNotFoundPageOf`), cross-check `generated/metamodel/types.go` `NavigationNavigationProfile`", "insight": "**The other engine is the control.** Legacy read the same bytes correctly throughout, which is what identifies a reader bug: `describe navigation X` on both engines must agree, and a disagreement localises the defect to the one that reads through gen. Accept the `$Type` the documents actually carry and keep gen's as a fallback branch. Note the two slots fail in **opposite directions** and want opposite fixes: for the login page a real Studio Pro document and `generated/metamodel` agree with the writers, so **gen** is wrong; for the not-found page \u2014 Studio Pro's **\"Fallback page\"** \u2014 metamodel and gen agree with each other and the three mxcli **writers** are the odd one out, emitting `Navigation$HomePage` where Studio Pro stores `Navigation$NotFoundHomePage`. ako/TestApp supplied the reference document that settled it. **Correction (2026-09-01): mxbuild does NOT accept either.** Measured on 11.13 against a build emitting the old spelling, `mx check` and `mxbuild --target=deploy` both exit 1 with \"Object of type 'Mendix.Modeler.WebUI.Navigation.HomePage' cannot be converted to type '...NotFoundHomePage'\" -- the project will not LOAD, so every downstream check is lost. What actually let it through is that nothing ever BUILT a project with a fallback page set: the automated mx-check coverage is doctype-tests/ only and no script there sets one, so the first was added by the fix itself. **Generalisable: 'the build tolerates it' is a claim that needs the same control as the fix** -- revert the writer, rebuild, and run the tool, or the reason a bug escaped gets recorded backwards and sends the next reader looking in the wrong place. Keep reading both `$Type`s regardless, but for the repair path rather than round-tripping: a pre-fix project does not build, and mxcli reads BSON directly, so accepting the old spelling is what lets it open and fix one. Repro `mdl-examples/bug-tests/navigation-describe-profile-pages.mdl`", "symptom": "`DESCRIBE NAVIGATION` prints `home page` and the menu but **silently omits `login page` and `not found page`**, so pasting its output back (the documented copy workflow) deletes both from the profile. The clauses are on disk and `MXCLI_ENGINE=legacy` prints them"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-07-ce0066-entity-access-out-date-after-alter-entity-m-gen-add.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-07-ce0066-entity-access-out-date-after-alter-entity-m-gen-add.json new file mode 100644 index 0000000000..227e5923b5 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-07-ce0066-entity-access-out-date-after-alter-entity-m-gen-add.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-07", "symptom": "CE0066 \"Entity access is out of date\" after `ALTER ENTITY M.Gen ADD ATTRIBUTE ...` where a SPECIALISATION of Gen also has an access rule — and `UPDATE SECURITY`, project-wide or scoped, reported \"All entity access rules are up to date\" and changed nothing (mendixlabs/mxcli#1047, reported against 0.21.0 on MPR v2, reproduced on 11.12.1 on BOTH engines). The reporter's own tool named the one missing member — `+ Spec: ProbeSecond.Gen.AfterSpec (ReadWrite)` — and took mx check 1 -> 0.", "cause": "ReconcileMemberAccesses computes the same-module ancestor set (sameModuleAncestors -> ownerIDs) and then uses it ONLY for the association pass. The attribute pass walked the entity's OWN attributes (ent.AttributesItems() / entityDoc[\"Attributes\"]), so a specialisation's expected member set never contained an inherited attribute: nothing looked missing, nothing was added, and `modified` stayed 0 — which the command prints as \"up to date\". Fixed by walking the chain for attributes too, qualifying each against the entity that DECLARES it, with a nearer entity's attribute shadowing an ancestor's of the same name.", "file": "`mdl/backend/modelsdk/domainmodel_security_write.go` (ReconcileMemberAccesses, collectAttrs) and `sdk/mpr/writer_security.go` (entityAttrsInChain, ownAttrsOf, generalizationRefOf); tests `mdl/backend/modelsdk/reconcile_inherited_attr_test.go`, `sdk/mpr/writer_security_inherited_test.go`; example `mdl-examples/bug-tests/1047-inherited-member-reconcile.mdl`", "insight": "The bug was one line of REUSE that never happened: the ancestor set was already computed two lines above the attribute loop and only the association loop consumed it. When a function computes a set and uses it for one of two symmetric passes, check the other pass. Three things worth carrying: (1) A count that drives a message is part of the contract — reporting 0 modified is what turned a broken model into the words \"All entity access rules are up to date\", and a false success is worse than an error because it ends the investigation. Assert the count, not just the stored state. (2) Legacy keyed coverage by BARE attribute name while preserving any ref not qualified against this entity, so naively adding inherited attributes would have preserved AND re-added the same member — the duplicate only shows on the second reconcile, so the regression test runs it twice. (3) The repair path cannot be proven by an .mdl file once the write path is fixed: ALTER ENTITY reconciles as it writes, so the script ends clean either way. Break the project with a PRE-FIX binary and repair it with the fixed one — that finally showed `update security` taking a real CE0066 from 1 to 0, which an earlier fix had recorded as unproven.", "ce": ["CE0066"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-08-alter-page-p-set-pagesize-10-datagrid2-errors-pluggable.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-08-alter-page-p-set-pagesize-10-datagrid2-errors-pluggable.json new file mode 100644 index 0000000000..9cfe7a0b8e --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-08-alter-page-p-set-pagesize-10-datagrid2-errors-pluggable.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-08", "symptom": "`alter page P { set PageSize = 10 on }` errors `pluggable property \"PageSize\" not found` on a grid that `create page \u2026 (PageSize: 20)` had just written and the app really pages at. `mxcli check --references` passes the script, so it fails only at exec, after earlier statements have landed; DESCRIBE PAGE prints the same capitalised `PageSize:`, so describe \u2192 edit \u2192 exec produced a script mxcli refused to run", "cause": "A pluggable property key is lowerCamel in the widget template (`pageSize`). CREATE resolves the author's spelling case-INsensitively (widget engine `lookupProperty`, and `WidgetV3.GetStringProp` before it); ALTER went through `setPluggableWidgetPropertyMut`, which compared the template key byte-for-byte, so only the exact `pageSize` worked. Direct sequel to Findings #1 (2026-07-27), which fixed the same class for FIRST-CLASS props and deliberately left the pluggable fallback case-sensitive with the comment 'template keys must match the template exactly'", "file": "`mdl/backend/pagemutator/mutator.go` (`setPluggableWidgetPropertyMut`)", "insight": "`strings.EqualFold` against the widget's own PropertyTypes. **The disproven belief is the reusable part**: keys are STORED case-sensitively, which is not a reason to MATCH them that way \u2014 the resolver searches one object type's PropertyTypes, and across every shipped template and definition that scope holds no two keys differing only in case (96 scopes, 1208 keys, 0 collisions). Measure the ambiguity before assuming it; here there was none, and the assumption cost a whole verb. **Cheapest localiser**: run the failing statement with the template's own casing \u2014 `set pageSize` succeeded where `set PageSize` failed, in ONE measurement, on the same widget in the same project. Both engines share `pagemutator`, so an engine split says nothing here (verified: modelsdk and legacy both fixed by the one change). Tests `TestSetPluggableProperty_MatchesTemplateKeyRegardlessOfCase` (+ typo-still-errors control, + `TestPluggablePropertyKeysAreUniqueIgnoringCase` pinning the no-collision argument); repro `mdl-examples/bug-tests/alter-page-pluggable-property-casing.mdl`; verified 0 errors on `mx check` 11.13.0. Two reporter claims did NOT hold: `describe page` DOES emit PageSize, but only when it differs from the widget default 20 (deliberate, so describe round-trips) \u2014 at the default it is omitted, which reads as 'describe cannot show it'. Still open and separate: `check --references` does not resolve pluggable property names at all, so a genuine typo (`PagSize`) still checks clean and fails at exec", "refs": ["mendixlabs/mxcli#1069"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-08-offline-navigation-profile-authored-mxcli-builds-routes.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-08-offline-navigation-profile-authored-mxcli-builds-routes.json new file mode 100644 index 0000000000..294ba971f8 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-08-offline-navigation-profile-authored-mxcli-builds-routes.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-08", "symptom": "An offline navigation profile authored by mxcli builds, routes and installs as a PWA, and shows an empty app — every gate green", "cause": "MDL had no syntax for offline synchronization, so a created offline profile got an empty OfflineEntityConfigs list. A Mendix offline profile downloads nothing until each entity has a sync mode; `mx check` reports 0 errors either way because an empty list is valid", "file": "`mdl/grammar/MDLParser.g4` (navSyncDef), `mdl/backend/modelsdk/navigation_write.go`, `sdk/mpr/writer_navigation.go`", "insight": "Creating a document kind is not the same as being able to configure it, and the gap is invisible to every static check — the symptom is an empty screen at runtime. When adding a profile/document kind, ask what makes it DO anything, not just what makes it exist. The write is an overlay keyed by entity so CompatibilityMode (stored, unauthorable) survives; building the element from the spec alone would clear it silently, the access-rule defect again", "refs": ["ako/TestApp", "PROPOSAL_offline_sync_configuration.md"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-08-property-studio-pro-writes-offline-entity-config.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-08-property-studio-pro-writes-offline-entity-config.json new file mode 100644 index 0000000000..5b4a5e6b14 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-08-property-studio-pro-writes-offline-entity-config.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-08", "symptom": "A property Studio Pro writes on an offline entity config (CompatibilityMode) was read from the model and silently discarded, so any future write path would have dropped it with no error and no mx check failure", "cause": "types.NavOfflineEntity carried three of the four properties Studio Pro actually writes. TestFieldCountDrift, which exists to catch exactly this on hand-copied structs, did not list NavOfflineEntity or NavigationProfile — so adding the field left the guard passing vacuously", "file": "`mdl/types/navigation.go`, `mdl/backend/mpr/convert.go`, `mdl/backend/mpr/convert_roundtrip_test.go`", "insight": "A drift guard is only worth what its list covers, and a guard that passes on a struct it does not know about is worse than none — it reads as coverage. Check the guard names your type before trusting a green run. Also: measure which properties are actually WRITTEN before deciding what to carry — gen declared six here and the reference document had four, with DownloadMode and ShouldDownload occurring zero times, so the risk was inverted from the expected one (writing a property Studio Pro fills in on load, not dropping one)", "refs": ["ako/TestApp", "ako/mxcli#413"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-09-call-web-service-legacy-soap-default-engine-mxcli-exec-06b.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-09-call-web-service-legacy-soap-default-engine-mxcli-exec-06b.json new file mode 100644 index 0000000000..47c7cc0d72 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-09-call-web-service-legacy-soap-default-engine-mxcli-exec-06b.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-09", "symptom": "`call web service` (legacy SOAP) on the DEFAULT engine: `mxcli exec 06b-soap-examples.mdl` reported success on all three microflows and `mx check` (11.13.0) then failed the project with `[CE0008] \"No action defined.\" at Action activity 'Activity'` plus two `[CE0109] \"Undefined variable 'Root'.\"`. Only reachable by rerunning with MXCLI_ENGINE=legacy, which is why legacy was still the documented fallback", "cause": "The codec engine READ the action (`actionFromGen` \u2192 `*microflows.WebServiceCallAction`, with a raw fallback) but `microflowActionToGen` had no case for it, so it hit `default: return nil` and the enclosing ActionActivity serialized with no action at all. The #850 shape: a missing WRITE case is not an unsupported feature, it is a silent drop that exec reports as success. The CE0109s are knock-on \u2014 the dropped action never bound $Root", "file": "`mdl/backend/modelsdk/microflow_webservice_write.go` (new), `microflow_write.go` (switch case)", "insight": "**Mirror the legacy serializer, and prove it by diffing the two engines' documents \u2014 do not re-derive the shape from the metamodel.** There is no Studio Pro-authored SOAP document in this repo, so legacy's output is the only reference that exists and is also what users' projects already contain. Method: exec the same script on each engine, `mxcli bson dump` both, normalise the random $IDs, diff. Three discrepancies fell out that no amount of reading would have shown, all in how the codec emits NULLS and MARKERS: (1) a Part property with no child encodes to nil and the encoder then SKIPS the key (`if val != nil`), so an unset part is an ABSENT key, never a null one \u2014 carry the null as a primitive `bson.Null{}` value instead, which marshals in place; (2) `codec.TypeDefaults.NullFields` does emit the key but APPENDS it after every property, so it cannot reproduce alphabetical key order; (3) both NullFields and list markers are registered per `$Type` and several types are SHARED between writers \u2014 `Microflows$HttpConfiguration` needs HttpHeaderEntries marker 3 for SOAP and 2 for REST, and legacy writes CustomLocationTemplate as null for SOAP but omits it for REST, so a global registration would have silently changed the REST path. Write those explicitly per call site. (The package already carries one such collision: `Microflows$HttpHeaderEntry` is registered 2 in microflow_write.go and 3 in odata_write.go, decided by file order.) Verified: the two engines' CallWebServiceAction documents are now identical key-for-key and value-for-value; `mx check` goes 3 structural errors \u2192 0, leaving only the script's 4 deliberate CE1613 dangling refs, which is exactly legacy's output. Revert control: removing the switch case fails all six unit tests with the CE0008 message. The doctype engineScriptSkip for modelsdk/06b was removed and the script now runs on BOTH engines", "refs": []} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-09-control-should-have-proven-guard-load-bearing-passed-guard.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-09-control-should-have-proven-guard-load-bearing-passed-guard.json new file mode 100644 index 0000000000..3ddd0e381e --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-09-control-should-have-proven-guard-load-bearing-passed-guard.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-09", "symptom": "A control that should have proven a guard was load-bearing PASSED with the guard removed — the test could not distinguish a correct writer from one that reset the property on every write", "cause": "The fixture stored `false` for a boolean property, and false is also the zero value. A writer that ignored the spec entirely wrote false; the correct writer preserved false. Identical output, so the assertion held either way", "file": "`mdl/backend/modelsdk/navigation_throw_sync_test.go`", "insight": "For a BOOLEAN property, a preservation test must exercise BOTH stored values — the non-zero one is the only case that can fail. This is the second time in one feature: CompatibilityMode needed a synthetic `true` because all seven reference configs carry false. The generalisation: when every real document agrees on a value, the fixture drawn from real documents cannot test preservation, and a synthetic counter-case is not optional. The tell is a control that fails to fail — if stubbing the guard leaves the suite green, the test is measuring nothing, and that is worse than no test because it reads as coverage", "refs": ["ako/mxcli#413", "ThrowPartialSyncError"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-10-mxcli-diff-local-mpr-v2-project-fails-error-mprcontents.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-10-mxcli-diff-local-mpr-v2-project-fails-error-mprcontents.json new file mode 100644 index 0000000000..18817d8841 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-10-mxcli-diff-local-mpr-v2-project-fails-error-mprcontents.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-10", "symptom": "`mxcli diff-local` on an MPR v2 project fails with `Error: mprcontents directory not found` while mprcontents/ exists and is populated; `MXCLI_ENGINE=legacy` works", "cause": "The modelsdk engine (the default) never overrode `Backend.ContentsDir()`, so it fell through to the generated `unimplemented` stub and returned \"\". diff-local reads \"\" as 'not a v2 project'.", "file": "mdl/backend/modelsdk/backend.go", "insight": "gen_unimplemented.go's promise that an unoverridden method 'fails loudly rather than silently dropping data' is CONDITIONAL on the method having an error to fail through: the generated body is `errUnimplemented` only when a result is `error`, a panic when there are no results at all, and a silent `var r0 T; return r0` otherwise. ContentsDir is in the third bucket and its zero value is a MEANINGFUL in-band answer (\"\" == MPR v1), so the missing implementation was indistinguishable from a v1 project rather than looking like a bug. The detectable signature was the contradiction between two questions the same command asks: Version() (implemented) says 2, ContentsDir() says v1. Guard added in mdl/backend/modelsdk/unimplemented_silent_test.go \u2014 reflect over FullBackend for error-less methods, go/parser the package for methods actually declared on *Backend, since reflection cannot tell a promoted method from an override (Go synthesises a wrapper named (*Backend).X for both). It immediately found a second one, InvalidateCache (a latent panic, no caller today).", "refs": ["mendixlabs/mxcli#1080"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-10-soap-call-web-service-writes-document-mxbuild-accepts-studio.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-10-soap-call-web-service-writes-document-mxbuild-accepts-studio.json new file mode 100644 index 0000000000..834538721b --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-10-soap-call-web-service-writes-document-mxbuild-accepts-studio.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-10", "symptom": "SOAP `call web service` writes a document mxbuild accepts and Studio Pro would not have written. A SEND MAPPING is silently DROPPED by both engines \u2014 `send mapping Mod.Export` parses, `mxcli check` passes, `exec` reports success, and nothing in the stored action references the mapping. Operation ARGUMENTS are dropped the same way", "cause": "sdk/mpr.serializeWebServiceCallAction was written without a Studio Pro reference and hardcodes five things it cannot know, and the codec engine's new writer reproduced it deliberately for parity. Measured against three Studio Pro-authored calls in ako/TestApp (Mendix 11.14.0, Clients.GetOrders / GetCustomerOrders / SaveOrder): ServiceName is the WSDL SERVICE name (\"OrdersWS\") not the local part of the imported service's qualified name (\"OrderSoapClient\"); ImportMappingCall.ContentType is \"Xml\" for a SOAP import mapping, not \"Json\"; Range.SingleObject follows cardinality (false for a list) rather than being always true; VariableType is the real result type (DataTypes$ObjectType with an Entity, DataTypes$BooleanType) rather than always DataTypes$VoidType; and a send mapping is Microflows$MappingRequestHandling {ContentType, MappingId, MappingVariableName}. Arguments live in RequestBodyHandling.ParameterMappings as Microflows$WebServiceOperationSimpleParameterMapping entries keyed by an escaped ParameterPath (\"http%3A//www.example.com/:GetOrder|OrderId\")", "file": "`sdk/mpr/writer_microflow_actions.go` (serializeWebServiceCallAction), `mdl/backend/modelsdk/microflow_webservice_write.go`", "insight": "**A guessed type name in a comment becomes a permanent refusal.** Legacy refused send mappings citing `Mendix$AdvancedRequestHandling`, said it 'requires a Studio Pro-generated example to determine the correct type storage name', and that refusal then shipped for as long as nobody went looking. The real type is `Microflows$MappingRequestHandling` \u2014 which THIS CODEBASE ALREADY WRITES for REST result/request handling \u2014 and the guessed name occurs in none of the three reference documents. The lesson is not about SOAP: when a writer refuses because a storage name is unknown, check whether a sibling feature already writes it before treating the refusal as a standing constraint. **Second, and the reason this was found at all: 'no reference exists' is a claim about where you looked.** The parity work asserted that no Studio Pro-authored SOAP document existed to pin against and used that to justify mirroring legacy; one existed in a separate repo the whole time (ako/TestApp, which carries both a consumed client and a published service). Byte-parity with what ships is a legitimate goal for a change scoped to stopping a silent drop \u2014 it is NOT evidence the shape is right, and conflating the two is how six defects got a passing test. Where a reference project exists, name it in the code so the next reader does not repeat the search", "refs": []} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-11-alter-page-replace-insert-inside-data-view-bound-datasource.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-11-alter-page-replace-insert-inside-data-view-bound-datasource.json new file mode 100644 index 0000000000..683b9d1d54 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-11-alter-page-replace-insert-inside-data-view-bound-datasource.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-11", "symptom": "`ALTER PAGE REPLACE`/`INSERT` inside a data view bound `datasource: selection ` re-scopes the new widget's attribute binding to the OUTER data view's entity (**CE1613** \"The selected attribute 'Mod.Outer.Attr' no longer exists\"), and inside a Gallery/DataGrid 2 sourced by a **microflow/nanoflow** drops it entirely (**CE0402** \"No value specified\", `describe` shows `ContentParams: [{1} = ]`). `mxcli check --references` and `exec` both report success; `CREATE PAGE` binds the same widget in the same position correctly", "cause": "The mutator resolved a widget's scope in TWO walks that each knew a different subset of the ten Forms$*Source kinds. `Forms$ListenTargetSource` carries no EntityRef at all \u2014 only the listen target's NAME \u2014 so the entity walk saw no source on the selection data view and left the context at the enclosing one. The flow walk (`findNearestDataSourceDoc`) read only a widget's TOP-LEVEL `DataSource` key, so a pluggable list \u2014 whose source sits at `Object.Properties[datasource].Value.DataSource` \u2014 contributed nothing, and its `Objects[].Properties[].Value.Widgets` descent (the one the entity walk gained in #935) was missing too", "file": "`mdl/backend/pagemutator/mutator.go` (`resolveSourceScope`/`resolveSourceScopeVia`, `listenTargetDataSource`, `widgetOwnDataSourceDoc`, `pluggableDataSourceDoc`; `EnclosingEntity`/`EnclosingEntityForChildren`/`EnclosingDataSourceFlow` now share the one walk `findNearestDataSourceDoc`, and `findEnclosingEntityContext` + its two helpers are deleted)", "insight": "**Count the source kinds before fixing one.** `generated/metamodel/types.go`'s `DataSource is implemented by` list closes the set at ten, and they divide exactly three ways \u2014 seven carry an EntityRef, two are flows, one (ListenTarget) borrows the scope of the widget it names \u2014 so one resolver can be complete, where three successive per-kind patches (FINDINGS #55 association+flow, #935 pluggable, this one) each left a hole. **A nearer source that resolves to no entity must SHADOW the outer one**: inheriting is what wrote the wrong entity, and it also mis-scoped a flow-sourced list nested in an entity-bound data view \u2014 a case the report did not name and the old code got wrong. The listen target is found by a shape-independent search for \"a document with this Name that has a data source\", which is what makes it work when the target is a pluggable widget keeping its source three levels inside its Object; a visited-set guards a hand-written listen cycle. **Measurement trap: a CE1613 SUPPRESSES the CE0402s in the same `mx check` run** \u2014 the first reading said mxbuild tolerated the unbound parameter, and the CE0402s only appeared once the re-scoped binding was fixed, so count bindings in `describe`, not errors. The issue's own second repro (a Gallery over a DATABASE source) no longer reproduced \u2014 #935 had fixed it \u2014 and the live defect was its flow-sourced variant, so re-measure a report against main before trusting its class. Tests `mdl/backend/pagemutator/mutator_selection_source_test.go` (7, incl. dangling/cyclic listen targets and the shadowing control); repro `mdl-examples/bug-tests/1076-alter-page-selection-and-flow-scope.mdl` \u2014 2 \u00d7 CE1613 + 3 unbound before, 0 errors after, on mxbuild 11.10.0. Each half proven load-bearing by stubbing it alone and rebuilding the CLI", "refs": ["mendixlabs/mxcli#1076", "#55", "#935"], "ce": ["CE0402", "CE1613"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-11-describe-microflow-exec-documented-copy-operation-silently.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-11-describe-microflow-exec-documented-copy-operation-silently.json new file mode 100644 index 0000000000..f9748298af --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-11-describe-microflow-exec-documented-copy-operation-silently.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-11", "symptom": "`describe microflow` -> `exec` \u2014 the documented copy operation \u2014 silently turns OFF a microflow's \"apply entity access\". Measured across 342 microflows in 4 projects (11.14.0): every microflow storing `ApplyEntityAccess: true` came back `false` (16/342, all 4 distinct Administration documents). A blocking `show message` also becomes non-blocking (16 microflows). `mxcli check` and mxbuild are both silent \u2014 the model is valid either way", "cause": "TWO causes wearing one symptom. (1) `ApplyEntityAccess` is HARDCODED false in both writers (`mdl/backend/modelsdk/microflow_write.go` SetApplyEntityAccess(false), `sdk/mpr/writer_microflow.go` {Key:\"ApplyEntityAccess\", Value:false}) and `microflows.Microflow` has no field for it, so the read side drops it first. (2) `ShowMessageAction.Blocking` is carried correctly end to end on BOTH engines \u2014 the loss is in DESCRIBE, which has no `blocking` keyword to emit, so the re-parse sets false", "file": "`mdl/backend/modelsdk/microflow_write.go`, `sdk/mpr/writer_microflow.go`, `sdk/microflows/microflows.go` (Microflow struct), `mdl/executor/cmd_microflows_format_action.go` (show message)", "insight": "**A round-trip audit needs a preservation control, or it cannot tell a bug from its own blind spot.** Here it was `StableId`: 342/342 preserved, exactly as ADR-0008 claims \u2014 a method that normalised ids away too aggressively would have reported that as churn, and everything else with it. **Separate `hardcoded in the writer` from `unspellable in DESCRIBE`**: they look identical in a before/after diff and need completely different fixes (model plumbing vs grammar), and `Blocking` proves a property can be perfectly carried by both engines and still be lost by the text round trip. **The same property handled two ways in one codebase is the tell**: `microflows.Rule` carries ApplyEntityAccess correctly (`rule_write.go`, `parser_rule.go`) while `microflows.Microflow` does not \u2014 so this was never an unknown-property gap, just an unfinished one. **Check what an 'empty' value actually holds before calling it a loss**: `ConcurrenyErrorMessage` looked like 58 lost translations and every single one had `Text: \"\"`, i.e. an empty entry versus no entry. Two more traps worth knowing: `ActionActivity.Caption` changes ONLY where `AutoGenerateCaption` is true (a user-set caption survives), and ~90% of the 417-line diff on a real microflow is layout \u2014 bezier vectors, sizes, connection indices \u2014 which buries the two lines that matter", "refs": []} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-11-making-soap-calls-describe-structurally-instead-base64.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-11-making-soap-calls-describe-structurally-instead-base64.json new file mode 100644 index 0000000000..70efbc0171 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-11-making-soap-calls-describe-structurally-instead-base64.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-11", "symptom": "Making SOAP calls describe structurally instead of as base64 silently rewrote Studio Pro's own documents: a describe -> exec round trip over ako/TestApp flipped `Range.SingleObject` false -> true with no error at all, and turned Clients.SaveOrder's `DataTypes$BooleanType` result into VoidType, which mxbuild reported as `[CE0366]` + `[CE6011]`", "cause": "`webServiceActionRequiresRawBSON` admitted keys BY NAME. That was sound while the supported set was the only nine keys the writer emitted \u2014 but a real call carries FIFTEEN, and six of the new ones are boilerplate mxcli writes at ONE fixed value. Admitting `HttpConfiguration`, `RequestHeaderHandling`, `IsValidationRequired`, `ProxyConfiguration`, `RequestProxyType` and `NewResultHandling` by name meant any call configured beyond mxcli's defaults would be normalised on the next exec", "file": "`mdl/backend/modelsdk/microflow_read_actions.go`, `sdk/mpr/parser_microflow_actions.go` (webServiceActionRequiresRawBSON and its value predicates)", "insight": "**The question a raw-fallback gate answers is not 'do I know this key' but 'would writing this back produce the same document'.** The two coincide only while the writer emits exactly the supported set; the moment a feature lands that widens what is representable, the by-name test starts approving documents it cannot reproduce \u2014 and the loss is invisible, because the result is a VALID model that differs from the user's. **The round trip is the only thing that catches it**: unit tests on the new feature all passed, `mx check` on the newly-written calls was 0 errors, and the regression only appeared when describe -> exec was run over the REFERENCE documents and the BSON diffed (`mxcli bson dump --type microflow --object`, ids normalised away). Two of the five diffs that surfaced were pre-existing microflow describe drift (NoCase case values, bezier control vectors) and unrelated \u2014 worth separating before blaming the change. Fixing it also SHRANK the feature's reach honestly: Studio Pro's own SOAP calls keep the raw form until `Range.SingleObject` is explained, and only mxcli-authored calls describe structurally. **A result type can come from somewhere MDL cannot see** \u2014 SaveOrder's Boolean is the WSDL operation's return type, not an import mapping's entity \u2014 so 'binds a result' does not imply 'derivable'", "refs": []} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-12-four-plain-widget-keywords-were-refused-default-engine.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-12-four-plain-widget-keywords-were-refused-default-engine.json new file mode 100644 index 0000000000..f621db0706 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-12-four-plain-widget-keywords-were-refused-default-engine.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-12", "symptom": "Four plain widget keywords were refused by the DEFAULT engine with \"widget *pages.X not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy\": statictext, dropdown, staticimage, dynamicimage (plus NanoflowSource as a list data source). Nothing in mdl-examples/ or in any skill used those keywords, so the doctype gate ran green the whole time. Then, closing the gap: `statictext` on EITHER engine produced a project that could not be loaded at all — `mx check` aborts with TypeCacheUnknownTypeException for Forms$Text before running any validation, and Studio Pro fails the same way", "cause": "Two unrelated things. (1) The modelsdk widget dispatch simply had no case arms for those five types — reachable set measured at five, not the twenty-three a name scan suggests: nineteen pages.* widget structs are constructed by nothing, and EntityPathSource / ShowHomePageClientAction are written by NEITHER engine, so their fallback named a path legacy could not take either. (2) `Forms$Text` is not a Mendix type. It is absent from modelsdk/gen, from generated/metamodel, and from all 3,257 Studio Pro Texts$Text holders in ako/TestApp; `buildTextWidgetV3` minted it for both `text` and `statictext`. Writing it is worse than a build error — the project is unopenable and unfixable in the modeler", "file": "`mdl/backend/modelsdk/widget_write_legacy_gaps.go` (new), `mdl/backend/modelsdk/widget_write.go`, `sdk/mpr/writer_widgets_display.go`, `mdl/executor/validate_widget_retired.go` (new), `mdl/executor/cmd_pages_builder_v3_widgets.go`", "insight": "**\"Parity with the other engine\" is the wrong bar until you have checked whether the other engine is right.** That was the starting assumption here and measuring overturned it: ako/TestApp carries three Studio-Pro-authored Forms$StaticImageViewer widgets (FeedbackModule) and legacy disagreed with all three — it omitted AlternativeText (non-omitempty in generated/metamodel) and wrote BSON null for the unset Image. Legacy's dynamic image was worse: a hand-rolled AlternativeText carrying a FallbackValue key that Forms$ClientTemplate does not have, four lines after a comment in the SHARED serializer saying exactly that (\"Must be Fallback object, not FallbackValue string\"). sdk/mpr was corrected to match rather than pinned as truth. **Look for a Studio Pro reference inside the fixture before concluding there is none** — `bson.Unmarshal` every mprcontents unit and count instances of the $Type; the marketplace modules a blank app ships are Studio Pro output. The same 40-line scanner settles value-shape questions no doc answers: measured 0 nulls against 4,400+ empty strings for by-name references, so an unset QualifiedName is \"\", never null; and marker 3 in 3,257 of 3,257 Texts$Text lists, so legacy's marker 2 for a non-empty one is the wrong side of that divergence, not modelsdk's 3. **A widget keyword the example corpus never uses is a keyword nothing tests**, whatever the coverage numbers say. **Check that the project still LOADS, not just that it builds** — a load failure aborts `mx check` before the error list, so grepping for CE-codes finds nothing and reads like success", "refs": []} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-12-modelsdk-default-engine-leaves-19-fullbackend-s-276-methods.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-12-modelsdk-default-engine-leaves-19-fullbackend-s-276-methods.json new file mode 100644 index 0000000000..b0570d375d --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-12-modelsdk-default-engine-leaves-19-fullbackend-s-276-methods.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-12", "symptom": "The modelsdk (default) engine leaves 19 of FullBackend's 276 methods to the errUnimplemented stub, which tells the user to \"rerun with MXCLI_ENGINE=legacy\" — the engine being retired. Nineteen unported methods reads as nineteen reasons legacy has to stay shipped and tested", "cause": "Seventeen of the nineteen cannot be reached at all: they are interface surface that only the MPR backend's own delegation and callers holding a concrete *mpr.Reader / *mpr.Writer (api/, examples/, cmd/mxcli commands that open a reader directly) ever touch, so the stub can never fire. The two that ARE reachable — GetRawUnitByName and ParseMicroflowBSON, four call sites in mdl/executor/cmd_microflows_builder.go — were invisible because each is a fast path with a working slow-path fallback: the stub errored, the code fell through to an O(n) module walk, and the result was identical", "file": "`mdl/backend/modelsdk/raw_lookup.go` (new), `mdl/backend/modelsdk/unimplemented_reachability_test.go` (new), `scripts/backend-reachability.sh` (new)", "insight": "**Grep cannot answer \"is this interface method reachable\" and the compiler can.** `b.reader.GetRawUnitByName(...)` inside the MPR backend and `ctx.Backend.GetRawUnitByName(...)` in the executor are indistinguishable to a regex, and receiver names vary, so a grep-based count said all nineteen had callers. Deleting one method from its interface at a time and rebuilding gives a yes/no per method with no judgement involved: 17 DEAD, 2 LIVE. Slow (one `go build ./...` each, minutes for the set) but decisive — script it, commit it, and pin its OUTPUT in a test rather than re-running it in CI. **A fallback hides an unimplemented method completely.** Nothing was broken and no test failed, because the fast path's failure was indistinguishable from a cache miss; the only symptom was work being done twice. Look for `if x, err := …; err == nil` fast paths when auditing what an engine cannot do. **Measure the speedup before claiming one**: restoring the fast path made no measurable difference (620ms vs 608ms over three runs on a 138-microflow project, within noise), because `mxcli exec` runs `check` first and check's own helpers load ListMicroflows anyway — so the cache the slow path builds is already warm. The value here is the removal of the errUnimplemented cliff, not speed", "refs": []} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-addattribute-updateattribute-were-unimplemented-default.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-addattribute-updateattribute-were-unimplemented-default.json new file mode 100644 index 0000000000..7671fa1d9e --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-addattribute-updateattribute-were-unimplemented-default.json @@ -0,0 +1 @@ +{"area":"mdl/backend","date":"2026-09-15","symptom":"AddAttribute and UpdateAttribute were unimplemented on the default engine, so api/ could only work on the retired legacy one — and api/'s whole integration suite reported green while verifying nothing","cause":"api/ imported mdl/backend zero times: it held a concrete *mpr.Writer and bypassed the backend abstraction, so the two methods had no caller through a backend value and were never ported.","file":"api/api.go","fix":"api.New takes a backend.FullBackend (plus an api.Open convenience that owns its connection); the two methods are implemented on the codec backend and struck off unreachableUnimplemented.","insight":"unreachableUnimplemented is a CENSUS OF WHO BYPASSES THE ABSTRACTION, not dead interface surface. Read its reason column as a map and it names api/, the MCP backend, and the cmd/mxcli commands holding a concrete reader; a method is on it BECAUSE such a caller exists and unreachable BECAUSE that caller does not use a backend value. So the list shrinks by closing a bypass, never by deleting methods — and the pin fails loudly when you do, which is how it should be used. Two traps in the port itself. (1) The generated gen list offers only Append and Remove, so replacing an element in place means rebuilding the list; the naive remove-then-add moves the edited attribute to the bottom of the entity, which is a diff on every edit — control: break the rebuild and the order test catches it. (2) THE SUITE THAT WOULD HAVE VERIFIED THE PORT HAD ONLY EVER SKIPPED. api/'s integration tests pointed at ../mx-test-projects/test-source-app, which is not in the repository, so all ten skipped on every machine and in CI since they were written (the #808 shape). `go test ./api/` passing said nothing. Repointed at the committed testdata/expr-checker fixture the codec backend's own tests use, and made a missing fixture FATAL rather than a skip, since a committed fixture's absence is a broken checkout. Before trusting a suite to verify a refactor, check it is not skipping."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-backend-createentity-createassociation-left-caller-s.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-backend-createentity-createassociation-left-caller-s.json new file mode 100644 index 0000000000..bdd0ac4b77 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-backend-createentity-createassociation-left-caller-s.json @@ -0,0 +1 @@ +{"area":"mdl/backend","date":"2026-09-15","symptom":"Backend.CreateEntity and CreateAssociation left the caller's semantic element holding an EMPTY ID, so the very next call failed with \"entity not found: \" (note the blank). The legacy writer had populated them, so this was a silent behaviour difference across the engine swap that no test caught.","cause":"assignEntityIDs/assignAssociationIDs mint identities on the gen element built by entityToGen/assocToGen, and nothing copied them back to the *domainmodel.Entity the caller passed in. assignID only fills an EMPTY id, so api/ was unaffected (its builders pre-assign one) — which is exactly why no existing test saw it.","file":"mdl/backend/modelsdk/domainmodel_write.go","fix":"copyAssignedIDs writes the minted entity and attribute ids back onto the caller's element, matching attributes BY NAME rather than by position; CreateAssociation does the same for its own id. Both only fill an empty id, so a caller-assigned one is preserved.","insight":"Found by RUNNING examples/modify_project after repointing it, not by any test — the unit suite, check-mdl and the whole integration gate were green. That is the argument for keeping example programs runnable and actually running them: an example is the only caller that uses the API the way an outside user would, with no pre-assigned ids and no knowledge of internals. Two specifics worth carrying. (1) The empty-string ID makes the error message read \"entity not found: \" with nothing after the colon — a trailing-blank in an error is a strong tell that an identity was never populated rather than looked up and missed. (2) When copying minted ids back, match sub-elements BY NAME, never by index: entityToGen can add, skip or reorder (an audit pseudo-type becomes a System-module generalization, not an attribute), so an index-based copy hands the caller ANOTHER attribute's identity — which looks like it worked and is worse than the empty id it replaced. The control matters here too: a test that only asserts 'id is non-empty' passes against an implementation that overwrites a caller-assigned id, which would break api/; assert the reported id equals the STORED one, and add a companion test that a pre-assigned id survives."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-create-modify-workflow-over-mcp-studio-pro-11-14-stored-flow.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-create-modify-workflow-over-mcp-studio-pro-11-14-stored-flow.json new file mode 100644 index 0000000000..d4ecad590d --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-create-modify-workflow-over-mcp-studio-pro-11-14-stored-flow.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-15", "symptom": "`create or modify workflow` over `--mcp` (Studio Pro 11.14) stored the flow's activities reversed, with an old activity left in and a new one missing ([Start, a, b, End] rewritten as A, B, C → Start, B, A, a, End; a larger flow also lost its parallel split and held a name twice); `alter workflow … replace activity X` left X in place. The update calls all reported SUCCESS", "cause": "One `ped_update_document` batch is not applied in the order sent. Every measured batch fits: ops run highest index first, and at one index the adds go in as a block in op order before the removes — so a remove at an index where something was just added removes the added element. `UpdateWorkflow` sent the flow's removes plus its middles in reverse at index 1 in one batch (and index-less adds for event sub-processes/handlers, which come out reversed); `ReplaceActivity` sent remove @k plus adds at k, k+1; `InsertAfterActivity` sent incrementing indices", "file": "`mdl/backend/mcp/workflow.go` (`UpdateWorkflow`, `InsertAfterActivity`, `ReplaceActivity`, `addAtOp`); simulator `pedListSim` in `mdl/backend/mcp/workflow_listops_test.go`", "insight": "**The code comment claimed the reverse-at-index-1 trick worked, and no fake PED modelled batch semantics, so the unit tests asserted the ops sent rather than the list stored.** Fix tests by simulating the server's list semantics and asserting the resulting ORDER, and keep a table test that replays each raw-PED measurement through the simulator — that is what makes the simulator trustworthy and each control meaningful. The first guess at the rule (\"adds first, then removes\") fit two measurements and failed the third; fit the model to every data point before building on it. Also: a live update-path probe needs a workflow the executor can see — either on disk, or created earlier in the same exec (the backend's session list)", "fix": "Never add to and remove from the same list in one batch: add the statement's elements at a single index in their own order (flow middles @1, event sub-processes and handlers @0, replacement activities @k+1), then remove the stored/replaced ones in a second update. Adding first leaves duplicates, not a gutted workflow, if the second update fails"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-default-engine-describe-workflow-printed-wait-notification-x.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-default-engine-describe-workflow-printed-wait-notification-x.json new file mode 100644 index 0000000000..9de9f8cc4f --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-default-engine-describe-workflow-printed-wait-notification-x.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-15", "symptom": "On the default engine, `describe workflow` printed `wait for notification x;` without its boundary events (and their handler flows); the legacy engine described them. A describe -> exec round trip lost them.", "cause": "workflowActivityFromGen's typed switch had no case for *genWf.WaitForNotificationActivity, so it fell to workflowSimpleActivityFromGen, which reads only Name and Caption from raw BSON. That path's comment said the wait activities 'have no genWf struct' — true once, stale by the time gen gained WaitForNotificationActivity with BoundaryEventsItems().", "fix": "Typed case reading boundaryEventsFromGen(a.BoundaryEventsItems()), like the five other activities that carry boundary events.", "file": "mdl/backend/modelsdk/workflow_read.go", "insight": "A fallback path documented as 'for types gen does not model' silently keeps catching a type after gen starts modelling it; the reader then under-reads without any error. When gen is re-vendored, grep the typed switches for types that newly have structs. The bug surfaced only because a probe described what it had just written — reading back your own write, per activity type, is the cheap check."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-deleting-legacy-sdk-mpr-backend-mdl-backend-mpr-turned-up.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-deleting-legacy-sdk-mpr-backend-mdl-backend-mpr-turned-up.json new file mode 100644 index 0000000000..cb4409e202 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-deleting-legacy-sdk-mpr-backend-mdl-backend-mpr-turned-up.json @@ -0,0 +1 @@ +{"area":"mdl/backend","date":"2026-09-15","symptom":"Deleting the legacy sdk/mpr backend (mdl/backend/mpr) turned up three things the removal plan had not sized: a runtime error telling users to rerun on the engine that had just been deleted, an integration suite that had been exercising the retired engine rather than the default one, and a cross-engine parity test whose comparison became vacuous.","cause":"Each is a reference to the second engine that was invisible while two engines existed. errUnimplemented's message named MXCLI_ENGINE=legacy as the fallback; setupTestEnv in mdl/executor/roundtrip_helpers_test.go hardcoded mprbackend.New() as its default, so most of the package's integration tests ran on legacy; TestODataService_EngineWriteParity compared writer A's key set against writer B's.","file":"mdl/backend/modelsdk/backend.go","fix":"errUnimplemented now asks for a bug report instead of naming a fallback. setupTestEnv defaults to the codec backend. The parity test keeps its value assertion (a published OData service retains AllowedModuleRoles) and drops the comparison, renamed to match. --engine/MXCLI_ENGINE kept as a warning-only no-op; `bson compare` and mdl/enginecompare deleted.","insight":"Deleting one of two implementations is not finished when the code compiles — grep for the deleted thing in three places the compiler cannot reach. (1) RUNTIME STRINGS: an error message naming a removed fallback is strictly worse than no fallback, because the user follows it and gets a second failure; this one had been shipping the instruction 'rerun with MXCLI_ENGINE=legacy' from 19 generated stubs. (2) TEST DEFAULTS: a shared setup helper's hardcoded choice of implementation decides what a whole package actually covers, and here it silently pointed most of mdl/executor's integration tests at the engine being retired — deleting the other one moved them onto the default engine for the first time, which is coverage gained, not lost. (3) DIFFERENTIAL TESTS: a test that compares A to B has no meaning with one implementation, but the PROPERTY it was a means to usually does — for the OData one, that a service keeps its role grants, which mx check reports as 0 errors either way. Drop the comparison, keep the property, rename the file so the next reader is not misled. One counter-case worth noting: the doctype gate's engine MATRIX was kept at one entry rather than collapsed, because its selection function still converts a stale MXCLI_TEST_ENGINES=legacy into a loud failure instead of a gate that selects zero engines and reports success."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-describe-microflow-prints-notify-action-notify-workflow.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-describe-microflow-prints-notify-action-notify-workflow.json new file mode 100644 index 0000000000..a4dfffb6dc --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-describe-microflow-prints-notify-action-notify-workflow.json @@ -0,0 +1 @@ +{"area":"mdl/backend/modelsdk","date":"2026-09-15","symptom":"`describe microflow` prints a notify action as `notify workflow $Workflow;` with no `$X =` output variable, although the stored action has one; a rewrite from that output drops it","cause":"`modelsdk/gen` binds NotifyWorkflowAction.outputVariableName to the BSON key \"VariableName\"; Studio Pro 11.14 and the 11.6 metamodel store \"OutputVariableName\". The reader used gen's accessor and got nothing; the hand-built writer wrote the right key, so only reads were wrong","file":"`modelsdk/gen/microflows/types.go` (`initNotifyWorkflowAction`, STORAGE-NAME OVERRIDE), `mdl/backend/modelsdk/microflow_read_actions.go`","insight":"A property whose writer is hand-built and whose reader goes through gen can be wrong in one direction only, which a write-then-read test on the codec cannot catch — both sides agree with themselves. Read a Studio Pro-saved document instead (ako/TestApp's ZzMxcliExample_Notify is the fixture). 16 gen types bind `VariableName`, and most are right (the metamodel stores `variableName` for aggregate, cast and create actions), so fix by type against the metamodel, never by search-and-replace. Settled on mxbuild 11.6/11.10/11.13: under `VariableName` the variable is undefined (CE0109)."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-every-alter-workflow-op-addressing-activity-e-g-insert.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-every-alter-workflow-op-addressing-activity-e-g-insert.json new file mode 100644 index 0000000000..d7a200306c --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-every-alter-workflow-op-addressing-activity-e-g-insert.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-15", "symptom": "Every ALTER WORKFLOW op addressing an activity (e.g. `insert boundary event on bugSplitJump …`) failed with `ambiguous activity \"bugSplitJump\" (2 matches); use @N to disambiguate` whenever the workflow also contained `jump to bugSplitJump`. Measured live over MCP against Studio Pro 11.14; `bugSplitJump@1` worked.", "cause": "buildJumpTo (mdl/executor/cmd_workflows_write.go) names every jump `JumpTo` and, with no MDL caption, sets Caption = the target's name. Both activity resolvers — mcpWorkflowMutator.searchActivities and wfmutator's findActivitiesRecursive/findActivityIndexRecursive — matched `name == ref || caption == ref` into ONE pool, so the jump's caption shadowed its target's name. Same defect in the MPR mutator, it was only reported over MCP.", "file": "mdl/backend/mcp/workflow.go", "fix": "Resolve in two tiers in both backends: without @N, activities whose name equals the reference win when any exist, else caption matches are used; with @N, every name-or-caption match still counts in DESCRIBE order. A first attempt also scoped @N to the chosen tier and CI caught it: mdl-examples/doctype-tests/24-workflow-examples.mdl addresses a second call activity as `ACT_Process@2` (found 1 matches) — @N is an established positional index over the pooled matches, so only the unqualified case may change. The jump's default caption was left alone — existing projects already carry that shape, and changing it would rewrite every mxcli-authored jump on the next re-run.", "insight": "A reference that can match either an identity (name, deduplicated) or a label (caption, free text) must rank them, never pool them — any label that happens to repeat an identity turns a unique name ambiguous, and mxcli's own defaults manufacture exactly that collision. The tell is `N matches` where N counts a jump/marker you did not think of as a candidate. When fixing a resolver, grep for its twin in the other backend (wfmutator vs mcp): the two were written to agree on @N numbering and had the same bug. Controls: both new tests fail with the reported message against the pooled resolver."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-five-testconnect-tests-mdl-backend-mcp-connect-reads-test-go.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-five-testconnect-tests-mdl-backend-mcp-connect-reads-test-go.json new file mode 100644 index 0000000000..d155de085d --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-five-testconnect-tests-mdl-backend-mcp-connect-reads-test-go.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-15", "symptom": "Five `TestConnect_*` tests in `mdl/backend/mcp/connect_reads_test.go` (added by ako/mxcli#468) fail in a devcontainer — on pristine origin/main — with `Connect: connect to MCP server \"http://127.0.0.1:NNNNN/mcp\": ... dial tcp 192.168.65.254:NNNNN: connect: connection refused`, while CI is green", "cause": "The tests built the backend with `New(ped.srv.URL+\"/mcp\", \"\")`. An empty dial address falls through to `defaultDial` → `dialFor` in `mdl/backend/mcp/client.go`, which rewrites a localhost endpoint to `host.docker.internal:` whenever that name resolves — the intended convenience for reaching a Studio Pro on the Docker host. Inside a devcontainer it resolves, so the request goes to the host gateway and never reaches the httptest listener on the container's own 127.0.0.1. On a CI runner the name does not resolve and the rewrite is a no-op, which is why the suite was green there", "file": "`mdl/backend/mcp/connect_reads_test.go` (`backendFor`, used by `connected`, `TestConnect_GetWorkflowReadsASeededWorkflow`, `TestConnect_DisconnectIsIdempotent`); precedent `fakePED.connectClient` in `client_test.go`", "insight": "**A fake server must be dialled at the address it is listening on, never through production address resolution.** Any helper that takes an optional dial/host override and defaults to environment-sensitive logic (DNS lookups, proxy env, gateway rewrites) makes a test pass or fail by where it runs, not by what it tests — and CI is precisely the environment where such a rewrite is inert, so it cannot catch it. Pass the listener address explicitly (as `connectClient` already did) rather than changing `defaultDial`, which is correct for real connections. The tell in the error is the dialled IP differing from the URL's host. Control: revert the dial to `\"\"` in the devcontainer and the same five fail with the reported message; `TestConnect_TheSameWriteSucceedsReadWrite` passes either way because it never builds an MCP backend, which is what localises the defect to the constructor call", "refs": ["ako/mxcli#468"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-mcp-backend-held-concrete-mpr-reader-local-reads-keeping.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-mcp-backend-held-concrete-mpr-reader-local-reads-keeping.json new file mode 100644 index 0000000000..e49be7e8ba --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-mcp-backend-held-concrete-mpr-reader-local-reads-keeping.json @@ -0,0 +1 @@ +{"area":"mdl/backend","date":"2026-09-15","symptom":"The MCP backend held a concrete *mpr.Reader for its local reads, keeping three methods (GetDomainModelByID, GetWorkflow, ListNavigationDocuments) on FullBackend with no caller through a backend value — and 190 tests in the package, not one of which called Connect, so swapping the reader underneath it would have landed unverified with the suite green.","cause":"Those three reads had no codec-backend implementation, so MCP could not compose a backend and kept a legacy reader instead. Each was a re-keying or widening of a read the package already did, not new decoding.","file":"mdl/backend/mcp/backend.go","fix":"Implemented the three on the codec backend (mdl/backend/modelsdk/mcp_bypass_reads.go), sharing domainModelFromGen / workflowFromGen / navProfileFromGen with their siblings; added Backend.ConnectReadOnly and pointed MCP's reader field at backend.FullBackend. Wrote mdl/backend/mcp/connect_reads_test.go — the first test to call Connect at all.","insight":"A test can be vacuous in two different ways in one file, and the second is the one you miss. The read-only assertion SKIPPED (MyFirstModule has no entities), which the runner prints — so it was obvious. The GetWorkflow assertion looped over the listing and the fixture has ZERO workflows, so the loop body never ran and the test reported PASS having called nothing. Same failure class as #808, but silent. The fixes differ by shape: for the skip, pick a probe needing nothing pre-existing (CreateEntity, not AddAttribute); for the empty loop, SEED the thing through a read-write backend, then reconnect read-only and assert a specific named item is in the listing BEFORE fetching it — that guard is what converts a future vacuous pass into a failure. Cheapest way to find both: print the counts a test loops over before trusting it. MCP's read-only constraint also has a real control: revert ConnectReadOnly to Connect and the write-refusal test fails with the reported symptom, and the companion TestConnect_TheSameWriteSucceedsReadWrite proves the probe write is not failing for an unrelated reason."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-moving-cmd-mxcli-project-tree-go-from-concrete-sdk-mpr.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-moving-cmd-mxcli-project-tree-go-from-concrete-sdk-mpr.json new file mode 100644 index 0000000000..c6d62eecca --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-moving-cmd-mxcli-project-tree-go-from-concrete-sdk-mpr.json @@ -0,0 +1 @@ +{"area":"mdl/backend","date":"2026-09-15","symptom":"Moving cmd/mxcli/project_tree.go from a concrete sdk/mpr reader to the codec backend silently dropped System.VerifyPassword from `mxcli project-tree` output. Build clean, all tests green, 131 bytes missing from a 78KB JSON tree.","cause":"The System module's Java actions are platform built-ins with NO stored unit in the .mpr. sdk/mpr's ListJavaActions/ListJavaActionsFull appended them via BuildSystemJavaActions(); the codec backend only decoded stored units, so it reported them as absent.","file":"mdl/backend/modelsdk/java.go","fix":"Moved the System Java action definitions from sdk/mpr to modelsdk/meta (which already owns the virtual System module's entities and associations) and appended them in both codec ListJavaActions and ListJavaActionsFull; sdk/mpr now delegates rather than holding a second copy. Regression tests plus a control that the STORED actions are still returned.","insight":"Found by diffing the command's output against a binary built from the pre-port commit — not by any test, and no test would have caught it. That baseline-diff is the technique worth keeping for any reader swap: build the old binary first (git stash -u; make build; cp bin/mxcli /tmp/before), then require byte-identical output on every command whose reader you touched. A 131-byte difference in 78KB of JSON is invisible to eyeballing and to 'it still runs'. Two structural lessons. (1) A SYNTHESIZED element is the thing a reader swap loses, because it exists in one reader's code rather than in the data — grep the old reader for 'not stored in' / 'virtual' / 'Build*' helpers before trusting a port. (2) The census in unimplemented_reachability_test.go CANNOT catch this class of bypass: it only lists methods with no implementation, so a caller holding a concrete reader while calling only IMPLEMENTED methods is invisible to it. project_tree.go called 36 semantic methods, every one on FullBackend, and never appeared — which is also why the Phase 3 write-up wrongly described all remaining bypasses as raw-unit debugging tools. The complete list of bypasses is the sdk/mpr IMPORTER list, not the census."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-over-mcp-against-studio-pro-11-14-create-workflow-any-multi.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-over-mcp-against-studio-pro-11-14-create-workflow-any-multi.json new file mode 100644 index 0000000000..7b3bf22df9 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-over-mcp-against-studio-pro-11-14-create-workflow-any-multi.json @@ -0,0 +1 @@ +{"area": "mdl/backend/mcp", "date": "2026-09-15", "symptom": "Over MCP against Studio Pro 11.14, `create workflow` with any `multi user task` fails: `ped_create_document …: {\"/flow/activities/1/taskPage\":\"Expected an object, but the value is missing.\"}`", "cause": "The 11.14 Workflows$MultiUserTaskActivity CONSTRUCTOR takes `taskPage: Element<'Workflows$PageReference'>`; mapWorkflowActivity sent the older bare `pageReference` string. Same class as the workflow constructor's `context` change in the same release", "file": "`mdl/backend/mcp/workflow.go` (`multiUserTaskConstructorTakesTaskPage`, `adaptMultiUserTaskPages`)", "insight": "PED constructor shapes drift per Studio Pro release independently of the element (update) shapes, and serverInfo.version is frozen at 1.0.0, so probe the constructor schema text rather than gating on a version. When one constructor of a document family changed shape in a release, check the others in that family before trusting the mapper — this one surfaced only because a phase-3 live probe used a multi-user task"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-over-mcp-against-studio-pro-11-14-every-create-microflow.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-over-mcp-against-studio-pro-11-14-every-create-microflow.json new file mode 100644 index 0000000000..d69c374b59 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-over-mcp-against-studio-pro-11-14-every-create-microflow.json @@ -0,0 +1 @@ +{"area": "mdl/backend/mcp", "date": "2026-09-15", "symptom": "Over MCP against Studio Pro 11.14, every `create microflow` fails at ped_create_document: `{\"/flows/0/$Type\":\"Expected an element with $Type property.\",\"/returnType\":\"Expected one of [Void, Boolean, …], got {\\\"type\\\":\\\"Void\\\"}\"}`", "cause": "The 11.14 Microflows$Microflow CONSTRUCTOR became a canvas skeleton: flows are `$Type`d elements, returnType is a bare enum with returnTypeEntity/returnTypeEnumeration beside it, parameters go in a `parameters` list, and every object constructor declares only x/y (plus caption/loopType). Fixing just the two rejected keys is not enough: a create that still sends relativeMiddlePoint, action, returnValue or a split/loop source is ACCEPTED and those are silently dropped (read back: positions 0,0, `action: null` → 'No action defined', empty return value)", "file": "`mdl/backend/mcp/microflow.go` (`microflowConstructorTakesSkeleton`, `adaptMicroflowSkeleton`, `CreateMicroflow`)", "insight": "A validation error lists only what the validator rejects, not what it discards — after making a PED create pass, READ THE DOCUMENT BACK before calling it fixed. The skeleton create plus one ped_update_document that `set`s /objectCollection/objects/N/{action,returnValue,splitCondition,loopSource} (N counts the parameters, which the constructor's $id(/objects/N) does not) round-trips cleanly. Reads expand one level, so a nested value (messageTemplate text, variableType entity, case value) looks empty until its own path is read — don't mistake the stub for loss"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-over-mcp-mcp-against-studio-pro-11-14-create-workflow-fails.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-over-mcp-mcp-against-studio-pro-11-14-create-workflow-fails.json new file mode 100644 index 0000000000..ee17b6c3c7 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-over-mcp-mcp-against-studio-pro-11-14-create-workflow-fails.json @@ -0,0 +1 @@ +{"area": "mdl/backend/mcp", "date": "2026-09-15", "symptom": "Over MCP (--mcp) against Studio Pro 11.14, `create workflow` fails with `ped_create_document …: Validation errors … {\"/context\":\"Expected reference (string), got undefined\",\"/workflowName\":\"Expected string, got object\"}`; and on any version a workflow's on-created microflows and event handlers were silently absent from the created document", "cause": "(1) Studio Pro 11.14's Workflows$Workflow CONSTRUCTOR takes `context` (entity qualified name) and plain-string `workflowName`/`caption`; the mapper sent the older `parameter` element and a StringTemplate. The element (update) shape is unchanged. (2) mapWorkflowActivity hard-coded `onCreatedEvent: {$Type: Workflows$NoEvent}` and mapWorkflow never sent `onWorkflowEvent`, so constructs added to the semantic model later were dropped", "file": "`mdl/backend/mcp/workflow.go` (`workflowConstructorTakesContext`, `mapWorkflowContent`, `mapOnCreatedEvent`, `mapWorkflowEventHandlers`)", "insight": "`ped_get_schema` is a live oracle — ask it rather than guessing a version: the constructor schema text says `context: Reference<'DomainModels$Entity'` on servers that need the new shape. Any hard-coded default in a PED mapper (NoEvent, empty list) becomes silent data loss the moment the semantic model gains the property — grep the mappers for literals when a construct becomes authorable. ped_check_errors lags: a fresh document reports 'No errors found.' and its real errors only on a later call, so re-check before trusting a clean verdict"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-phase-4a-took-sdk-mpr-from-27-importers-0-but-nothing.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-phase-4a-took-sdk-mpr-from-27-importers-0-but-nothing.json new file mode 100644 index 0000000000..f343ee7c36 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-phase-4a-took-sdk-mpr-from-27-importers-0-but-nothing.json @@ -0,0 +1 @@ +{"area":"mdl/backend","date":"2026-09-15","symptom":"Phase 4a took sdk/mpr from 27 importers to 0, but nothing stopped the count from creeping back — there was no build or test guard, only the plan document and a habit.","cause":"The invariant lived in prose. A single new `import \"github.com/mendixlabs/mxcli/sdk/mpr\"` compiles, passes every test, and reintroduces exactly the blind spot Phase 4a existed to close: the unimplemented-method census in mdl/backend/modelsdk lists methods with NO implementation, so a caller reaching one through a concrete *sdk/mpr.Reader never appears in it. That is what hid project_tree.go's 36 semantic reads (#477) and cmd_extract_templates.go's FindCustomWidgetType (#484) until each was found by hand.","file":"mdl/backend/sdkmpr_import_guard_test.go","fix":"TestNothingImportsTheLegacyEngine parses every .go file's imports (go/parser, ImportsOnly) and fails naming any file that imports sdk/mpr, with the remedy in the message. Controlled by dropping a one-line file importing sdk/mpr into examples/ — it fails and names the file.","insight":"A zero-count invariant needs TWO positive controls or it passes vacuously forever, and the failure mode is silent by construction: a walk rooted at the wrong directory, a skipped-dir rule that is too broad, or an import-parsing mistake all report '0 importers' and read as success. So assert (1) a plausible number of files was actually scanned (here >500; it sees 2551) and (2) the detector can see imports AT ALL, by counting a package the repo definitely does import (mdl/backend, 120 files). Only then does 0 mean zero. This is scripts/check-tunnel-deps.sh's pattern — it asserts chisel IS in the linux graph before asserting it is absent from windows/darwin — and the same reasoning as a bug-fix control: a test that only ever passes has not been shown to detect anything. Practical note: skip sdk/mpr's own directory by comparing the path to the repo root rather than by basename, or a directory named mpr elsewhere is skipped too."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-six-fullbackend-methods-sat-unreachable-census-being-treated.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-six-fullbackend-methods-sat-unreachable-census-being-treated.json new file mode 100644 index 0000000000..d4b98eda50 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-six-fullbackend-methods-sat-unreachable-census-being-treated.json @@ -0,0 +1 @@ +{"area":"mdl/backend","date":"2026-09-15","symptom":"Six FullBackend methods sat on the unreachable census being treated as abstraction bypasses awaiting a port, when nothing anywhere wanted them: the whole WidgetSerializationBackend interface (SerializeWidget/ClientAction/DataSource/WorkflowActivity), GetUnitTypes and UpdateLayout. They had been superseded and simply never removed.","cause":"scripts/backend-reachability.sh reports DEAD for 'nothing calls this through a backend value', and that one verdict covers three different situations. The census header read all of them as bypasses, so the standing instruction was to port them — which for these would have meant implementing methods with no caller.","file":"mdl/backend/mutation.go","fix":"Deleted WidgetSerializationBackend, GetUnitTypes and UpdateLayout from the interface; regenerated unimplemented_gen.go and mcp/unsupported_gen.go (276 -> 270 methods); dropped the mock stubs and a duplicate *Backend impl. Census 11 -> 6, and the remaining six are genuine bypasses. Rewrote the census header and the probe script's own header to name the three causes.","insight":"A reachability probe that removes a method and rebuilds answers ONE question — is anything calling this through the interface — and DEAD has three causes wanting opposite fixes: BYPASS (a caller wants it but holds a concrete type; port the caller), ORPHAN (no caller anywhere; delete), DUPLICATE (callers exist but through a narrower package-local interface with a DIFFERENT SIGNATURE; delete). The probe cannot separate them; a grep for callers under any type can. DUPLICATE is the one that misleads, and it is worth knowing its two tells. First, a raw call-site count looks healthy — SerializeWidget had 6 and SerializeClientAction 4 — so the name reads as live; only comparing signatures shows the interface copy is unused (bson.D on the real path vs (any, error) on FullBackend). Second, the supersession is usually DOCUMENTED at the replacement, not at the corpse: WidgetBuilderBackend.SerializeWidgetToOpaque says 'This replaces the direct mpr.SerializeWidget call' — so grepping the NEW method's doc comment finds the old one faster than auditing the old one does. A stale comment on the dead method actively lies (the vestigial *Backend SerializeWorkflowActivity claimed the ALTER WORKFLOW paths used it; they use codecWorkflowDeps). Generalisable: when a helper has both an (any, error) and a concrete-typed variant, suspect the general-typed one is the abandoned interface obligation."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-timer-boundary-events-written-over-mcp-studio-pro-11-14.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-timer-boundary-events-written-over-mcp-studio-pro-11-14.json new file mode 100644 index 0000000000..fd7a7031c0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-timer-boundary-events-written-over-mcp-studio-pro-11-14.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-15", "symptom": "Timer boundary events written over `--mcp` (Studio Pro 11.14): every one reports CE0126 \"Missing value for parameter 'Timer'\"; an interrupting timer's `jump to` outside a parallel split is stored as an End, and inside a split an End path gets a target-less jump; a `{ }` path gets an End appended after the end-of-path marker and is refused; the timer events of a single user task are not stored at all. mxbuild 11.13 builds all of these", "cause": "Neither timer boundary event constructor has `firstExecutionTime` (ped_get_schema), so the delay mxcli sent was ignored; the interrupting constructor normalizes the path terminator (End outside a split, jump inside) and needs `isInsideOfParallelSplit`, which mxcli sent only for notification events; the single user task constructor drops `boundaryEvents`, and the re-add covered notification events only", "file": "`mdl/backend/mcp/workflow.go` (`markBoundaryEventsInSplits`, `timerBoundaryEventOps`, `applyTimerBoundaryEvents`, `applyUserTaskBoundaryEvents`, `InsertBoundaryEvent`)", "insight": "**Three traps that each looked like success.** (1) `ped_update_document` re-validates the WHOLE document, so one malformed boundary path makes every later update fail — the post-write `set` of the delay included; refusing bad shapes before sending is load-bearing, not cosmetic. (2) An `add` to a `boundaryEvents` list does NOT append and does not keep add order (I, NI-A, NI-B in one batch stored NI-B, NI-A, I); finishing timers by statement index put each delay on the OTHER event and `ped_check_errors` said \"No errors found.\" — only reading `firstExecutionTime` back per event caught it. Identify an added element by the `persistentId` that appeared, never by index. The constructor, by contrast, keeps order. (3) `create or modify` over MCP cannot see a workflow created earlier over MCP and not yet saved — the executor's existence check reads the .mpr on disk — so a live update-path probe must target a document that is on disk", "fix": "Send no `firstExecutionTime`; set it afterwards at the event's stored path. Refuse interrupting-timer paths running to their end, interrupting-timer paths in a split not ending in a jump, and non-interrupting-timer paths ending in a jump. Set `isInsideOfParallelSplit` on interrupting timers. Restore a jump outside a split by removing the constructor's End and adding the jump (measured clean). Re-add a single user task's events one `add` at a time, mapping statement index to stored index by persistentId, and address nested paths through that map; ALTER insert reads ids before/after the add"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-wiring-findcustomwidgettype-from-modelsdk-mpr-reader-onto.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-wiring-findcustomwidgettype-from-modelsdk-mpr-reader-onto.json new file mode 100644 index 0000000000..09dcd96cf3 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-15-wiring-findcustomwidgettype-from-modelsdk-mpr-reader-onto.json @@ -0,0 +1 @@ +{"area":"mdl/backend","date":"2026-09-15","symptom":"Wiring FindCustomWidgetType from modelsdk/mpr.Reader onto the codec Backend by straight delegation made `mxcli extract-templates` extract 0 of 6 templates, reporting for each widget: '[SKIP] Combo box: widget type is bson.D, want bson.D'. The type assertion in the caller names the same type on both sides of 'want'.","cause":"modelsdk/mpr builds RawType/RawObject with the v2 BSON driver (go.mongodb.org/mongo-driver/v2/bson) while sdk/mpr and every caller use v1 (go.mongodb.org/mongo-driver/bson). They are unrelated Go types that both print as 'bson.D', so the mismatch is invisible in the error text. types.RawCustomWidgetType declares the fields as `any` to avoid a BSON dependency, which removes the compiler's ability to catch it too.","file":"mdl/backend/modelsdk/widget_custom_find.go","fix":"Convert at the backend boundary with the package's existing v2ToV1BSON helper, so RawType/RawObject always hold v1 bson.D — the currency sdk/mpr established and callers assert. Verified by extracting all 6 templates byte-for-byte identically to the pre-change binary (1.2MB datagrid.json included); reverting the conversion fails the new test with 'RawType is bson.D, want v1 bson.D'.","insight":"An `any` field crossing an engine boundary can carry the RIGHT type name and the WRONG package, and the error message will look like a tautology. When a type assertion fails with identical type names on both sides, the question is which import path each came from, not what the type is — the two BSON drivers coexist in this repo on purpose (modelsdk is v2, sdk/mpr and the CLI are v1) and widget_pluggable_write.go's v2ToV1BSON already existed for the write direction. A cast written to silence that compile/assert error panics at runtime instead. Two process notes from the same change. (1) GREP FOR AN EXISTING IMPLEMENTATION BEFORE WRITING ONE: the walker had been in modelsdk/mpr all along (FindAllCustomWidgetTypes + collectCustomWidgets, and it populates UnitName/WidgetName which a fresh implementation would omit); only the backend wiring was missing, which is exactly what 'this should be unreachable' in the unimplemented error meant. (2) unimplemented_gen.go still emits the stub after a method is implemented — the generator writes a complete fallback set and Backend's own method shadows it — so the thing to update is the unreachableUnimplemented map in unimplemented_reachability_test.go, which fails loudly if a listed method becomes implemented."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-16-describe-enumeration-system-workflowactivitytype-enumeration.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-16-describe-enumeration-system-workflowactivitytype-enumeration.json new file mode 100644 index 0000000000..0a9b2919a7 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-16-describe-enumeration-system-workflowactivitytype-enumeration.json @@ -0,0 +1 @@ +{"area": "mdl/backend/modelsdk", "date": "2026-09-16", "symptom": "`describe enumeration System.WorkflowActivityType` -> \"enumeration not found\" while `describe entity System.WorkflowActivityRecord` prints `ActivityType: Enumeration(System.WorkflowActivityType)` in the same session. `show enumerations` omits every System enum; `check --references` REJECTS a valid attribute typed against one (a false positive that blocks correct scripts); `CATALOG.attributes LEFT JOIN CATALOG.enumerations` resolves 0 of 19 on a blank app. Values could only be guessed at until the build rejected one with CE1613", "cause": "`modelsdk/meta.SystemEnumerations` — all 15 System enumerations with their values — had been in the tree since #889 with ZERO non-test consumers. The System module is not stored in the .mpr at all (measured: the string `WorkflowActivityType` occurs in 0 of 370 mprcontents units and 0 bytes of the .mpr sqlite), so its entities/associations/Java actions are each synthesized by a `Build*` helper and appended to a listing. The enumeration half had the data table and neither the helper nor the wiring, so the entity attribute printed a type naming a document nothing could produce", "file": "`modelsdk/meta/system_enumerations.go` (new, `BuildSystemEnumerations`); wired in `mdl/backend/modelsdk/enumeration.go` (`ListEnumerations`, `GetEnumeration`)", "insight": "**A data table with no consumer looks exactly like a missing feature.** Before opening any file, `grep -rn --include=*.go | grep -v _test` — zero hits is the whole diagnosis, and it took one command. The virtual System module has one wiring point PER LISTING, so the question for any new System doctype is \"which listings must it appear in\", not \"is the data there\". **One append fixed four of the five reported symptoms at once** (describe, show, check --references, catalog) because the catalog builder takes `ctx.Backend` as its reader — so `ListEnumerations` is the single choke point. Two traps. (1) `search` was NOT fixed by it: the strings index is built from value CAPTIONS (`builder_modules.go`), and the meta table carries names only, so System enums produce 0 rows in `CATALOG.strings` while user enums produce 36. Defaulting a caption to the value name would be inventing text indistinguishable from a developer's own — left unfixed and filed instead. (2) A stale `.mxcli/catalog.db` made the catalog look unfixed for three measurements; delete it before concluding anything about catalog output. Diagnosis tip: the sibling guard `TestModelerSystemEntities_HaveResolvableGeneralizations` existed and its enumeration twin did not — when one half of a synthesized module has a resolvability test, check whether the others do", "refs": "mendixlabs/mxcli#1102, mendixlabs/mxcli#1071, #889", "ce": "CE1613"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-16-sdk-mpr-zero-importers-rm-rf-sdk-mpr-still-would-not-have.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-16-sdk-mpr-zero-importers-rm-rf-sdk-mpr-still-would-not-have.json new file mode 100644 index 0000000000..cf4c1fc47a --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-16-sdk-mpr-zero-importers-rm-rf-sdk-mpr-still-would-not-have.json @@ -0,0 +1 @@ +{"area":"mdl/backend","date":"2026-09-16","symptom":"With sdk/mpr at zero importers, `rm -rf sdk/mpr` still would not have been safe: it had two live dependencies that an import-based check cannot see. sdk/mpr/version has six importers (two of them shipping code, cmd/mxcli/docker/build.go and patch.go), and cmd/mxcli/docker/update_widgets_test.go reads sdk/mpr/testdata/v1-project by FILESYSTEM PATH.","cause":"The zero-importer guard matched the exact string \"github.com/mendixlabs/mxcli/sdk/mpr\". A SUBPACKAGE is a different import path, and a testdata directory is not an import at all — it is an os.DirFS string. Neither shape appears in a check written against the parent package's path.","file":"sdk/mpr","fix":"Repointed the six version importers at mdl/types (sdk/mpr/version.ProjectVersion is `type ProjectVersion = types.ProjectVersion`, an ALIAS, so it is the same type rather than a compatible one — modelsdk/mpr/version declares a duplicate struct and would NOT have been), moved the v1-project fixture to modelsdk/mpr/testdata/, verified both with the package still present, and only then deleted. 163 files, 41,674 lines.","insight":"Before deleting a package, search for THREE things, not one: the package's own import path, its subpackages' import paths (`.../pkg/`), and its directory as a literal string (testdata read through os.DirFS, go:embed, scripts). The last two are invisible to any importer census. Repoint everything FIRST and prove the build and tests green while the package still exists — that separates 'the repoint was wrong' from 'the deletion was wrong', which a single combined commit cannot distinguish. Two measurements worth keeping: the shipped binary is byte-identical in SIZE before and after, confirming the linker had already dropped the package, so this deletion removes source weight and not runtime behaviour; and `sdk/widgets` dropped to zero importers as a side effect but must NOT be deleted, because modelsdk/widgets/dirty_template_test.go reads sdk/widgets/templates/mendix-11.6 by path — the same path-not-import trap, found by grepping for the directory name rather than the import."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-end-end-confirmation-1119-data-loss-live-database-plus-part.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-end-end-confirmation-1119-data-loss-live-database-plus-part.json new file mode 100644 index 0000000000..eb5a981e59 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-end-end-confirmation-1119-data-loss-live-database-plus-part.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-17", "symptom": "END-TO-END CONFIRMATION of the #1119 data loss on a live database, plus the part nobody had measured: a recreated column with a MODEL DEFAULT is silently backfilled with that default, so the loss can look like plausible data rather than empty cells.", "cause": "Same as the #1119 record: `UpdateEntity` rebuilt each attribute with `raw==nil`, so `EmitGUID` wrote `GUID = $ID` and the runtime's synchroniser saw every attribute as deleted-and-re-added.", "file": "`mdl/backend/modelsdk/domainmodel_child_identity.go`; measured against Mendix 11.13.0 + PostgreSQL 16 via `mxcli run --local --ensure-db`", "insight": "The experiment, worth repeating for any identity-related write: blank `mxcli new` app on 11.13.0, subject `Administration.Account` (Studio Pro-authored, so `GUID != $ID` on all three attributes \u2014 an entity mxcli CREATED is immune, because its GUID equals its $ID from birth and an ALTER then reproduces the same value, which is exactly how this can be missed in testing). Seeded 607 rows. (1) `mendixsystem$attribute.id` holds the model `GUID` byte-for-byte once the .NET field order is undone \u2014 verified on all three attributes, and it matches the GUID, never the `$ID`. (2) PRE-FIX binary, one `ALTER ENTITY \u2026 SET DOCUMENTATION`: all three GUIDs became equal to their `$ID`, `mx check` reported 0 ERRORS on the result, then boot logged `ConnectionBus: Executing 14 database synchronization command(s)` and `fullname` went 607 -> 0, `email` 607 -> 0. The column ORDER in `\\d` changed, which is the cheap tell that a column was dropped and re-added rather than altered. (3) The boolean `IsLocalUser` read as intact at first \u2014 607 non-null \u2014 and that was a MEASUREMENT TRAP: it carries `default true` in the model, so the fresh column was backfilled from the default. Proved by setting all 607 to `false` first: after the GUID change they were all `true` again. So `count(col)` is not a data-loss test; seed a NON-DEFAULT value and check the values. (4) FIXED binary, same ALTER: GUIDs unchanged, and after redeploy all 607 rows kept `fullname`, `email` AND the non-default `islocaluser=false`. The control is the pre-fix binary built from the parent commit in a git worktree, not a stubbed guard, so it exercises the real shipped path.", "refs": ["mendixlabs/mxcli#1119"], "ce": []} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-every-mdl-write-existing-persistent-entity-resets-each.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-every-mdl-write-existing-persistent-entity-resets-each.json new file mode 100644 index 0000000000..38f098da88 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-every-mdl-write-existing-persistent-entity-resets-each.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-17", "symptom": "Every MDL write to an EXISTING persistent entity resets each attribute's storage GUID to the attribute's own `$ID`. The next deploy against a database that already holds data drops and recreates every column of that entity: rows and associations survive, all attribute values are gone. Reported from production \u2014 28 attributes of 607 rows emptied by one edit. `mxcli check`, `exec`, `mx check` and the build are all clean, and `DESCRIBE ENTITY` is byte-identical before and after. All six ALTER forms do it, INCLUDING `SET DOCUMENTATION`, which touches no attribute", "cause": "The unclosed half of #657. `UpdateEntity` rebuilds the target with `entityToGen`, which rebuilds every ATTRIBUTE via `attributeToGen` -> fresh `NewAttribute` (`raw==nil`), so the codec's `EmitGUID` default writes `GUID = $ID`. #657 carried `orig.Raw()` onto the rebuilt ENTITY and noted that siblings survive via the list-rebuild raw passthrough \u2014 but the target's own children are all rebuilt, and nothing carried their raw. The new GUID comes out equal to the STORED `$ID` (not a random one) because `canon.TransplantIDs` then substitutes the stored `$ID` over every 16-byte occurrence of the fresh one, GUID included, since the GUID *is* that value", "file": "`mdl/backend/modelsdk/domainmodel_alter.go` -> `UpdateEntity`; fix in `mdl/backend/modelsdk/domainmodel_child_identity.go` (`carryChildIdentity`); guard in `modelsdk/canon/storageguid.go`", "insight": "Three separate guards could not see this, and the reason is worth more than the fix. (1) The corruption is IDEMPOTENT: GUID is derived from a now-stable `$ID`, so the second identical write produces byte-identical bytes, elision fires and the executor reports `Unchanged`. Damage happens exactly once, on the first write, and every later diagnostic \u2014 including re-running the same script \u2014 looks clean. Measured: same unit bytes, 0 of 16 GUIDs matching the original. (2) `TestFreshGUIDFieldsHaveAnIdentityDecision` sees only codec `FreshGUIDFields`; a GUID derived from `$ID` is `EmitGUID`, a different registration, so it was never in that guard's view \u2014 the same blind spot that let `Workflows$*.PersistentId` through in #949. (3) `identityFields`/`CarryIdentity` reach only top-level properties of the document ROOT; these GUIDs sit on nested elements. When a write 'succeeds' but the reader shows nothing changed, assert on raw BSON \u2014 and ask whether the damage REPEATS, because a one-shot corruption defeats every same-vs-same check. Pair a GUID carry on the identity the caller tracked (`domainmodel.Attribute.ID`, which `attributeFromGen` round-trips), never on a structural pairing: `TransplantIDs`' matching is deliberately tolerant because a wrong `$ID` match only makes a diff bigger, but a wrong GUID match makes the runtime adopt another column's data under a new name and type. That is also why the canon guard REFUSES rather than repairs. The guard found the one legitimate GUID writer on the first full-suite run \u2014 `marketplace.ApplyIdentities`, which transplants captured GUIDs onto a module's replacement documents, exactly as Studio Pro's update does \u2014 so it needs a named opt-out (`UpdateRawUnitOwningStorageGUIDs`), and running the whole suite is how you find out which paths a new write-path refusal breaks. Issue mendixlabs/mxcli#1119", "refs": ["mendixlabs/mxcli#1119", "#657", "#949"], "ce": []} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-microflow-s-export-level-studio-pro-s-hidden-api-switch.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-microflow-s-export-level-studio-pro-s-hidden-api-switch.json new file mode 100644 index 0000000000..7c754e537e --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-microflow-s-export-level-studio-pro-s-hidden-api-switch.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-17", "symptom": "A microflow's **export level** (Studio Pro's Hidden/API switch \u2014 whether it is part of the module's public surface when the module is exported as a package) is reset to `Hidden` by any `CREATE OR MODIFY MICROFLOW`. Every checker stays green, because a hidden microflow is a valid microflow; the module's API is simply smaller", "cause": "`microflowToGen` wrote `out.SetExportLevel(\"Hidden\")` unconditionally, `microflowFromGen` never read it back, and `sdk/microflows.Microflow` had no field \u2014 the identical three-layer gap as the deep-link URL in the same function", "file": "`mdl/backend/modelsdk/microflow_write.go` (microflowToGen), `mdl/backend/modelsdk/microflow.go` (microflowFromGen), `mdl/executor/cmd_microflows_build.go`, `sdk/microflows/microflows.go`", "fix": "Same carry as the URL, plus a DEFAULT: `\"\"` is not a member of `MicroflowsExportLevel`, so an empty model value is written as `Hidden` rather than passed through (the precedent is `json_write.go`). DESCRIBE emits `-- Export level:` only when the value is not `Hidden`", "insight": "Found by running the mechanical audit the URL fix prompted \u2014 `grep 'out.Set.*(\"\\|(nil)' microflow_write.go` over the one function \u2014 which is the cheap move after any instance of this class and turned up three more constants in one line. **The measurement that shaped the fix**: three real marketplace modules (Business Events 3.12.0, External Database Connector 6.2.3/6.3.0) store `Hidden` on 3 of 3 microflows and 55 of 55 documents overall, all three exporting at module level `Source` \u2014 so the hardcoded value was not wrong, it was a default masquerading as a constant. That is the shape of the trap: the audit finds the constant, but only a reference document tells you whether to carry it, default it, or leave it alone. A marketplace `.mpk` is a free source of these \u2014 `unzip -o pkg.mpk project.mpr` gives a real Studio Pro-authored MPR to query, no Studio Pro and no network needed (`mx-modules/` holds three). **Never carry an enum-valued property straight through without a default**: a stored document that says nothing reads as `\"\"`, and writing `\"\"` back is precisely the unloadable-model write CLAUDE.md warns about \u2014 mxbuild tolerates it and Studio Pro throws at MprProperty.cs. Controls: pinning the writer back, stubbing the reader, and neutralising the executor carry each fail a different test with the reported symptom"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-microflow-s-url-deep-link-studio-pro-shows-microflow-s.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-microflow-s-url-deep-link-studio-pro-shows-microflow-s.json new file mode 100644 index 0000000000..5c51696c87 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-microflow-s-url-deep-link-studio-pro-shows-microflow-s.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-17", "symptom": "A microflow's URL (the deep link Studio Pro shows on the microflow's properties, Mendix 10.6+, e.g. `item/{Key}`) disappears after any `CREATE OR MODIFY MICROFLOW` \u2014 including one that only edits the body. `mxcli check`, `mx check` and mxbuild all report success before and after; the loss is visible only in Studio Pro (#1120)", "cause": "`microflowToGen` wrote `out.SetUrl(\"\")` and `out.SetUrlSearchParametersQualifiedNames(nil)` unconditionally in its `major >= 10` block, `microflowFromGen` never read either back, and `sdk/microflows.Microflow` had no field to hold them \u2014 so the value had no path across a rewrite at any of the three layers", "file": "`mdl/backend/modelsdk/microflow_write.go` (microflowToGen), `mdl/backend/modelsdk/microflow.go` (microflowFromGen), `mdl/executor/cmd_microflows_build.go`, `sdk/microflows/microflows.go`", "fix": "Carry `Url`/`UrlSearchParameters` the way AllowConcurrentExecution/MarkAsUsed/ApplyEntityAccess already are: field on the semantic microflow, read in microflowFromGen, written from the model in microflowToGen, and seeded from the stored microflow in the executor's rewrite path. DESCRIBE emits a `-- URL: \u2026` note, because a describe -> rename -> exec COPY still has nothing to preserve from", "insight": "This is the fifth property in `microflows.Microflow` lost this way and the first with NO checker behind it, which is what made it a user report rather than an internal find. The earlier four were all caught by a build error eventually (CE4899 for the concurrency flags, CE0122 for Excluded) or by a security review (ApplyEntityAccess); a microflow with no URL is simply a valid microflow, so every gate stays green and only a human opening Studio Pro can see it. **Generalisable**: when auditing a rebuild for guard-don't-drop, rank the constants it writes by whether a checker would notice their absence \u2014 the ones nothing checks are the ones that reach users, and they are exactly the ones a 'does this look like configuration?' audit skips. The mechanical version is to diff a Studio Pro document key by key against the writer's output; here `grep 'out.Set.*(\"\")\\|(nil)' microflow_write.go` finds the whole remaining set in one line (`ExportLevel` pinned to \"Hidden\", `ConcurrencyErrorMicroflow`/`ConcurrencyErrorMessage` emptied \u2014 both still unguarded, though CE4899 makes the concurrency pair loud). Measured control: reverting either half (SetUrl or the read) alone fails TestMicroflowRoundTrip_DeepLinkURL with the reported symptom, so both halves are load-bearing"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-unit-tests-carried-microflow-property-url-export-level.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-unit-tests-carried-microflow-property-url-export-level.json new file mode 100644 index 0000000000..12011fdef4 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-17-unit-tests-carried-microflow-property-url-export-level.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-17", "symptom": "Unit tests for a carried microflow property (URL, export level, concurrency) all pass, and the end-to-end behaviour against a real project is still unverified \u2014 the integration gate that would have caught it, `TestMxCheck_DoctypeScripts`, `t.Skip`s whenever `mx` is absent, which is every run in a fresh container", "cause": "Two separate measurement errors, both invisible to `go test`. (1) The test fixture paired `Url: \"item/{Key}\"` with `UrlSearchParameters: [\"\u2026.Key\"]` \u2014 the SAME parameter \u2014 which mxbuild rejects as **CE5612**: a parameter used in the URL path may not also be a search parameter. Nothing in a unit test validates the model, so the fixture described a document Mendix refuses to build. (2) `bin/mxcli` was stale: `go build ./mdl/...` and `make test` had been run after each fix, but not `make build`, so the end-to-end run exercised a binary predating two of the three commits", "file": "`mdl/backend/modelsdk/microflow_roundtrip_flags_test.go`, `mdl/executor/microflow_carried_properties_test.go`, `mdl/executor/roundtrip_doctype_test.go` (the skipping gate)", "fix": "Fixture uses a distinct `Filter` parameter and says why. End-to-end procedure that actually measures it: `mxcli setup mxbuild -p ` (~719 MB, works through the session proxy), copy `testdata/expr-checker` as the fixture, create the microflow with mxcli, seed the unauthorable properties straight into the stored unit with `mpr.NewWriter` + `UpdateRawUnit`, then `mx check` BEFORE (the fixture must be a document Mendix accepts, or it proves nothing), `mxcli exec` a body-only rewrite, read the unit back, `mx check` after", "insight": "**A skipping integration gate is worse than no gate**: `mxCheckAvailable()` + `t.Skip` means a green `make test` says nothing about mxbuild, and reading the CLAUDE.md line about #808 is not the same as checking whether it applies to your own run \u2014 `ls ~/.mxcli/mxbuild` is. **Rebuild the binary before any end-to-end run**, and check its mtime against the last commit: a stale `bin/mxcli` produced a result (URL survived, export level did not) that looked exactly like a genuine second-read-path defect, and sent me hunting for a duplicate resolver that does not exist. **Seed the fixture through the writer, not by hand-editing BSON**, and always `mx check` the seeded state first: the CE5612 error came from the seed, not from mxcli, and without the before-check it would have been misattributed to the fix. Measured, mxbuild 11.6.6: pre-fix binary rewrites the microflow to `Url=\"\"`, empty search params, `ExportLevel=\"Hidden\"`; post-fix keeps all three; `mx check` 0 errors on both the seeded control and the rewritten project"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-18-alter-page-m-p-set-documentation-refused-unsupported-page.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-18-alter-page-m-p-set-documentation-refused-unsupported-page.json new file mode 100644 index 0000000000..98512c1d64 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-18-alter-page-m-p-set-documentation-refused-unsupported-page.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-18", "symptom": "`ALTER PAGE M.P { SET Documentation = '...' }` is refused: \"unsupported page-level property: Documentation\". Documenting an existing page therefore means re-running its CREATE, because the `/** … */` doc comment on the create statement is the only other source", "cause": "`applyPageLevelSetMut` handled Title/Url/Popup*/Class/Style and had no Documentation case. The field was otherwise fully understood — `pageToGen` has always written it on CREATE via `out.SetDocumentation`, and gen binds it as `property.NewPrimitive[string](\"Documentation\")`. The grammar already parsed it (`identifierOrKeyword EQUALS propertyValueV3`) and the executor has no allowlist, so it was one missing case in the mutator, not a syntax gap", "file": "`mdl/backend/pagemutator/mutator.go` (`applyPageLevelSetMut`)", "insight": "One case covers **Page, Layout AND Snippet** — all three declare Documentation and all three reach this function through `SetWidgetProperty(\"\")`. Store an empty string rather than rejecting it: removing a doc comment from a script has to be expressible, and the property is a bare string with no unset value. **Update the unsupported-property message in the same change** — that list is the only guidance a reader gets, and a stale one sends them back to the CREATE workaround the fix exists to remove (a test asserts the message names Documentation). Note the re-run workaround is worse than it sounds: describe → exec is only as complete as what MDL can spell, so restating a page to document it can silently lose widgets", "refs": ["#527"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-access-rule-entity-carrying-autoowner-autochangedby-made.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-access-rule-entity-carrying-autoowner-autochangedby-made.json new file mode 100644 index 0000000000..b1130fc921 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-access-rule-entity-carrying-autoowner-autochangedby-made.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-20", "symptom": "An access rule on an entity carrying `AutoOwner` (or `AutoChangedBy`) made mxbuild report the whole module as **CE0066** \"Entity access is out of date\" \u2014 and `UPDATE SECURITY`, the documented repair for exactly that error, printed `Reconciled 1 access rule(s) in module Mod` and left the error standing. Four lines reproduce it on a clean production-security app: `alter entity M.Fab add attribute Owner: AutoOwner;` + `update security;`. The original reporter bisected 9 entities and 36 rules one grant at a time behind a ~40s `mx check` to find it, because CE0066 names only the module.", "cause": "mxcli wrote a MemberAccess for the implicit `System.owner` / `System.changedBy` association, in TWO places that had to agree: the GRANT handler (`cmd_security_write.go`) and `ReconcileMemberAccesses`. Mendix maintains those members itself and treats a rule naming one as out of date. The audit DATE members were already known to work this way (issuetracker #20) \u2014 the owner/changedBy pair was assumed to be the opposite case because they are associations rather than attributes, and Mendix really does add them implicitly. Fixed by writing no entry for any of the four, and by REMOVING a stored one in the reconcile (an explicit case before the foreign-module branch, which otherwise preserves `System.*` forever on the grounds that System is not loaded).", "file": "`mdl/backend/modelsdk/domainmodel_security_write.go` (isAuditMemberRef, ReconcileMemberAccesses), `mdl/executor/cmd_security_write.go`", "insight": "**The decisive probe was removing the entry, not adding anything.** CE0066 says 'out of date', which reads as 'something is missing' and sends you looking for a member to add; the model had one too many. A build flag (`MXCLI_PROBE_NO_SYSOWNER`) that dropped the entry took the module from CE0066 to 0 errors in one mxbuild run and settled it. The same repo's earlier finding had already written the rule down \u2014 *'Ask mxbuild what it wants instead of inferring symmetry'* \u2014 and this defect is that exact inference, made in the same file for the sibling members. **A fix here is not done when the new writes are correct**: `update security` exists to repair a project an older mxcli damaged, so the reconcile has to remove the entry, not merely stop adding it. Measured separately: a stale `System.owner` entry survived even after the flag was turned off, because `!assocRefBelongsTo` preserved it as an unverifiable foreign-module reference.", "refs": ["#554", "#524", "issuetracker #20"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-describe-page-exec-over-studio-pro-authored-page-reports.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-describe-page-exec-over-studio-pro-authored-page-reports.json new file mode 100644 index 0000000000..93acf112f5 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-describe-page-exec-over-studio-pro-authored-page-reports.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-20", "symptom": "`describe page` → `exec` over a **Studio Pro-authored** page reports `Replaced page`, not `Unchanged` — the rebuild is not semantically equal to what was stored, so ADR-0008's elision cannot fire and the unit churns in version control on every re-run. `mx check` is 0 errors either way. Measured on ako/TestApp Rules.RuleAction_NewEdit at 11.14.0: fourteen differences", "cause": "Four independent classes, all 'the rebuild writes a constant where Studio Pro stores a value': (1) `pageToGen` hardcoded Autofocus/CanvasWidth/CanvasHeight; (2) save_changes/cancel_changes/close_page/delete_object never wrote `DisabledDuringExecution`, and save_changes wrote `SyncAutomatically` true; (3) `AttributeRef.EntityRef` emitted only on the navigated branch; (4) `Forms$PageVariable` had only the one name field set, so the other five keys were never marked dirty", "file": "`mdl/backend/modelsdk/page_write.go` (`carryStoredPageHeader`, `bsonInt`), `widget_write.go` (four action cases + two `RegisterTypeDefaults`), `modelsdk/codec/defaults.go` + `encoder.go` (new `FalseFields`)", "insight": "**mxcli round-tripping its own output proves NOTHING about this class** — measured: the MDL bug-test reports `Unchanged` on the unfixed build too, because mxcli writes the page and mxcli describes it, so its constants agree with themselves. The reference must be a Studio Pro document; a committed CI fixture only works if it is one. **A population selected by name can confirm whatever it excluded**: the first sweep filtered `$Type` on `endswith(\"ClientAction\")`, got a tidy 'True on 82 of 82', and so missed `Forms$NoAction` (False on 83 of ~7,300) and `Forms$MicroflowAction` (5 of 81) — scan by the PROPERTY, not by a name pattern. **Hardcoded looked safe and was not**: CanvasWidth takes seven distinct values across 67 pages and the hardcoded 1200 matched 4, so a round trip moved the canvas of 63. **The int width bit this fix once**: Studio Pro stores both canvas dimensions as int64 while the gen setter takes int32, so the natural `.(int32)` assertion matched nothing — and the first unit test passed anyway because its own fixture wrote int32, i.e. the test encoded the assumption under test (bson-numeric-width). Prefer `TypeDefaults` over patching each construction site: `Forms$PageVariable` is built in three places. Remaining after the fix: 1 of 14, a pluggable-widget Object property — CE0463 territory, deliberately out of scope", "refs": ["#541", "#529"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-drop-entity-left-every-cross-module-association-pointing.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-drop-entity-left-every-cross-module-association-pointing.json new file mode 100644 index 0000000000..99794a7b57 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-drop-entity-left-every-cross-module-association-pointing.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-20", "symptom": "`DROP ENTITY` left every CROSS-MODULE association pointing at the deleted entity in place. Dropping the local BY-ID (FROM) end made mxbuild 11.14.0 unable to LOAD the project: `System.AggregateException \u2026 (The given key '' was not present in the dictionary.)` at `StreamingBsonUnitReader.ResolvePostponedProperties()` \u2014 no CE code, no document named, so the obvious reading is 'the project is corrupt, restore from git'. Dropping the BY-NAME (TO) end is milder and still wrong: CE1613 at the cross-module association. `show associations` shows a raw GUID where the parent entity should be.", "cause": "`removeAssocsReferencing` swept `dm.AssociationsItems()` and asserted `*genDm.Association` per item, so the SEPARATE `CrossAssociations` collection was never looked at. Fixed with `removeCrossAssocsReferencing`, matching BOTH ends because a cross-module association addresses them differently \u2014 FROM by element id (local), TO by qualified name (another module) \u2014 called in DeleteEntity locally and in its cascade over the other domain models.", "file": "`mdl/backend/modelsdk/domainmodel_alter.go` (removeCrossAssocsReferencing, DeleteEntity)", "insight": "**Reported against a view entity; nothing about it was view-entity specific.** The reporter met it dropping view entities (whose associations are DERIVED from OQL, so there is no CREATE ASSOCIATION to undo) and filed it that way. The first probe \u2014 a view entity and its source entity in the SAME module \u2014 did not reproduce at all, and that negative is the useful one: it says the variable is cross-module, not view-ness. A plain `create association A.X from A.X to B.Y` plus `drop entity A.X` reproduces the identical crash. Two lessons: when a repro fails, vary the dimension the report did not mention before doubting the report, and treat a collection-typed `.(*T)` assertion in a cascade as a place where a sibling type hides. mxbuild's diagnostic distinguishes the two ends for free \u2014 a dangling 16-byte pointer is a LOAD crash, a dangling qualified name is CE1613 \u2014 so testing only one end proves half the fix.", "refs": ["#553", "#556"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-every-page-mxcli-wrote-carried-autofocus-property-mendix.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-every-page-mxcli-wrote-carried-autofocus-property-mendix.json new file mode 100644 index 0000000000..3eaddd0587 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-20-every-page-mxcli-wrote-carried-autofocus-property-mendix.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-20", "symptom": "Every page mxcli wrote carried `Autofocus` (a property Mendix introduced in 11.1.0) and every page and snippet carried `Variables` (10.17.0), whatever the project's version. On a Mendix 10 project the document therefore held a key that project's metamodel does not declare — the class of defect that makes Studio Pro throw InvalidOperationException at MprProperty.cs while mxbuild reports 0 errors. Found while fixing mendixlabs/mxcli#1121 (the same defect in the page's PARAMETERS) and deferred there as out of scope.", "cause": "`pageToGen` called `SetAutofocus(\"DesktopOnly\")` unconditionally, and both `Forms$Page` and `Forms$Snippet` registered `Variables` in `codec.RegisterTypeDefaults(...).MandatoryLists`, which the encoder emits as an empty typed-array marker for every new element. The page writer had no version awareness at all until #1121 threaded `*types.ProjectVersion` into `pageToGen` for the parameter keys.", "file": "`mdl/backend/modelsdk/page_write.go` (pageSupportsAutofocus/pageSupportsVariables, encodePage, docEncoder), `mdl/backend/modelsdk/snippet_write.go` (encodeSnippet), `modelsdk/codec/encoder.go` (Encoder.OmitKeys). Tests `mdl/backend/modelsdk/page_version_keys_test.go`, `modelsdk/codec/omitkeys_test.go`.", "insight": "**A version-floored key can arrive by two different routes, and only one of them is a gen property.** `Autofocus` is set through gen, so the fix is to not set it; `Variables` exists only because the Studio Pro defaults registry adds it, and a gen `PartList` has no 'present but empty' state — so there is nothing to leave unset and the suppression has to happen in the encoder. That registry is global and keyed by `$Type` alone, so it cannot see a project version: hence `Encoder.OmitKeys`, per-encode. **Suppressing a key and dropping data are different things**: the empty `Variables` marker is safe to suppress, but variables the script DECLARED are refused instead (guard-don't-drop, ADR-0005) — silently dropping them leaves widgets referencing names that are gone (CE1151) from a statement that reported success. Two measurement traps hit on the way: the first control run died on a pre-existing entity before reaching the page statement, so the page was never rewritten and the unchanged output read as a pass — a control that does not run looks exactly like a control that passes; and ALTER PAGE turned out to be safe without any change, because the page mutator edits the stored raw BSON and marshals it back, so it can never invent a key. Check which write paths actually rebuild a document before assuming a header fix has to cover all of them.", "refs": ["#1121"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-21-alter-settings-workflows-add-group-auditors-reports-added.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-21-alter-settings-workflows-add-group-auditors-reports-added.json new file mode 100644 index 0000000000..cb83a595e0 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-21-alter-settings-workflows-add-group-auditors-reports-added.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-21", "symptom": "`alter settings workflows add group 'Auditors'` reports \"Added workflow group: Auditors (3 group(s))\" and writes nothing \u2014 `show workflow groups` still lists 2, and `mx check` is 0 errors either way", "cause": "`UpdateProjectSettings` overlays the workflows part field by field onto the PRESERVED raw part, so a child LIST that nothing rebuilds is carried through from disk unchanged. Adding `Groups` to the semantic model and to the read path is not enough; the write needs `settingsoverlay.WorkflowGroups(ws, rawPart)`. Identical shape to the enabled-language list the same function already documents", "file": "`mdl/backend/modelsdk/settings_write.go` + `mdl/settingsoverlay/settingsoverlay.go` (`WorkflowGroups`)", "insight": "For anything under Settings$ProjectSettings, the executor's success message proves NOTHING \u2014 it reports the in-memory model, and the overlay is where a list quietly fails to land. Assert on the re-read document, not the handler's output. Two more things a reference project settles in one dump and a guess gets wrong: the `Groups` typed-array marker is 2, not the 3 every other settings child list uses (`ArrayMarker` preserves a stored one, but the fallback matters on a fresh list), and the element's `$ID` is the RUNTIME's identity \u2014 a booted 11.13.0 app keys `system$workflowgroup.modelguid` on it, byte-identical once the .NET GUID field order is undone, so re-minting it on a description edit would orphan every group membership with a perfectly valid model. Control: deleting the one overlay call reproduces the symptom verbatim. mendixlabs/mxcli#272", "refs": ["mendixlabs/mxcli#272"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-21-create-modify-view-entity-changed-only-oql-printed-unchanged.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-21-create-modify-view-entity-changed-only-oql-printed-unchanged.json new file mode 100644 index 0000000000..f36e14f062 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-21-create-modify-view-entity-changed-only-oql-printed-unchanged.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-21", "symptom": "`CREATE OR MODIFY VIEW ENTITY` that changed ONLY the OQL printed `Unchanged view entity: \u2026` while `describe entity` showed the new query stored. Changing the attribute list as well reported `Modified` correctly, which is why it hid. Also: the OQL document's unit was replaced under a FRESH GUID on every run, even a byte-identical one, so an MDL-generated project could never come back clean in git (one of the four units #556 measured).", "cause": "A view entity's OQL lives in a separate `DomainModels$ViewEntitySourceDocument` unit, and the executor DELETED it and INSERTED a fresh one on every write. `ReportMutation` downgrades the verb when writes were offered and none landed, but the counters are incremented only at the update choke points (`writer_core.go` reconcileWithStored / MoveUnit) \u2014 `InsertUnit` is not counted at all. So the domain-model unit was offered and correctly elided, the OQL write was invisible, and the report believed the half it could see. Fixed with `WriteViewEntitySourceDocument`, which keeps the stored unit's id and goes through `UpdateRawUnit` \u2192 reconcile: an identical query is elided, a changed one lands and is counted, duplicates are still cleared.", "file": "`mdl/backend/modelsdk/move_view_write.go` (WriteViewEntitySourceDocument, encodeViewEntitySourceDocument), `mdl/executor/cmd_entities.go`", "insight": "**The first fix that comes to mind \u2014 count InsertUnit \u2014 would have swapped a false \"Unchanged\" for a false \"Modified\".** Measuring before changing is what caught it: re-running a BYTE-IDENTICAL script still re-minted the source document's unit id, so counting inserts would have made every view-entity statement report Modified forever. The right fix was the one ADR-0008 already mandates (wire the write path to canon.Reconcile), and it fixes the churn and the verb together. Generalisation worth remembering: any content that reaches storage through `InsertUnit` is invisible to the elision check, so a statement whose only landing write is a NEW unit can still be mis-reported \u2014 `MoveUnit` has a comment explaining it was counted for exactly this reason, and insert/delete were missed. Control the fix on the identical re-run, not just the changed one.", "refs": ["#583", "#556", "#910"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-22-create-workflow-overview-page-x-reports-created-workflow.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-22-create-workflow-overview-page-x-reports-created-workflow.json new file mode 100644 index 0000000000..5bcad7049d --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-22-create-workflow-overview-page-x-reports-created-workflow.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-22", "symptom": "`create workflow … overview page X` reports `Created workflow` and exit 0 and stores NOTHING — the written unit carries no page reference and not even the page's qualified name as a string. `mx check` passes (a workflow with no overview page is valid) and `describe workflow` omits the clause, so nothing reveals the loss. Running `alter workflow … set overview page X` afterwards DOES write it, which is what makes the split visible", "cause": "Two fields for one concept, never joined: the executor set semantic `Workflow.OverviewPage` (`cmd_workflows_write.go:170`) and `workflowToGen` only ever read `Workflow.AdminPage`, which nothing set. The READ half was wrong in the mirror direction — `workflowFromGen` took `g.OverviewPageQualifiedName()`, so even the correctly-written ALTER read back empty and the catalog's overview-page reference edge never fired", "file": "`sdk/workflows/workflow.go` (the two fields collapsed to one), `mdl/backend/modelsdk/workflow_write.go` (`workflowToGen`), `mdl/backend/modelsdk/workflow_read.go` (`workflowOverviewPageName`)", "insight": "**The Model SDK's StructureVersionInfo settles which of two rival property names is real, in one grep**: `npm pack mendixmodelsdk` then `src/gen/workflows.js` gives `overviewPage: {deleted: \"9.11.0\"}` and `adminPage: {introduced: \"9.11.0\"}` — so AdminPage (a `Workflows$PageReference` CHILD, not a by-name string) is the stored property, and `generated/metamodel` agrees by declaring AdminPage and no OverviewPage. `modelsdk/gen` declares BOTH, which is how a reader and a writer ended up on opposite sides of a 9.11 rename inside one package. **The version branch CLAUDE.md's overlay rule would demand is dead here, and that is a measurement not an assumption**: `workflowToGen` writes `WorkflowV2`, introduced in 11.1.0, unconditionally — so no reachable project wants the pre-9.11 key. Write one spelling, READ both (a read fallback invents nothing). **The differential that proves it on a real build**: same script, same project, only the write suppressed — control 0 errors, fixed `CE7410 \"The selected page 'Overview' should accept a parameter of type 'Workflow'\"` on mxbuild 11.6.6. mxbuild can only validate a page it can see, so the error IS the evidence; with a valid overview page both variants are 0 errors, which is the usual weak-signal trap. Useful side-finding: an overview page takes **System.Workflow**, while a user task's page takes **System.WorkflowUserTask** — two pages, two parameters. NOT fixed: no check rule for CE7410 yet, and `WorkflowV2` being written unconditionally is questionable for a 10.x project. Same shape as the `create … comment 'text'` bug (findings/mdl-grammar.jsonl 2026-08-25): grep for `stmt.X = …` / `wf.X = …` with no matching read. Tests `mdl/backend/modelsdk/workflow_overview_page_test.go`; repro `mdl-examples/bug-tests/workflow-586b-overview-page-dropped.mdl`", "refs": ["ako/mxcli#586"], "ce": ["CE7410"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-22-modelsdk-mpr-version-projectversion-declared-own-struct-same.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-22-modelsdk-mpr-version-projectversion-declared-own-struct-same.json new file mode 100644 index 0000000000..7ae2458aa7 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-22-modelsdk-mpr-version-projectversion-declared-own-struct-same.json @@ -0,0 +1 @@ +{"area":"mdl/backend","date":"2026-09-22","symptom":"modelsdk/mpr/version.ProjectVersion declared its own struct with the same seven fields as mdl/types.ProjectVersion instead of aliasing it, so a *version.ProjectVersion could not be passed where a *types.ProjectVersion was wanted and vice versa — two unrelated Go types that both print as 'ProjectVersion'.","cause":"The deleted sdk/mpr/version aliased the canonical type (`type ProjectVersion = types.ProjectVersion`); this copy declared a duplicate. CLAUDE.md's shared-types rule asks for the alias, and nothing enforced it. The duplication survived the legacy-engine retirement because it compiles perfectly — the two declarations are field-for-field identical, so only an assignment ACROSS the boundary reveals them as different types.","file":"modelsdk/mpr/version/version.go","fix":"Made it an alias. The four methods it redeclared (IsAtLeast, IsAtLeastFull, String, IsMPRv2) were verified semantically identical to types' first — IsAtLeast differed only in early-return style, same truth table — and now come from types. IsSupported/SupportsFeature could not survive as methods on an aliased type and had ZERO callers anywhere (measured), so they went with Feature, MinVersion, featureVersions and SupportedVersionRange; that map called itself 'the fallback when the YAML registry is unavailable' and the live registry is sdk/versions/mendix-{9,10,11}.yaml via checkFeature.","insight":"A same-shape duplicate type is invisible to every signal except an assignment across the package boundary: it compiles, tests pass, and the error it eventually produces names the same type on both sides of 'want'. So the guard is a COMPILE-TIME assertion, not a runtime test — `var _ *types.ProjectVersion = (*version.ProjectVersion)(nil)` builds only under an alias and fails to build under a duplicate, which is strictly stronger than anything a test body can assert. Write it before the fix and watch it fail to compile; that failure IS the reproduction. Two measurements that made the cleanup safe rather than brave: diff the method BODIES before assuming the redeclarations are redundant (identical behaviour, different style, is the common case and the dangerous one is the near-miss), and count callers of anything the alias forces you to drop — here six exported symbols had zero. Unrelated trap hit while verifying: four cmd/mxcli tests that read skill files failed once in a full `go test ./...` interleaved with `make check-mdl`, which runs sync-skills (rsync --delete into cmd/mxcli/skills/). They pass in isolation, on clean main, and in an uninterleaved full run — do not attribute a skills-reading test failure to your change without re-running it alone."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-22-move-entity-mod-e-target-either-silently-destroyed-entity-s.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-22-move-entity-mod-e-target-either-silently-destroyed-entity-s.json new file mode 100644 index 0000000000..372ee1e49f --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-22-move-entity-mod-e-target-either-silently-destroyed-entity-s.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-22", "symptom": "`MOVE ENTITY Mod.E TO Target` either silently destroyed the entity's whole table on the next deploy, or \u2014 after the #1119 write guard landed \u2014 was REFUSED outright for any entity participating in an association, naming a `DomainModels$CrossAssociation` the user never mentioned.", "cause": "`MoveEntity` (`mdl/backend/modelsdk/association_move_write.go`) rebuilds the entity with `entityToGen` and carried NEITHER of the two identity carries `UpdateEntity` has \u2014 no `SetRaw(orig.Raw())` (#657, the entity's own GUID) and no `carryChildIdentity` (#1119, attributes and indexes). Separately `crossAssocFromGenAssoc` built a fresh `NewCrossAssociation` that preserved the association's `$ID` but not its GUID, so `EmitGUID` wrote `GUID = $ID`. The guard saw only that third one, because the conversion happens IN PLACE in the source unit while the entity lands in a different unit where its `$ID` pairs with nothing.", "file": "`mdl/backend/modelsdk/association_move_write.go` (`MoveEntity`, `crossAssocFromGenAssoc`, `crossAssocRawFromAssoc`)", "insight": "MEASURED on Mendix 11.13.0 + PostgreSQL 16, same 250-row starting state both ways: GUID re-minted -> the old table is dropped and an empty one created (250 -> 0 rows); GUID preserved -> the runtime RENAMES the table (`myfirstmodule$moveprobe` -> `administration$moveprobe`, 3 DDL commands) and every row survives with its values. So a module move preserves data IFF the GUID does \u2014 the runtime resolves the entity by GUID, not by table name \u2014 and a move is therefore the WORST case of this class, losing a whole table rather than a column. No warning is warranted; the carry is the fix. Two method notes. (1) Converting an element to a different `$Type` cannot use `SetRaw`, because the encoder passes `$Type` through from raw, and cannot use gen's `SetDataStorageGuid` either: gen binds it under key `DataStorageGuid` where Studio Pro stores `GUID` (a keyaudit row) AND types it `string` where the property is a 16-byte binary. The way through is a RAW transform of the stored document \u2014 here `$Type` rewritten, `ChildPointer` -> `Child`, the two `*Connection` waypoints dropped, everything else verbatim \u2014 with the target key set taken from `generated/metamodel`, the arbiter: exactly the 13 keys `DomainModelsCrossAssociation` declares, confirmed against the emitted document. `SetRaw` + `InitFromRaw` leaves the element clean, so the encoder's existing-element path passes it through and the registered `EmitGUID` default (fresh elements only) does not double up. (2) The MXCLI-CREATED IMMUNITY bit three times in one session and is now the first thing to check when a GUID test or repro script passes: an entity mxcli created has GUID == $ID from birth, so 'GUID = $ID' re-mints the same value and nothing is detectable. It voided a runtime control, and it makes the MDL bug-test script in `mdl-examples/bug-tests/` unable to fail \u2014 verified by running the pre-fix binary against it. A raw carry onto a MOVED entity is nonetheless safe: the unmodeled `Image`/`ImageData` are qualified names for a document that does not move with the entity (so keeping them is correct, and today's move silently DROPPED the image), and everything needing a rewrite is modeled and therefore dirty. Separate, pre-existing, NOT this fix: `MOVE ENTITY` rewrites no qualified-name references, so a move of a referenced entity leaves 33 CE1613s in a blank 11.13 app \u2014 identical count before and after this change.", "refs": ["ako/mxcli#503", "mendixlabs/mxcli#1119", "#657"], "ce": ["CE1613"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-23-create-modify-persistent-entity-refused-1119-storage-guid.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-23-create-modify-persistent-entity-refused-1119-storage-guid.json new file mode 100644 index 0000000000..77a395ced6 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-23-create-modify-persistent-entity-refused-1119-storage-guid.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-23", "symptom": "`CREATE OR MODIFY PERSISTENT ENTITY` was REFUSED by the #1119 storage-GUID guard on a doctype script that had been passing for months: `failed to update entity: refusing to write unit d82b0484-…: 1 element(s) kept their $ID but would be written with a different GUID — dff2ced1-… (DomainModels$Attribute): stored 4b52b36b-…, would write dff2ced1-…`. Two independent defects wore that one message.", "cause": "(1) A REAL data loss the guard caught: `mergeDeclaredOntoStoredEntity` sets `merged.Attributes = declared.Attributes` and `merged.Indexes = declared.Indexes` — the lists the STATEMENT declares, built from text by the visitor and carrying no element ID — and `carryChildIdentity` keyed entirely on that ID, reading an empty one as 'a genuinely new member, so a fresh GUID is right'. Every attribute of a re-declared entity was therefore re-minted, i.e. #1119 through a second executor path. (2) A FALSE POSITIVE in the guard itself: `canon.TransplantIDs` pairs STRUCTURALLY ($Type + shape, LCS-anchored), so on a statement that drops six differently-named attributes and adds one, it paired the NEW attribute with a REMOVED one and handed it that stored `$ID`; the codec had written `GUID = $ID` and the transplant substitutes over every 16-byte binary, so the GUID followed. The guard's premise — written into its own doc comment as 'unambiguously' — that a shared `$ID` after the transplant means the same element, is false.", "file": "`mdl/backend/modelsdk/domainmodel_child_identity.go` (`carryAttributeIdentity`, `carryIndexIdentity`), `modelsdk/canon/storageguid.go` (`sameMember`, `elementGUIDs`)", "insight": "ONE ERROR MESSAGE, TWO DEFECTS, AND FIXING EITHER ALONE LEAVES IT RED — which is why the first fix (the name fallback) changed nothing and the SAME element and GUIDs came back byte-for-byte. That repetition was the signal: an identical failure after a real fix means the reproduction is exercising a different code path than the one reasoned about. What settled it was describing the actual subject: the marketplace `PublishedBusinessEvent` has six attributes and NONE is named `EventId`, so there was no member to carry — the pairing itself was spurious. Reproduce against the real stored document before believing any theory about which elements correspond. METHOD that made this cheap: the CI failure reproduced locally in 0.5s as a backend unit test (strip the IDs off a fixture entity's attributes, call UpdateEntity) versus 26s for the integration subtest, but ONLY the integration subtest could have found the second defect, because the false pairing needs a real drop-six-add-one document. Run both. TRADE-OFF worth restating: the guard now pairs on `$ID` + `$Type` + `Name`, which loses one arm — a RENAME that re-mints a GUID is no longer refused, since the name is what changed — and that arm is covered directly by the carry tests where it is decidable. A backstop that refuses correct writes is worse than a backstop with a hole: the first makes documented statements unusable, and this one already had. Also: feeding a deliberately-approximate pairing to a guard promotes its error rate into refusals. TransplantIDs' correctness bar is low ON PURPOSE (a wrong match only makes a diff bigger); anything that reads its output as identity has to add its own test of identity.", "refs": ["mendixlabs/mxcli#1119", "mendixlabs/mxcli#1169", "ako/mxcli#643"], "ce": []} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-23-describe-java-action-prints-contextobject-entity-parameter.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-23-describe-java-action-prints-contextobject-entity-parameter.json new file mode 100644 index 0000000000..73f8c03d6a --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-23-describe-java-action-prints-contextobject-entity-parameter.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-23", "symptom": "`describe java action` prints `ContextObject: entity <>` for a parameter declared `entity not null`; a bare type-parameter reference (`Obj: pEntity`, `returns pEntity`) reads back nameless too. The description no longer round-trips", "cause": "The stored parameter type (`CodeActions$EntityTypeParameterType` / `ParameterizedEntityType`) holds only a BY_ID pointer to the `CodeActions$TypeParameter`. `javaActionFromGen` carried the ID into the semantic type but never resolved it to the name, and the name is all the describer prints. `javascript_read.go` had always done this resolution pass; the Java reader was ported without it", "file": "`mdl/backend/modelsdk/java_read.go` (`resolveJavaActionTypeParameterNames`)", "insight": "The executor's `entity <>` fallback in `formatJavaActionType` is the tell: an empty name at DESCRIBE means the *reader* dropped a by-ID resolution, not that the writer lost it \u2014 the write path sets both ID and name, so a create\u2192read unit test in the backend reproduces it without any project. When a JS and a Java reader cover the same `CodeActions$` shapes, diff their post-processing first; any pass one has and the other lacks is a candidate. The write was never wrong (replaying the fixed description into a fresh project describes identically), so no `mx check` run is needed. Issue mendixlabs/mxcli#1034", "refs": ["mendixlabs/mxcli#1034"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-23-six-statements-were-affected-only-one-report-s-title.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-23-six-statements-were-affected-only-one-report-s-title.json new file mode 100644 index 0000000000..6ef1bc26e1 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-23-six-statements-were-affected-only-one-report-s-title.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-23", "symptom": "Six statements were affected and only one was in the report's title. On upstream, `ALTER ASSOCIATION ... SET COMMENT|SET OWNER`, `CREATE OR MODIFY ASSOCIATION` (even an identical re-run), `RENAME ASSOCIATION` and `RENAME ENTITY` each re-minted the storage GUID of EVERY element in the module's domain-model unit — reporter measured 282 in one module (37 entities, 224 attributes, 21 associations) and a runtime crash, `Cannot invoke \"...Table.getTableName()\" because \"table\" is null`. On a branch carrying the #1119 write guard the same six statements were REFUSED instead, naming 9 elements on a blank 11.13 app, so the commands were simply unavailable for any Studio Pro-authored module.", "cause": "`Backend.UpdateDomainModel` (`mdl/backend/modelsdk/domainmodel_alter.go`) removes and rebuilds the WHOLE Entities and Associations lists, so every element arrives `raw == nil` and the codec's `EmitGUID` default writes `GUID = $ID`. Its own doc comment claimed it preserved \"each element's identity\", which was true of the `$ID` (explicitly `SetID`-ed) and false of the `GUID`. This is the same defect as #657 (entity) and #1119 (its children) on the OTHER list-rebuild shape: `UpdateEntity` swaps one entity into an otherwise raw-passthrough list, so its siblings were always safe; here there are no passthrough siblings at all.", "file": "`mdl/backend/modelsdk/domainmodel_alter.go` (`UpdateDomainModel`); the carry helper is `domainmodel_child_identity.go` (`carryChildIdentity`)", "insight": "WHAT MAKES THIS THE EXPENSIVE ONE IS THE STATEMENT NOBODY ASSOCIATED WITH IT: `RENAME ENTITY` routes through the same function, and an entity's name IS its table name. Per the #503 measurement (11.13.0 + PostgreSQL 16, same 250-row start both ways) a preserved GUID makes the runtime RENAME the table and a re-minted one makes it DROP the table and create an empty one — so a rename loses a whole table, where an ALTER loses a column. The title said ALTER ASSOCIATION; the census of call sites is what found the rest. METHOD: enumerate every caller of a GUID-bearing converter (`entityToGen`, `assocToGen`, `attributeToGen`) rather than reproducing the reported statement — that closed the class in one pass and turned up a fifth site, `UpdateAttribute`, reachable only through the public `api/` package and therefore invisible to any MDL repro. The fix is simpler than #503's: `assocToGen` is Association -> Association, so the stored and rebuilt `$Type` agree and a direct `SetRaw` works, where a cross-module move had to transform the raw because the `$Type` changes. A raw carry here also SUBSUMES #872, which patched the association line anchors property-by-property after the same 'the rebuild only carries what the semantic model models' mechanism dropped them. TWO MEASUREMENT TRAPS, both cost time. (1) A GUID census keyed on element NAME reads a RENAME as 'the old element vanished, a new one appeared' — 4 moved and 1 added where the truth was 0; key it on `$ID`. (2) `ALTER ASSOCIATION ... SET DOCUMENTATION` is a parse error; the real spelling is `SET COMMENT`, so a probe written from the wrong keyword reports the statement unaffected. THE GUARD IS NOW THE CHEAPEST ASSERTION AVAILABLE for this class: a write that lands on a Studio Pro-authored unit IS proof no GUID in it moved, which is why the end-to-end check is 'the statement succeeded', not a BSON diff — verified anyway by grepping the 16-byte binaries under key `GUID` out of the `.mxunit` before and after (9 of 9 identical, all five statements, `mx check` 0 errors).", "refs": ["mendixlabs/mxcli#1169", "#657", "mendixlabs/mxcli#1119", "ako/mxcli#503", "#872"], "ce": []} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-25-data-view-datasource-nanoflow-module-nf-e-g-describe-exec.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-25-data-view-datasource-nanoflow-module-nf-e-g-describe-exec.json new file mode 100644 index 0000000000..af4e6182d6 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-25-data-view-datasource-nanoflow-module-nf-e-g-describe-exec.json @@ -0,0 +1 @@ +{"area":"mdl/backend","date":"2026-09-25","symptom":"A data view with `DataSource: nanoflow Module.NF` (e.g. describe → exec of Feedback v4.0.2's FeedbackModule.ShareFeedback) passes `mxcli check` and exec, then mxbuild 11.13.0 reports CE2633 \"No nanoflow configured for the data source of this data view\". Same result through `alter page … set DataSource = nanoflow X on dv`","cause":"Both writers nested the name in a `Forms$NanoflowSettings` child (ParameterMappings marker 3) by analogy with `Forms$MicroflowSource`, which really does nest `Forms$MicroflowSettings`. Studio Pro's `Forms$NanoflowSource` is FLAT: ForceFullObjects, Nanoflow, ParameterMappings (marker 2) directly on the source — exactly gen's shape. mxbuild found no Nanoflow key. The raw nested builder also never read d.ParameterMappings, so a parameterized source nanoflow lost its arguments. On the read side, the ALTER PAGE flow-context lookup (`flowFromDataSourceDoc`) and describe's argument reader (`flowSourceArgs`) only knew the nested shape, so Studio Pro-authored nanoflow sources yielded no entity context / no arguments","file":"`mdl/backend/modelsdk/widget_write_legacy_gaps.go` (`nanoflowSourceToGen` via gen + `Forms$NanoflowSource` TypeDefaults in `widget_write.go`), `mdl/backend/pagemutator/mutator.go` (`serializeDataSourceBson`, `flowFromDataSourceDoc`), `mdl/executor/cmd_pages_describe_datasource.go` (`flowSourceArgs`)","insight":"**The code comment asserted the wrong shape as a measured fact** (\"Studio Pro nests it in a Forms$NanoflowSettings child … Legacy's shape is the one with a working project behind it\") and a unit test pinned it — both were parity-with-legacy, never measured. `Forms$NanoflowSettings` is not a type in modelsdk/gen or generated/metamodel: **when gen and a hand-rolled builder disagree about a type's shape, grep gen for the type the builder invents before trusting the builder**. What settled it in one step: a 60-line scanner that `bson.Unmarshal`s every mprcontents unit and prints the key-set (with list markers) of each `$Type` instance — 5 of 5 flat nanoflow sources, and 4 of 4 microflow sources nested as gen says, so the microflow path needed nothing. **Enumerate every writer of the type, not just the reported one**: the ALTER PAGE setter had its own copy of the same wrong literal, and the read-side lookups keyed on the wrong shape meant Studio Pro pages were the ones silently mis-read. Readers keep the nested fallback for pages written before the fix. Verified: exec + `mx check` 11.13.0 CE2633 → 0 errors for CREATE PAGE (ShareFeedback round trip, repro script) and ALTER PAGE; ShareFeedback's dataView5 DataSource ndsl now matches Studio Pro exactly. Control: implementation reverted → the 5 new tests fail with the nested key set. Repro `mdl-examples/bug-tests/dataview-nanoflow-source-ce2633.mdl`","refs":[],"ce":["CE2633"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-25-returns-list-pentity-declared-type-parameter-produced-mx.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-25-returns-list-pentity-declared-type-parameter-produced-mx.json new file mode 100644 index 0000000000..7b6063071c --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-25-returns-list-pentity-declared-type-parameter-produced-mx.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-25", "symptom": "`returns list of pEntity` for a declared type parameter produced mx check CE1613 \"The selected entity '.pEntity' no longer exists.\" on the action and on a `list of pEntity` parameter; a Studio Pro \"List of \" read back as a bare `List` (DESCRIBE and catalog) and a rewrite of it would serialize a list of an unnamed entity.", "cause": "types.ListType carried only Entity. The reader handled only a ConcreteEntityType list element, the writer always emitted one, and CREATE sent `list of T` down the entity path (Module \"\" + \".\" + T).", "file": "mdl/types/javaaction_types.go (ListType.TypeParameterID), mdl/backend/modelsdk/java_read.go (listTypeFromGen), java_write.go (codeActionListTypeToGen), javascript_read.go, mdl/executor/cmd_javaactions.go (listOfTypeParameter)", "insight": "The Model SDK is the arbiter for which element a slot accepts: `ParameterizedEntityType.createInListTypeUnderParameter` (metamodel 7.21.0+) settles that a list element may be a type parameter, so no version gate. No fixture had a Studio Pro-authored instance, so the evidence is mx check on 11.6.6: previous build CE1613 x2, fixed build 0 errors, clean baseline 0. The JavaScript writer reuses the Java converter, so one write fix covers both; the JS reader is separate raw-map code and needed its own case.", "refs": ["mendixlabs/mxcli#1183"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-26-alter-page-set-imageurl-img1-pluggable-image-prints-altered.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-26-alter-page-set-imageurl-img1-pluggable-image-prints-altered.json new file mode 100644 index 0000000000..82b244ab81 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-26-alter-page-set-imageurl-img1-pluggable-image-prints-altered.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-26", "symptom": "`ALTER PAGE … SET ImageUrl = '…' ON img1` (pluggable Image) prints \"Altered page\" and changes nothing: DESCRIBE still shows the old URL. Same silent success for SET on any pluggable widget property of kind Expression, Image, Icon, Action, DataSource, Attribute or Widgets", "cause": "setPluggableWidgetPropertyMut (mdl/backend/pagemutator/mutator.go) wrote every value to Value.PrimitiveValue. imageUrl is a TextTemplate; readers take Value.TextTemplate. The DataGrid 2 column setter had the identical defect fixed on 2026-08-18 (columnValueField) — the widget-level setter sitting next to it was never given the same schema dispatch.", "file": "mdl/backend/pagemutator/mutator.go (setPluggableWidgetPropertyMut, buildPropKindMap), test pluggable_property_kind_test.go, example mdl-examples/bug-tests/1201-alter-page-set-pluggable-texttemplate.mdl", "insight": "When a fix lands on one setter for 'write the field the schema declares', grep the sibling setters in the same file for the same always-PrimitiveValue write — a WidgetValue carries every variant field, so the wrong write never errors and every signal stays green. The live control is cheap without mxbuild: copy testdata/expr-checker, create an image with URL A, ALTER to B, describe (pre-fix binary shows A). A null TextTemplate means the slot is hidden (#574); ALTER does not re-run visibility, so refuse rather than build an envelope there. Not fixed here: a pluggable boolean SET stores \"yes\"/\"no\" — check what CREATE stores before changing it.", "refs": ["mendixlabs/mxcli#1201", "mendixlabs/mxcli#1069", "mendixlabs/mxcli#750", "mendixlabs/mxcli#574"], "rules": []} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-26-describe-exec-nanoflow-detaches-every-annotation-from.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-26-describe-exec-nanoflow-detaches-every-annotation-from.json new file mode 100644 index 0000000000..84db48ce5e --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-26-describe-exec-nanoflow-detaches-every-annotation-from.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-26", "symptom": "describe → exec of a nanoflow detaches every annotation from its activity (AnnotationFlows 3 → 0, so the next describe prints the notes as free-floating) and deletes ExportLevel, UseListParameterByReference and ReturnVariableName; MarkAsUsed is cleared. Rules drop their annotation flows too", "cause": "nanoflowToGen and ruleToGen never wrote ObjectCollection.AnnotationFlows (microflowToGen did), and nanoflowToGen never set the three header keys; nanoflowFromGen did not read ReturnVariableName or MarkAsUsed and the builder hardcoded MarkAsUsed false", "file": "mdl/backend/modelsdk/nanoflow_write.go, rule_write.go, microflow.go; mdl/executor/cmd_microflows_build.go", "insight": "Three flow flavours share the object/flow converters but each has its own top-level writer, and each dropped a different subset — grep every *ToGen for the same list loop, not just the reported one. For keys with no MDL spelling and unknown version floors (Nanoflow.ExportLevel, UseListParameterByReference), carry ONLY the keys the stored unit has: nothing was ever written on create, Studio Pro fills an absent key, and an undeclared one makes the unit unopenable", "refs": ["ako/mxcli#705"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-26-describe-exec-snippet-turns-type-web-into-would-reset-non.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-26-describe-exec-snippet-turns-type-web-into-would-reset-non.json new file mode 100644 index 0000000000..35d3cee098 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-26-describe-exec-snippet-turns-type-web-into-would-reset-non.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-26", "symptom": "describe → exec of a snippet turns its Type 'Web' into '' (and would reset a non-default ExportLevel or canvas size)", "cause": "snippetToGen wrote Type, ExportLevel, CanvasWidth and CanvasHeight as constants on every write; the Type constant was \"\"", "file": "mdl/backend/modelsdk/snippet_write.go", "insight": "The snippet twin of #541 (carryStoredPageHeader). \"\" is not a member of PagesType (Native | Web), so every snippet mxcli created was also invalid. Carry the stored header on UpdateSnippet (width-agnostic canvas read — Studio Pro stores int64); a new snippet gets Web/Hidden/800×600, measured on all four Blank-template snippets", "refs": ["ako/mxcli#705", "#541"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-27-any-write-domain-model-even-rewriting-sibling-entity-drops.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-27-any-write-domain-model-even-rewriting-sibling-entity-drops.json new file mode 100644 index 0000000000..448fe6118a --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-27-any-write-domain-model-even-rewriting-sibling-entity-drops.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-27", "symptom": "Any write of a domain model (even rewriting a sibling entity) drops LocalizeDate from every DateTime attribute in the unit; external entity rewrites also rewrite Source/Key/Parts marker 2 -> 3", "cause": "attributeTypeToGen emitted DateTimeAttributeType without LocalizeDate and attributeTypeFromGen never read it; Rest$ODataKeyPart had no registered list marker so the encoder default 3 applied", "file": "mdl/backend/modelsdk/domainmodel.go, domainmodel_write.go", "insight": "A whole-unit rewrite re-serializes every sibling from the struct, so a property the struct cannot hold is lost on EVERY entity of the module, not just the edited one. Absent LocalizeDate means Mendix's default true (mxcli wrote it absent for years), so the reader must map absent to true or the fix would unlocalize every mxcli-authored DateTime. Only a Studio Pro fixture with DateTime attributes (TestApp) shows it", "refs": ["ako/mxcli#743"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-27-running-unchanged-describe-user-role-output-create-modify.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-27-running-unchanged-describe-user-role-output-create-modify.json new file mode 100644 index 0000000000..2be4f571d9 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-27-running-unchanged-describe-user-role-output-create-modify.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-27", "symptom": "Running unchanged `describe user role` output (create or modify user role) rewrites Security$ProjectSecurity: /UserRoles[0] marker 2 -> 3; same for DemoUsers on a demo-user rewrite", "cause": "The encoder's default PartList marker is 3; Studio Pro writes 2 for ProjectSecurity.UserRoles and .DemoUsers (PedApp, TestApp, expr-checker agree). An untouched list passes through raw, so it only shows once an element in it is rewritten", "file": "mdl/backend/modelsdk/security_write.go", "insight": "A marker bug hides until the first rewrite of an element in that list; census the stored marker per (owner $Type, key) across Studio Pro fixtures and register it with RegisterPropertyListMarker", "refs": ["ako/mxcli#731"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-alter-association-owner-storage-comment-delete-behaviour.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-alter-association-owner-storage-comment-delete-behaviour.json new file mode 100644 index 0000000000..4f7efbcdad --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-alter-association-owner-storage-comment-delete-behaviour.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-28", "symptom": "ALTER ASSOCIATION (owner, storage, comment, delete behaviour) and CREATE OR MODIFY ASSOCIATION on a cross-module association print success and write nothing; DESCRIBE shows the old values (#792)", "cause": "UpdateDomainModel rebuilt Entities and Associations and left CrossAssociations as raw passthrough under a stale comment claiming they were not in the semantic model — crossAssocFromGen had been filling them on every read", "file": "mdl/backend/modelsdk/domainmodel_alter.go", "fix": "patchCrossAssociations: edit each stored CrossAssociation in place, setting only properties that differ (GUID, export level, capabilities never touched); restrict side gets its refusal text, leaving restrict rebuilds the delete behaviour under its $ID so the message slot is null; a stored one absent from the semantic model is refused. Executor reads the association back after ALTER and errors 'change not persisted' on mismatch", "insight": "A whole-model update that rebuilds some lists and passes others through is only as correct as its list of what the semantic model carries; when a read converter exists for a list, the write side must consume it. The executor's success message was never evidence — a read-back is. A cleared gen Part omits the key; to get Studio Pro's explicit null, rebuild the element so the registered NullFields apply", "refs": ["ako/mxcli#792"], "test": "TestIssue792_UpdateDomainModelPersistsCrossAssociations, TestIssue792_CrossAssociationLeavingRestrictClearsMessage, TestAlterAssociation_RefusesToReportAWriteThatDidNotLand"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-alter-workflow-m-w-drop-aiagenttask1-outcome-old-drop.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-alter-workflow-m-w-drop-aiagenttask1-outcome-old-drop.json new file mode 100644 index 0000000000..05fee296c9 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-alter-workflow-m-w-drop-aiagenttask1-outcome-old-drop.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-28", "symptom": "`alter workflow M.W { drop aiAgentTask1 outcome '' }` (and the old `drop condition '' on X`) deleted the activity's default outcome and printed \"Altered workflow\"; on a parallel split the same statement deleted the first path. Measured on TestApp's Studio Pro-authored workflow.Workflow1 (aiAgentTask1 Outcomes 2 -> 1 items)", "cause": "DropOutcome/DropBranch (wfmutator and the MCP mutator) compared the address with DGetString(Value) / a lenient JSON unmarshal, which read an outcome that stores NO string value (ParallelSplitOutcome, VoidConditionOutcome, BooleanConditionOutcome) as \"\", so an empty value matched the first valueless member", "file": "mdl/backend/wfmutator/mutator.go", "fix": "a value address matches only an outcome whose Value is a stored string (wfmutator: type assertion on DGet; mcp: pedOutcomeElem.hasValue requires a JSON string); 'Default' still names a void outcome by type", "insight": "The #791 review claimed value-addressed drops cannot land on another kind's member; they can when the accessor maps 'absent' to the zero value. Any match of an author-supplied string against a getter that returns \"\" for missing is an address that silently hits the first member lacking the field", "refs": ["ako/mxcli#791"], "test": "TestTestAppAlterWorkflow_DropPathOnUserTaskIsRefused"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-alter-workflow-m-w-drop-usertask1-path-1-old-drop-path-path.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-alter-workflow-m-w-drop-usertask1-path-1-old-drop-path-path.json new file mode 100644 index 0000000000..29c7dc7e87 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-alter-workflow-m-w-drop-usertask1-path-1-old-drop-path-path.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-28", "symptom": "`alter workflow M.W { drop userTask1 path 1 }` (and the old `drop path 'Path 1' on userTask1`) deleted the user task's FIRST OUTCOME and printed \"Altered workflow\"; `drop path '' on X` deleted the last outcome/path. Measured on TestApp's Studio Pro-authored workflow.Workflow1: 'Good' of Good/Bad/Ugly gone, project still loads, check clean", "cause": "DropPath (wfmutator and the MCP mutator) matched \"Path N\" against the index of whatever Outcomes list the resolved activity had, never checking it was a parallel split; the insert ops' kind check (#415) lived only in the executor, which reads the STORED workflow, and it deliberately skipped the drops because a drop cannot write a wrong type — but it can address the wrong member", "file": "mdl/backend/workflow_members.go", "fix": "backend.CheckWorkflowMemberList (per storage $Type, the member lists the document declares) guards every member op in both mutators; backend.ParallelPathIndex refuses an empty/unnumbered caption and an out-of-range number; validateAlterWorkflowActivityKinds refuses drop path on a non-split at check time; `drop X boundary event` among several is refused under mdl 1 / warns MDL-V1-BOUNDARYDROP under mdl 0", "insight": "A drop cannot corrupt the TYPE of a list, which is why #415's guard left drops out — but an index-addressed drop on a list of another kind silently removes real content. Old unit fixtures built the split and decision as Workflows$UserTask, so the tests could never have seen it; the guard made those fixtures fail first", "refs": ["ako/mxcli#791", "ako/mxcli#415"], "test": "TestTestAppAlterWorkflow_DropPathOnUserTaskIsRefused"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-create-modify-consumed-odata-service-over-studio-pro-client.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-create-modify-consumed-odata-service-over-studio-pro-client.json new file mode 100644 index 0000000000..f6f965d314 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-create-modify-consumed-odata-service-over-studio-pro-client.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-28", "symptom": "create or modify consumed odata service over a Studio Pro client: UseQuerySegment true -> false, Icon, CustomLocationTemplate and the empty ApplicationId/EndpointId/CatalogUrl/proxy/microflow keys deleted, MetadataReferences [3] -> [0], ValidatedEntities [1] -> [0]", "cause": "consumedODataServiceToGen writes the document wholesale from a model that holds none of these; placeholders (UseQuerySegment false, ExportLevel Hidden) and addStrIf-omitted empties replaced what Studio Pro stored", "file": "mdl/backend/modelsdk/odata_write.go (carryStoredConsumedODataService)", "fix": "UpdateConsumedODataService lays the fresh document over the stored one: placeholder keys keep the stored value, an omitted optional key is written \"\" when stored (so a cleared value stays cleared), lists keep the stored marker", "insight": "When the model is a strict subset of the stored document, overlay onto the stored bytes rather than growing the model field by field; but an optional key the writer omits must become \"\" rather than be carried, or clearing it through alter silently stops working", "refs": ["ako/mxcli#743"], "test": "TestConsumedODataServiceRewrite_CarriesWhatTheModelDoesNotHold"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-every-domain-model-rewrite-deletes-maybegeneralization.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-every-domain-model-rewrite-deletes-maybegeneralization.json new file mode 100644 index 0000000000..4d023ae591 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-every-domain-model-rewrite-deletes-maybegeneralization.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-28", "symptom": "Every domain-model rewrite deletes MaybeGeneralization HasOwnerAttr/HasChangedByAttr/HasCreatedDateAttr/HasChangedDateAttr = false on Studio Pro entities (#721 B)", "cause": "entityToGen set only the true flags, for parity with the legacy serializer", "file": "mdl/backend/modelsdk/domainmodel_write.go", "fix": "Write all four flags", "insight": "Legacy-writer parity is not Studio Pro parity; the flags were the last getput loss of a Studio Pro external entity, which is why it surfaced in #743", "refs": ["ako/mxcli#721", "ako/mxcli#743"], "test": "TestEntityToGen_WritesEveryGeneralizationFlag"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-rewriting-studio-pro-workflow-drops-empty-flow-persistentid.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-rewriting-studio-pro-workflow-drops-empty-flow-persistentid.json new file mode 100644 index 0000000000..bc1112d48e --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-28-rewriting-studio-pro-workflow-drops-empty-flow-persistentid.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-28", "symptom": "Rewriting a Studio Pro workflow drops the empty Flow and the PersistentId of every outcome that leads nowhere, writes RelativeMiddlePoint/Size \"\" where Studio Pro stores \"0;0\", and deletes an empty EventSubProcesses [2]", "cause": "The writer added an outcome's Flow only when non-nil (Workflows$Outcome.flow is Required), gave condition outcomes no PersistentId, and omitted an empty EventSubProcesses list because the property is version-floored (11.8)", "file": "mdl/backend/modelsdk/workflow_write.go (encodeWorkflow, outcomeFlowToGen)", "fix": "Always write an outcome Flow; PersistentId on every outcome; \"0;0\" layout; empty EventSubProcesses via MandatoryListMarkers, suppressed through Encoder.OmitKeys below 11.8 only when the workflow has none (MandatoryListMarkers now honours OmitKeys)", "insight": "Encoder.OmitKeys suppresses a key whatever it holds, so version-floor only the empty default: suppressing the key outright would drop declared sub-processes on a project whose version could not be read", "refs": ["ako/mxcli#743"], "test": "TestWorkflowToGen_EveryOutcomeStoresAFlow"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-29-alter-association-create-modify-association-rename-mxcli.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-29-alter-association-create-modify-association-rename-mxcli.json new file mode 100644 index 0000000000..d1bced3380 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-29-alter-association-create-modify-association-rename-mxcli.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-29", "symptom": "ALTER ASSOCIATION / CREATE OR MODIFY ASSOCIATION / RENAME / mxcli layout on a Studio Pro module turns every API-exported entity in the module Hidden and rewrites every entity's access rules (50 of 93 elements in TestApp WorkflowCommons changed for a no-op write); ALTER ENTITY and MOVE ENTITY reset the target's (and its attributes') export level the same way. mx check stays clean (#801)", "cause": "entityToGen / attributeToGen / assocToGen SET ExportLevel \"Hidden\", and a property the rebuild sets is dirty and wins over the carried raw bytes; UpdateDomainModel rebuilt every entity and association, not only the one named. Member-access writers set only the reference in use, while Studio Pro writes both Attribute and Association keys (the unused one as \"\"), so every rebuilt rule differed from its stored self", "file": "mdl/backend/modelsdk/domainmodel_carry.go", "fix": "UpdateDomainModel passes an entity/association through as its stored element when the semantic model equals the stored one read back (entityUnchanged/assocUnchanged, ID- and nil/empty-insensitive semanticEqual). Rebuilds of existing entities (UpdateEntity, UpdateDomainModel, MoveEntity) go through carryStoredEntity: raw + child identity + stored ExportLevel (entity and carried attributes) + unchanged access rules substituted by the stored element, changed ones keep $ID/caption/documentation. carryStoredAssociation: raw + ExportLevel + unchanged delete behaviour. DomainModels$MemberAccess registers EmptyStringFields Attribute/Association", "insight": "A raw carry only preserves what the converter does NOT set; every constant a converter writes (here ExportLevel \"Hidden\") is still reset on the carried element, so audit the converter's Set* constants, not only unmodelled keys. The whole-list rebuild shape needs an 'unchanged -> pass the stored element' short-circuit or its blast radius is the unit. Invisible on mxcli-authored content, where the stored value IS the constant: TestApp is the only fixture with API entities (WorkflowCommons) and has no API attribute, so the attribute case was set up on top of it. The roundtrip harness had 87 getput entries (#721 B and TestApp associations) that were this bug", "refs": ["ako/mxcli#801", "ako/mxcli#721"], "test": "TestIssue801_UpdateDomainModelChangesOnlyTheAlteredElement, TestIssue801_OtherEntityRebuildsKeepExportLevel"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-29-describe-exec-create-modify-alter-rebuilds-document-turns.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-29-describe-exec-create-modify-alter-rebuilds-document-turns.json new file mode 100644 index 0000000000..b29d2deec2 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-29-describe-exec-create-modify-alter-rebuilds-document-turns.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-29", "symptom": "describe -> exec, CREATE OR MODIFY or an ALTER that rebuilds the document turns an API-exported document Hidden: enumerations, pages, layouts, rules, view-entity OQL source documents, import/export mappings, JSON structures, published and consumed REST services, scheduled events, workflows, database connections, business event services, data transformers, queues, regular expressions and agent-editor documents. The run reports success, mx check is clean; the module's public surface silently shrinks. A workflow's own `export level API` clause was a no-op on create and on rewrite.", "cause": "Each rewrite converter builds a fresh document and writes ExportLevel as a constant (\"Hidden\"), or passes the semantic model's value where the executor itself filled in \"Hidden\" (mappings, database connection, business events), and the unit is replaced wholesale. MDL has no export-level spelling for most of these kinds, so describe cannot print it and the executed script cannot restore it. workflowToGen ignored wf.ExportLevel entirely. The round-trip harness could not see it: every document in TestApp and PedApp is Hidden, the constant itself.", "file": "mdl/backend/modelsdk/export_level_carry.go", "fix": "One byte-level carry, keepStoredExportLevel(unitID, contents): replaces only the top-level ExportLevel element of the freshly encoded rewrite with the stored value, copying every other element verbatim, and never adds the key. Wired into every Update path that writes ExportLevel (UpdateEnumeration/Rule/Layout/ImportMapping/ExportMapping/JsonStructure/PublishedRestService/ConsumedRestService/DataTransformer/DatabaseConnection/BusinessEventService, writeCustomBlob update, WriteViewEntitySourceDocument update; page via carryStoredPageHeader). Kinds with an MDL spelling (workflow, scheduled event, queue, regular expression) use keepStoredExportLevelUnlessSet: an authored level wins. workflowToGen now writes orDefault(wf.ExportLevel, \"Hidden\").", "insight": "A fixture-driven round trip is blind to any constant that happens to equal every fixture value: 775 TestApp documents round-tripped while 10 kinds hid API documents. Set the subject to the non-default value first (here: patch ExportLevel to API on the working copy) and run both the plain describe output and an edited one, because an elided unchanged write passes a converter that still writes the constant. Carrying at the encoded-bytes level covers gen-typed, newElem-built and hand-serialized writers with one helper, where a gen setter per converter would have needed three mechanisms.", "refs": ["ako/mxcli#816", "ako/mxcli#801", "ako/mxcli#812"], "test": "mdl/backend/modelsdk/issue816_export_level_test.go (TestUpdatePaths_KeepStoredExportLevel, 18 kinds); mdl/roundtrip/export_level_test.go (TestTestAppExportLevelSurvivesRoundTrip, -tags integration)"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-29-describe-page-snippet-exec-studio-pro-page-silently-removes.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-29-describe-page-snippet-exec-studio-pro-page-silently-removes.json new file mode 100644 index 0000000000..47c00aa450 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-29-describe-page-snippet-exec-studio-pro-page-silently-removes.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-09-29", "symptom": "describe page/snippet -> exec on a Studio Pro page silently removes a button's confirmation (ConfirmationInfo -> nil), its progress bar (ProgressBar Blocking/NonBlocking -> None) and progress message, resets a microflow call's Asynchronous/FormValidations, flips DisabledDuringExecution false -> true on actions and on the empty NoAction event slots of every input widget, and deletes OnLeaveAction from check boxes, date pickers and radio button groups; mx check and the build stay green (#721 L2, 18 round-trip allowlist entries)", "cause": "The action settings had no MDL spelling, so clientActionToGen/microflowSettingsToGen wrote constants for all of them (DisabledDuringExecution true, ProgressBar None, null message and confirmation, Asynchronous false, FormValidations All), and three input-widget cases never set OnLeaveAction", "file": "mdl/backend/modelsdk/widget_write.go", "fix": "New `with ( Key: value )` settings list after any page action (grammar actionSettingsV3, visitor validates keys per action type, ast.ActionSettingsV3 -> pages.ActionExecution/FlowCallSettings -> writer); describe prints only non-default settings. NoAction slots have no behaviour to spell, so their DisabledDuringExecution is carried from the stored widget of the same name (carryNoActionExecution in UpdatePage/UpdateSnippet). OnLeaveAction written for CheckBox/DatePicker/RadioButtonGroup. A blank message is a Text with no translations, not en_US ''", "insight": "Split the constants by whether they mean something: a setting with behaviour (a confirmation, a progress bar, disabled-while-running on a real action) gets a spelling so a fresh script can say it too; a value on a slot that runs nothing (DisabledDuringExecution on a NoAction, 946 false vs 380 true on the fixtures) is carried, not spelled. Measure the stored population per slot, not per type: a NoAction under an Action key is true on 20954 of 20984, and majority-false on the input-widget events. The last diff to go was a blank ProgressMessage stored as a Text with zero translations", "refs": ["ako/mxcli#721"], "test": "TestClientActionToGen_MicroflowSettings, TestCarryNoActionExecution, TestWidgetToGen_InputWidgetsWriteOnLeaveAction, TestRenderClientActionMDL_Settings, TestActionSettings_DescribeParsesBackToTheSameAction, TestActionSettings_Refusals; roundtrip allowlists (PedApp 2, TestApp 4 entries struck)"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-30-ako-mxcli-843-rehearsal-m2-under-mdl-1-create-modify.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-30-ako-mxcli-843-rehearsal-m2-under-mdl-1-create-modify.json new file mode 100644 index 0000000000..caa6e17cd1 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-30-ako-mxcli-843-rehearsal-m2-under-mdl-1-create-modify.json @@ -0,0 +1 @@ +{"date": "2026-09-30", "area": "mdl/backend", "symptom": "ako/mxcli#843 (rehearsal M2): under mdl 1, `create or modify nanoflow … returns Boolean as $Done` over a nanoflow stored without a return variable refuses \"the stored document has no ReturnVariableName property … set it in Studio Pro\"; the same statement on a microflow reports \"set: ReturnVariableName\".", "cause": "mfmutator.SetHeader refuses any stated header key the stored document lacks (a key the project version does not declare makes the document unopenable). mxcli's nanoflow writer omits ReturnVariableName when the statement has no `as $Var`, while the microflow writer always writes it on 10+, so only nanoflows hit the refusal.", "fix": "Optional mfmutator.PropertyDeclarer on Deps; the codec deps answer from the metamodel version data (type, then Microflows$MicroflowBase; ReturnVariableName is 10.12+) against the project version, and SetHeader inserts the key after its predecessor in the encoder's order. No answer (MCP, unknown version) keeps the refusal.", "insight": "A refusal keyed on 'the stored document lacks the key' conflates 'this version has no such property' with 'the writer left it out'; the metamodel version data separates the two. Studio Pro 11 stores ReturnVariableName on every nanoflow (PedApp: 13 of 13), so adding it matches what Studio Pro writes.", "issue": "ako/mxcli#843", "file": "mdl/backend/mfmutator/header.go"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-09-30-ako-mxcli-859-review-pr-864-after-built-comparison-landed.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-30-ako-mxcli-859-review-pr-864-after-built-comparison-landed.json new file mode 100644 index 0000000000..c98eaaa2f5 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-09-30-ako-mxcli-859-review-pr-864-after-built-comparison-landed.json @@ -0,0 +1 @@ +{"date": "2026-09-30", "area": "mdl/backend", "symptom": "ako/mxcli#859 review of PR #864: after the built comparison landed, changing or adding `show page M.P with title = 'X'` in a `create or modify microflow` reported \"Unchanged microflow\" and wrote nothing, under mdl 0 and mdl 1 (main spliced it). Nothing warned.", "cause": "builtAsStored compares the declared flow and the stored flow both READ BACK through the codec, so any property the reader drops compares equal whatever either side holds. The ShowFormAction reader never read FormSettings.TitleOverride. Probing encode(built) against encode(readback(built)) over mdl-examples found the reader also dropped ExclusiveSplit/LoopedActivity ErrorHandlingType and a REST call's bound output variable (ResultHandling.ResultVariableName -> RestCallAction.OutputVariable), plus CallWebServiceAction (#861). Before the built comparison such a loss was a visible phantom re-splice; after it, a silently dropped edit.", "fix": "ReadBackMicroflow/ReadBackNanoflow re-encode what they read back and refuse (error -> statement diff, the pre-#859 path) when it is not the document first written, $IDs aside (sameWritten). The reader now reads TitleOverride, the split's and loop's ErrorHandlingType, and a bound REST call's OutputVariable, so those flows keep matching.", "insight": "A comparison made on both sides through the same lossy reader cannot see what the reader loses; the lost property becomes a change that is never written. When equality is decided after a decode, prove the decode lossless for the value at hand (write it again and compare bytes) and fall back when it is not. The probe that found the fields: diff encode(x) with encode(decode(encode(x))) over every mdl-examples flow.", "issue": "ako/mxcli#859", "file": "mdl/backend/modelsdk/microflow_readback.go, mdl/backend/modelsdk/microflow_read_actions.go, mdl/backend/modelsdk/microflow.go, mdl/roundtrip/flow_idempotent_shapes_test.go"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-ako-mxcli-528-mendixlabs-mxcli-293-data-view-footer-could.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-ako-mxcli-528-mendixlabs-mxcli-293-data-view-footer-could.json new file mode 100644 index 0000000000..b49c7962f5 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-ako-mxcli-528-mendixlabs-mxcli-293-data-view-footer-could.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "mdl/backend", "symptom": "ako/mxcli#528 / mendixlabs/mxcli#293: a data view footer could not be edited — `replace footer1 with {…}` (describe's name), the name the script wrote, and `dvMain.footer` all reported not found.", "cause": "A data view footer is a REGION: its widgets live in the data view's FooterWidgets and the footer has no stored Name. The builder hoisted the children and discarded the name; describe invented `footer1`.", "fix": "`.footer` resolves as a region in ResolveAlterTarget/ResolvesTarget; INSERT INTO appends (turning ShowFooter on for an empty footer), REPLACE swaps the content (a describe-style `footer { }` block is unwrapped), DROP empties it; a not-found names the footer addresses. A name on a data view footer is MDL-DEPR005; describe prints `footer { }`. REPLACE may also reuse the names of the widgets it removes (ContainedWidgetNames).", "insight": "Same shape as scroll-container regions: a slot without a name is addressed positionally by its owner. Check the grammar's own doc comment too — it advertised `replace footer1`, a form that never resolved.", "issue": "ako/mxcli#528", "file": "mdl/backend/pagemutator/footerregion.go"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-ako-mxcli-627-fluent-api-s-attributemodifier-apply-backend.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-ako-mxcli-627-fluent-api-s-attributemodifier-apply-backend.json new file mode 100644 index 0000000000..2b3f1a20df --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-ako-mxcli-627-fluent-api-s-attributemodifier-apply-backend.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "mdl/backend", "symptom": "ako/mxcli#627: the fluent API's AttributeModifier.Apply() (Backend.UpdateAttribute) on a Studio Pro-authored attribute is refused with \"refusing to write unit …: 1 element(s) kept their $ID but would be written with a different GUID (DomainModels$Attribute)\"; without the #1119 guard it would re-mint the GUID and drop the column on the next deploy.", "cause": "UpdateAttribute rebuilds the attribute with attributeToGen (raw == nil) and carried only the $ID, so the codec's EmitGUID default wrote GUID = $ID. No MDL statement reaches it — every ALTER ENTITY form goes through UpdateEntity, which has the carry — so it was found by enumerating the converter's call sites, not by a repro.", "fix": "UpdateAttribute carries the stored attribute's raw bytes and export level onto the rebuild via carryStoredAttribute, the helper now shared with carryAttributeIdentity. Attribute -> Attribute keeps $Type, so SetRaw suffices.", "insight": "Test on PedApp (GUID != $ID); an mxcli-created attribute re-mints the same value and cannot fail. The unfixed code fails the test via the write guard's refusal, which is the observable symptom on main.", "issue": "ako/mxcli#627", "file": "mdl/backend/modelsdk/domainmodel_alter.go"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-ako-mxcli-628-move-entity-parent-b-move-entity-child-b-both.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-ako-mxcli-628-move-entity-parent-b-move-entity-child-b-both.json new file mode 100644 index 0000000000..ef3f8c1b89 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-ako-mxcli-628-move-entity-parent-b-move-entity-child-b-both.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "mdl/backend", "symptom": "ako/mxcli#628: `move entity A.Parent to B; move entity A.Child to B;` both report success, then Studio Pro cannot open the project: System.AggregateException: The given key '' was not present in the dictionary.", "cause": "MoveEntity scanned only sourceDM.AssociationsItems(); an existing cross-association (created by the first move) was invisible, so moving its FROM entity left its ParentPointer naming an element absent from the unit, and moving its TO entity left Child naming the old module. Cross-unit damage, so the #1119 write guard cannot see it.", "fix": "association_move_cross.go: own cross-associations (FROM = moved entity) travel to the target, or become a plain DomainModels$Association there when the TO entity is already in it (raw transform: $Type, Child -> ChildPointer binary id, connection points added, GUID passed through); a cross-association in the target naming the entity becomes plain; any other module's cross-association naming it is re-pointed. MovedAssociation.SameModule lets the executor report the plain conversions apart.", "insight": "Assert on the stored documents (every ParentPointer/ChildPointer resolves in its own unit, every cross Child resolves to an entity in the named module); mx check is a weak signal for this class. The four sequences (to-then-from, from-then-to, travel, re-point) are distinct shapes, not one.", "issue": "ako/mxcli#628", "file": "mdl/backend/modelsdk/association_move_cross.go"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-1214-alter-page-set-editable-true-lvrows.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-1214-alter-page-set-editable-true-lvrows.json new file mode 100644 index 0000000000..2b7a064ef1 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-1214-alter-page-set-editable-true-lvrows.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "mdl/backend", "symptom": "mendixlabs/mxcli#1214: `alter page … { set Editable = true on lvRows }` printed \"Altered page\" and the list view stayed Editable = false, so its inputs rendered read-only.", "cause": "pagemutator's `editable` case wrote the value only when it was a string (the input-widget Always/Never enum). A list view's and data view's Editable is a BOOLEAN, so `true` fell through and returned nil — success with nothing stored. A lower-case string ('never') was stored verbatim.", "fix": "setWidgetEditableMut: the stored value's type decides the vocabulary (bool → true/false; string → canonical Always/Never, Conditional only via [expr]); anything else, or a widget with no Editable, is refused. EditableIf now also sets the Conditional enum, as CREATE does.", "insight": "A setter switch that type-asserts and returns nil on a miss is a silent no-op generator; read the STORED field's type first, it says which vocabulary the property has on this widget.", "issue": "mendixlabs/mxcli#1214", "file": "mdl/backend/pagemutator/mutator.go"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-1247-alter-page-replace-combobox12-combobox.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-1247-alter-page-replace-combobox12-combobox.json new file mode 100644 index 0000000000..ff46501d8b --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-1247-alter-page-replace-combobox12-combobox.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "mdl/backend", "symptom": "mendixlabs/mxcli#1247: `alter page … replace comboBox12 with { combobox … }` to add a sort reset the combo box's translated placeholder and readOnlyStyle; on TestApp's Studio Pro-authored Rules.BusinessRule_NewEdit it also turned the stored Editable \"Never\" into \"Always\" and an expression property's PrimitiveValue \"\" into \"false\". exec reported success, mx check stayed green.", "cause": "A pluggable widget is rebuilt from its template plus the MDL mappings, so every property MDL cannot state comes back at the template value. #830's passthrough is all-or-nothing and only for unchanged widgets.", "fix": "For one pluggable widget replaced by one of the same package, the executor also builds the STORED widget as describe prints it (baseline); pagemutator.mergeUnstatedPluggable keeps the stored Type and every stored property/field on which replacement and baseline agree, and grafts the differing values with their TypePointers re-aimed at the stored Type by key path. Anything that cannot be lined up falls back to the plain replace.", "insight": "'What the statement states' is measured by building the description of the stored widget beside the statement, not by listing mapped keys: a property that comes out equal from both builds is the template's, not the author's.", "issue": "mendixlabs/mxcli#1247", "file": "mdl/backend/pagemutator/pluggable_replace.go"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-1247-datagrid-2-restating-testapp-s-studio.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-1247-datagrid-2-restating-testapp-s-studio.json new file mode 100644 index 0000000000..0244d9088e --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-1247-datagrid-2-restating-testapp-s-studio.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "mdl/backend", "symptom": "mendixlabs/mxcli#1247 on a DataGrid 2: restating TestApp's Studio Pro-authored dataGrid2_1 with one column caption changed silently turned itemSelectionMethod \"rowClick\" into \"checkbox\", reset onClickTrigger and rebuilt every column's and filter's unmapped properties — the combo-box fix's merge did not apply and REPLACE fell back to the template rebuild.", "cause": "remapTypePointers re-aimed every TypePointer in the grafted columns value, including those of the nested filter widgets, which point into their OWN Type; no counterpart -> merge refused -> plain replace. And even when it applied, the columns list was grafted whole, so one changed caption reset every column's unstated properties.", "fix": "remapTypePointers leaves a nested CustomWidgets$CustomWidget as built; an object-list property whose objects line up one for one with the baseline's is merged object by object (properties indexed by their property type's path), so only the stated property of the stated column changes.", "insight": "A fallback that restores the old behaviour is the old bug: test the merge on the most nested pluggable widget available (DataGrid 2 with filters), not only on a flat one, and count the fallbacks.", "issue": "mendixlabs/mxcli#1247", "file": "mdl/backend/pagemutator/pluggable_replace.go"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-698-over-mcp-call-microflow-error-rollback.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-698-over-mcp-call-microflow-error-rollback.json new file mode 100644 index 0000000000..58f357a03f --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-01-mendixlabs-mxcli-698-over-mcp-call-microflow-error-rollback.json @@ -0,0 +1 @@ +{"date": "2026-10-01", "area": "mdl/backend", "symptom": "mendixlabs/mxcli#698: over --mcp, `call microflow … on error rollback|continue` was created with PED's default error handling; a custom handler's error edge was written as an ordinary sequence flow. No error either way.", "cause": "mapMicroflowAction built every action map without errorHandlingType (only notify carried it), and buildFlowDocContent wrote every flow alike — PED's SequenceFlow constructor has no property for an error-handler flow (ped_get_schema, Studio Pro 11.14).", "file": "mdl/backend/mcp/microflow.go (mapObjectTree, buildFlowDocContent)", "fix": "carryErrorHandlingType writes the action's ErrorHandlingType (read by reflection); Custom/CustomWithoutRollBack and IsErrorHandler flows are refused with a message to run without --mcp.", "test": "TestMapObjectTree_CarriesErrorHandlingType, TestMapObjectTree_RefusesACustomErrorHandler", "insight": "ped_get_schema answers 'can PED express this' in one call; check it before mapping a property, and refuse what it cannot hold."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-alter-page-set-caption-tabpage2-fails-widget-tabpage2-not.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-alter-page-set-caption-tabpage2-fails-widget-tabpage2-not.json new file mode 100644 index 0000000000..d290f5ba09 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-alter-page-set-caption-tabpage2-fails-widget-tabpage2-not.json @@ -0,0 +1 @@ +{"date": "2026-10-02", "area": "mdl/backend", "symptom": "alter page … set (Caption = '…') on tabPage2 fails with `widget \"tabPage2\" not found` although describe page prints `tabpage tabPage2`; a tab caption missing its default-language translation (mxbuild CE4899 after switching DefaultLanguageCode) could only be fixed by re-creating the whole page.", "cause": "findInWidgetChildren (and its twins findNearestDSInChildren and collectWidgetScopeInChildren) walked TabPages[] only to descend into each page's Widgets[], never matching the Forms$TabPage's own Name — a tab page is a named element that lives in a list that is not a widget list.", "fix": "Walk TabPages through findInWidgetArray / findNearestDSInWidgets / collectWidgetScope like any named list; INSERT INTO a tab page appends to its Widgets, and INSERT BEFORE/AFTER, REPLACE and DROP on a tab page are refused (refuseTabPageSiblingEdit) because they would write widgets into TabPages or orphan the control's DefaultPagePointer.", "insight": "Resolving a new kind of node also hands it to every structural op that trusts parentKey/parentArr: making X addressable means deciding, per op, what writing into X's parent list does.", "file": "mdl/backend/pagemutator/mutator.go", "issue": ""} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-create-association-x-from-e-b-f-delete-restrict-entity.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-create-association-x-from-e-b-f-delete-restrict-entity.json new file mode 100644 index 0000000000..60a9d6a47b --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-create-association-x-from-e-b-f-delete-restrict-entity.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-10-02", "issue": "ako/mxcli#803", "symptom": "`create association A.X from A.E to B.F on delete restrict` (TO entity in another module) stores ChildDeleteBehavior DeleteMeIfNoReferences with a null ChildErrorMessage — the shape that stops the runtime starting (CapTrackV2 §1). The same statement within one module, and ALTER on the cross-module one, were fine.", "cause": "crossAssocToGen built the delete behaviour without the restrict message; #795 added patchCrossDeleteErrorMessage only at one call site (patchCrossAssociations' new-element arm), not in the converter, so CreateCrossAssociation still wrote null.", "file": "`mdl/backend/modelsdk/association_move_write.go` (crossAssocToGen)", "fix": "Call patchCrossDeleteErrorMessage inside crossAssocToGen, as assocToGen does inline; drop the now-redundant call site.", "insight": "A property fix belongs in the converter, not at the call site that was reported — enumerate the converter's callers. Test: mdl/backend/modelsdk/issue803_cross_assoc_restrict_test.go, keep behaviour as the null control."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-create-entity-over-mcp-stores-every-attribute-string-200.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-create-entity-over-mcp-stores-every-attribute-string-200.json new file mode 100644 index 0000000000..ca72102915 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-create-entity-over-mcp-stores-every-attribute-string-200.json @@ -0,0 +1 @@ +{"date": "2026-10-02", "area": "mdl/backend", "symptom": "create entity over --mcp stores every attribute as String(200) on both Studio Pro 11.14 and 11.15 MCP servers; String lengths never written (also on alter entity add attribute).", "cause": "pedAttribute sent \"$Type\":\"DomainModels$Attribute\" inside the DomainModels$Entity constructor, whose attributes are PlainObject<{name,type,enumerationName}>; with $Type present PED ignores `type` and still answers SUCCESS and a clean check. Neither attribute constructor has a length property.", "fix": "buildEntityValue clears $Type on nested attributes (omitempty); applyAttributeLengths sets /entities/N/attributes/M/type/length after create and after add for every String not at the schema default 200.", "insight": "The same element has two constructor shapes depending on where it is added (nested PlainObject vs standalone constructor) \u2014 read ped_get_schema for the container, not the element. A clean ped_check_errors proves nothing about a dropped property; only a read-back of each type caught it.", "file": "mdl/backend/mcp/domainmodel.go", "issue": "ako/mxcli#923"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-create-workflow-display-x-over-mcp-stores-document-name.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-create-workflow-display-x-over-mcp-stores-document-name.json new file mode 100644 index 0000000000..8fbdebe999 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-create-workflow-display-x-over-mcp-stores-document-name.json @@ -0,0 +1 @@ +{"date": "2026-10-02", "area": "mdl/backend", "symptom": "create workflow \u2026 display 'X' over --mcp stores the document name as the workflow title and workflowName (Studio Pro 11.14 and 11.15).", "cause": "The context-shaped Workflows$Workflow constructor ignores the title (11.14 'caption', 11.15 'title') and workflowName it is given; 11.15 also renamed caption->title, which its permissive schema would have dropped silently anyway.", "fix": "workflowConstructorTakesContext's probe records the title key (workflowCtorTitle); workflowCreateLeafOps sets /title and /workflowName/text after the create (set ops both releases accept).", "insight": "Read back what a constructor stored instead of trusting SUCCESS: both releases accepted the key and applied nothing.", "file": "mdl/backend/mcp/workflow.go", "issue": ""} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-getput-describe-exec-combo-box-editable-expr-rewrites.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-getput-describe-exec-combo-box-editable-expr-rewrites.json new file mode 100644 index 0000000000..4c848908e3 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-getput-describe-exec-combo-box-editable-expr-rewrites.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-10-02", "symptom": "getput: describe → exec of a combo box with `Editable: ` rewrites ConditionalEditabilitySettings/Conditions[0]: 2 -> 3", "cause": "Forms$ConditionalEditabilitySettings registered Conditions as a plain mandatory list (default marker 3); Studio Pro writes [2], as for visibility", "file": "`mdl/backend/modelsdk/widget_write.go` (RegisterTypeDefaults)", "insight": "measured on both editability settings in TestApp; the ALTER path (pagemutator setWidgetConditionalSettingMut) still hand-writes [3] for both settings", "refs": ["#721"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-project-whose-default-language-de-de-alter-page-set-caption.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-project-whose-default-language-de-de-alter-page-set-caption.json new file mode 100644 index 0000000000..25cd42d940 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-project-whose-default-language-de-de-alter-page-set-caption.json @@ -0,0 +1 @@ +{"date": "2026-10-02", "area": "mdl/backend", "symptom": "On a project whose default language is de_DE, alter page … set Caption on a tab page (and set Label on any Studio Pro 11 input widget) reports `Altered page` and stores nothing; set Caption on a button writes the German text over the first translation (en_US); set Title overwrites every language with the same string.", "cause": "Four text writers with four rules: setTranslatableText looked for a `Translations` key no Mendix document has, then DSet a `Text` field DSet cannot add (silent no-op on every real Texts$Text); setWidgetLabelMut read only the legacy `Label` key while 10+/11 store LabelTemplate (a Forms$ClientTemplate); setClientTemplateText wrote Items[0] whatever its language; updateTextsTextValue/updateClientTemplateText wrote all translations. A unit test built the fictional `Translations` shape and asserted nothing, so it stayed green.", "fix": "One helper, setTextsTextTranslation: update or append the Texts$Translation for model.AuthoringLanguage() (the project default DESCRIBE shows), keep other languages; setTranslatableText dispatches on $Type (Texts$Text / Forms$ClientTemplate) and refuses anything else; setWidgetLabelMut tries LabelTemplate first; execAlterPage resolves the authoring language before the first mutation.", "insight": "A text setter's test must build the shape read back from a Studio Pro-authored page (bson dump --format ndsl) and assert the stored translation per language; `err == nil` proves nothing for a setter whose miss path is DSet returning false. Verified live: de_DE switch → CE4899 on tabPage2, set Caption → de_DE added, en_US/nl_NL/ar_DZ kept, 0 errors.", "file": "mdl/backend/pagemutator/mutator.go", "issue": ""} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-studio-pro-11-15-over-mcp-describe-entity-edited-session.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-studio-pro-11-15-over-mcp-describe-entity-edited-session.json new file mode 100644 index 0000000000..9659c56ad3 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-studio-pro-11-15-over-mcp-describe-entity-edited-session.json @@ -0,0 +1 @@ +{"date": "2026-10-02", "area": "mdl/backend", "symptom": "Studio Pro 11.15 over --mcp: DESCRIBE of an entity edited this session shows String(unlimited) for String(200) attributes; reconstructed associations have empty type/owner.", "cause": "11.15's ped_read_document omits every property equal to its schema default (StringAttributeType.length=200, Association.type=Reference/owner=Default, NoGeneralization.persistable=true, BooleanConditionOutcome.value=false); readers mapped absent to Go's zero value, and length 0 means unlimited.", "fix": "attributeTypeFromPED reads length as *int and defaults absent to 200; reconstructAssociations defaults absent type/owner to Reference/Default.", "insight": "Diff the READ output of the same element across releases, not just tool schemas: this change is in one sentence of ped_read_document's description. Any new PED reader must treat an absent property as the schema default (ped_get_schema kind:element shows '= default').", "file": "mdl/backend/mcp/read_router.go", "issue": ""} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-studio-pro-11-15-over-mcp-every-write-module-fails.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-studio-pro-11-15-over-mcp-every-write-module-fails.json new file mode 100644 index 0000000000..61ccda5a4a --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-studio-pro-11-15-over-mcp-every-write-module-fails.json @@ -0,0 +1 @@ +{"date": "2026-10-02", "area": "mdl/backend", "symptom": "Studio Pro 11.15 over --mcp: every write in a module fails 'validation failed for ' with another document's error (e.g. an empty enumeration elsewhere); a nonexistent document checks 'No errors found.'", "cause": "11.15 replaced ped_check_errors' documents[] with filters{documentType,documentNamePrefix}+pagination and made all input schemas additionalProperties-permissive, so the old argument is accepted and IGNORED: the check ran project-wide. The answer became a paged listing ('Listing problems a-b (out of n). Check ID: k', then 'Name' (Type): unit headers).", "fix": "checkDocumentNow (mdl/backend/mcp/check_errors.go) sends filters when SupportsToolArg('ped_check_errors','filters'), fetches every page repeating the filters (a page without them lists the whole project), re-runs on a stale window, and keeps only problems under the exact 'Name' (Type): header (documentNamePrefix is a prefix). An unscoped listing from the fallback form is scoped the same way.", "insight": "A permissive schema turns an argument rename into a silent no-op, so 'the call succeeded' proves nothing; the live control needs a deliberately broken document AND a clean one checked in the same run (TestLive_CheckErrorsIsScoped). Gate on the newer shape's argument, never on serverInfo.version.", "file": "mdl/backend/mcp/check_errors.go", "issue": ""} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-testlive-entityaccessrulereject-fails-every-live-studio-pro.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-testlive-entityaccessrulereject-fails-every-live-studio-pro.json new file mode 100644 index 0000000000..589b5b8257 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-testlive-entityaccessrulereject-fails-every-live-studio-pro.json @@ -0,0 +1 @@ +{"date": "2026-10-02", "area": "mdl/backend", "symptom": "TestLive_EntityAccessRuleReject fails on every live Studio Pro MCP server (entityIndex: not found) \u2014 its fixture module ExpenseApproval exists in no test project.", "cause": "The live test read a hand-made fixture (module/entity/role) from one developer's project via env defaults instead of building it.", "fix": "The test creates its own entity Zz_R12_AccessFixture_ in MXCLI_MCP_MODULE, grants MXCLI_MCP_ROLE (default .User) once, then asserts the second grant is rejected with the rule count unchanged; skips with the role to set when the role does not exist (module roles cannot be created over MCP).", "insight": "A live test that depends on state it does not create fails as an environment problem on every other machine, so its real failures read as noise; build the fixture, and skip only on what MCP cannot author.", "file": "mdl/backend/mcp/security_test.go", "issue": "ako/mxcli#924"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-view-entity-s-oql-document-studio-pro-excluded-re-included.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-view-entity-s-oql-document-studio-pro-excluded-re-included.json new file mode 100644 index 0000000000..d68da699cf --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-02-view-entity-s-oql-document-studio-pro-excluded-re-included.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-10-02", "symptom": "A view entity's OQL document that Studio Pro excluded is re-included by any CREATE OR MODIFY of the view entity that changes its query.", "cause": "encodeViewEntitySourceDocument writes Excluded=false as a constant, and WriteViewEntitySourceDocument's update path carried only the stored ExportLevel (#816), not Excluded.", "file": "`mdl/backend/modelsdk/move_view_write.go` (WriteViewEntitySourceDocument), `export_level_carry.go` (keepStoredExcluded)", "insight": "A shared create/update encoder that writes model state as a constant needs a carry on the update path for every such key, not just the one a previous bug named (#816 ExportLevel, #914 Excluded).", "refs": ["ako/mxcli#827", "ako/mxcli#914"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-04-ako-mxcli-980-create-modify-navigation-native-profile.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-04-ako-mxcli-980-create-modify-navigation-native-profile.json new file mode 100644 index 0000000000..5df347c24a --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-04-ako-mxcli-980-create-modify-navigation-native-profile.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-10-04", "symptom": "ako/mxcli#980: create or modify navigation on a native profile ignored its menu block, sync block, login/not-found page and on-sync-error while reporting 'updated'; describe printed a native nanoflow home as `home microflow` and bottom bar items without action or icon", "cause": "navPatchNativeProfile only patched home pages; nativeNavProfileFromGen read BottomBarItem.Page only; the grammar had no nanoflow home", "file": "`mdl/backend/modelsdk/navigation_write.go` navPatchNativeProfile, `navigation_read.go` nativeNavProfileFromGen, `mdl/executor/cmd_navigation.go` checkProfileClauses", "insight": "A profile-kind-specific writer must refuse every clause it does not apply (checked in the executor before anything is written, including profile creation), and describe must not print what exec would refuse — list it as comments. No native profile exists in any local fixture, so the reader is pinned with a hand-built gen document", "refs": ["#980"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-04-ako-mxcli-980-found-adding-navigation-profiles-testapp-round.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-04-ako-mxcli-980-found-adding-navigation-profiles-testapp-round.json new file mode 100644 index 0000000000..16ae218b8f --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-04-ako-mxcli-980-found-adding-navigation-profiles-testapp-round.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-10-04", "symptom": "ako/mxcli#980 (found adding navigation profiles to the TestApp round-trip harness): describe -> exec of an offline profile rewrote every multi-line sync constraint as the one-line text describe prints", "cause": "navOfflineConfigs wrote the spec's constraint verbatim; describe folds Studio Pro's multi-line layout (singleLine) and the visitor re-normalises it, so the text never matches the stored bytes", "file": "`mdl/backend/modelsdk/navigation_write.go` navOfflineConfigs / xpathWithoutLayout", "insight": "When describe deliberately changes layout, the writer must carry the stored layout for an unchanged value; compare with whitespace outside string literals removed (inside a literal it is part of the value — that is the control)", "refs": ["#980"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-05-mendix-11-15-testmappingfixtureroundtrip-fails-blank-app-s.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-05-mendix-11-15-testmappingfixtureroundtrip-fails-blank-app-s.json new file mode 100644 index 0000000000..e278a68746 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-05-mendix-11-15-testmappingfixtureroundtrip-fails-blank-app-s.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-10-05", "symptom": "On Mendix 11.15 `TestMappingFixtureRoundTrip` fails for the blank app's own FeedbackModule mappings: describe -> exec adds `\"MessageDefinition\": \"\"`; and a mapping `with message definition` built on 11.15 fails `mx check` with CE0270 \"No root element could be found in the schema\"", "cause": "Mendix 11.15 REMOVED the mapping's `MessageDefinition` key (message definitions became `MessageDefinitions$MessageDefinition2` documents; the source moved to `MessageDefinition2` as `Module.MessageName`, measured with `mx convert` 11.15.0). The writer added the key unconditionally and the executor put the source in it, which 11.15 no longer reads", "file": "`model.ImportMapping`/`ExportMapping` (`MessageDefinition *string`, `MessageDefinitionSource()`), `mdl/backend/modelsdk/mapping_read.go` (`optionalStringFromRaw`) + `mapping_write.go`, `mdl/executor/mapping_messagedefinition.go` (`mappingMessageDefinitionKeys`, `messageDefinitionsAreDocuments`), `sdk/versions/mendix-1{0,1}.yaml` (`integration.message_definition_collection` max 11.14.99, `message_definition_document` min 11.15.0)", "insight": "**A key can be REMOVED by a version, not only introduced** \u2014 the #279 carry rule applies in both directions, so `MessageDefinition` is now a pointer like `MessageDefinition2`. The source key follows the stored document's shape on an update (a pre-11.15 document transplanted into an 11.15 project keeps its source in `MessageDefinition`, which is what kept the fixture's AgentCore/Email_Connector mappings green) and the project version only on a create. Caught by the new 11.15 nightly job, not by any fixture: none of the pinned documents was 11.15-shaped until the blank 11.15 app's own FeedbackModule mappings appeared in the base project", "refs": ["ako/mxcli#987", "ako/mxcli#279"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-06-describe-microflow-prints-unsupported-action-microflows.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-06-describe-microflow-prints-unsupported-action-microflows.json new file mode 100644 index 0000000000..14390c8fdd --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-06-describe-microflow-prints-unsupported-action-microflows.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-10-06", "symptom": "`describe microflow` prints `-- Unsupported action: Microflows$SendEmailAction` for the Studio Pro 11.13+ Send Email activity, and MDL has no statement to create it", "cause": "No reader case, and no grammar/AST/builder/writer. The vendored gen type `genMf.SendEmailAction`/`EmailMessage` predates 11.12: it binds Subject/MessageBody* as expressions, but 11.13 deleted those and stores SubjectTemplate/MessageBodyPlainTextTemplate/MessageBodyHtmlTemplate (Microflows$StringTemplate) plus a CustomHeaders list the gen type has no field for", "file": "mdl/backend/modelsdk/microflow_send_email.go", "insight": "The gen type existing (the issue cited it as 'the storage side seems known') was a false lead: it is the pre-11.12 shape, so both directions are raw-keyed against a Studio Pro-saved document (ako/TestApp Email.EmailMF) and a key-for-key shape test (TestSendEmailActionToGen_MatchesStudioProShape) pins types and markers — it caught ConnectionTimeout being int64 and the empty CustomHeaders list needing marker 3. Version facts come from mendixmodelsdk 4.116 StructureVersionInfo (templates 11.12.0, plain Subject deleted 11.13.0), hence the 11.13.0 gate. Run negative controls through mxbuild before writing check rules: of the plausible rules, 'check server identity needs SSL' and the header-name rule are ACCEPTED by mxbuild 11.15.0-rc.4 (so warnings), while type faults are CE9528 for address/host/port/user (not the CE0117 exprcheck's E009 used to name — slots can now carry their own Mxbuild code). Attachment is a bare variable name (CE0109 on a wrong one). Shapes MDL cannot restate (auth document, pre-11.13 expression subject, non-SMTP) stay UnsupportedAction. Studio Pro's Test Email tab (TestEmailMessage) is written empty on rewrite, by design. An unedited describe->exec round trip is elided as Unchanged, so it cannot demonstrate loss; author-from-script is the meaningful control (old binary: `mismatched input 'email' expecting REST`).", "refs": ["mendixlabs/mxcli#1315"], "ce": ["CE9528", "CE0117", "CE0720", "CE0109"], "rules": ["MDL-EMAIL01", "MDL-EMAIL02", "MDL-EMAIL03", "E009"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-06-opening-change-studio-pro-s-changes-panel-throws-system.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-06-opening-change-studio-pro-s-changes-panel-throws-system.json new file mode 100644 index 0000000000..44d3505a1a --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-06-opening-change-studio-pro-s-changes-panel-throws-system.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-10-06", "symptom": "Opening a change in Studio Pro's Changes panel throws `System.InvalidOperationException: Objects with ID … of type Forms$MicroflowParameterMapping do not have the same properties. baseNames = Expression, Parameter, Variable; newNames = Parameter, Expression` after `alter page … set ('onClickAction': call microflow M.F(P = $P)) on w`. `mx check` is 0 errors; the page opens. Second, silent half: `$P` (a page parameter) is stored as the Expression \"$P\", which Studio Pro does not bind (CE1571, the #1140 form)", "cause": "(1) `bindParameterMappingValue` sets only Expression for an Expression-bound argument; the gen `Variable` Part stays unset and the encoder omits an unset Part on a new element, so the key vanished. Studio Pro writes all three keys, nulling the unused slot, and its merge library compares key sets per $ID — the canon $ID carry makes the new mapping the SAME object as the stored one, so the diff sees two shapes of one element. (2) `convertASTAction` (ALTER PAGE SET Action and named action slots) built its pageBuilder with no paramScope/localVariables, so `classifyFlowArgValue` could not recognise a page/snippet parameter", "file": "`mdl/backend/modelsdk/widget_write.go` (RegisterTypeDefaults NullFields Variable on Forms$MicroflowParameterMapping + Forms$NanoflowParameterMapping), `mdl/executor/cmd_alter_page.go` (convertASTAction seeds ParamScope, storedPageVariables, isSnippet from the mutator)", "insight": "**`mx diff base.mpr new.mpr out.mpr` reproduces Changes-panel crashes headless** — it runs the same MergeLib DifferenceComputer and prints the identical \"do not have the same properties\" line (exit 129). Base = a copy of the project before the write, made by the FIXED build so it has Studio Pro's key set. That turns a Windows-GUI-only symptom into a two-binary A/B on Linux. **The base must share the $ID** — the crash needs a paired object, which the identity carry supplies; a fresh element is only an add and never trips it, so a create-only repro passes on broken code. **Register the null slot, don't set it at the call site**: `Part.Set(nil)` still encodes as omitted, and there are three construction sites (action, nanoflow action, nanoflow data source) — NullFields covers all and any future one. **A builder constructed outside CREATE PAGE is the recurring hole for #1140-style classification**: INSERT/REPLACE already seeded the scope from the mutator, SET did not; grep for `&pageBuilder{` and check each seeds paramScope + localVariables + isSnippet. Measured on mxbuild 11.12.2: buggy mx diff exit 129 with the reported line, fixed exit 0; mx check 0 errors on both", "refs": ["mendixlabs/mxcli#1317", "mendixlabs/mxcli#1180", "mendixlabs/mxcli#1140"], "ce": ["CE1571"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-07-after-rename-java-action-m-ja-old-ja-new-mxcli-rename-java.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-07-after-rename-java-action-m-ja-old-ja-new-mxcli-rename-java.json new file mode 100644 index 0000000000..a3fb003c4d --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-07-after-rename-java-action-m-ja-old-ja-new-mxcli-rename-java.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-10-07", "symptom": "After `RENAME JAVA ACTION M.JA_Old TO JA_New` (or `mxcli rename java-action`), javasource/m/actions/JA_New.java still declares `public class JA_Old`, its constructor `public JA_Old(` and toString `return \"JA_Old\";` \u2014 javac: `class JA_Old is public, should be declared in a file named JA_Old.java`. `mxcli docker build` reports BUILD SUCCEEDED regardless", "cause": "Backend.RenameJavaSourceFile (mdl/backend/modelsdk/java_write.go) only os.Rename'd the file; nothing rewrote the generated parts that carry the action's name", "file": "mdl/backend/modelsdk/java_write.go, sdk/javaactions/rename.go", "insight": "A green mxbuild does NOT prove the javasource on disk compiles: mxbuild regenerates action stubs in the copy it builds (class, constructor, toString follow the model; user code, extra code and the import list are kept byte-for-byte, a mention of the old name inside them included), so the defect only shows where the on-disk file is compiled directly \u2014 `run --local --watch` hot reload, IDEs. The cheap probe is javac on the one file with runtime/bundles/com.mendix.public-api.jar on the classpath. The oracle for the rewrite is mxbuild itself: run the mxbuild binary on the project IN PLACE (not `docker build`, which uses a temp copy) and diff the javasource file \u2014 it changed exactly three lines; that pair is now testdata/mxbuild/JA_Renamed*. Rewrite those three spots, not a full GenerateSource, which would reformat a Studio Pro-authored file. Control: unfixed binary \u2192 javac error above; fixed \u2192 javac exit 0, docker build BUILD SUCCEEDED", "refs": ["follow-up to mendixlabs/mxcli#1318"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-07-alter-page-insert-after-tpone-tabpage-tptwo-insert-into.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-07-alter-page-insert-after-tpone-tabpage-tptwo-insert-into.json new file mode 100644 index 0000000000..6a3bedc3b6 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-07-alter-page-insert-after-tpone-tabpage-tptwo-insert-into.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "symptom": "alter page … { insert after tpOne { tabpage tpTwo … } } and insert into tabsMain { tabpage … } both fail: `failed to insert: failed to build widgets: failed to build widget tpTwo: tabpage must be a direct child of tabcontainer`; the only route to add a tab was rewriting the page with create or modify", "cause": "applyInsertWidgetMutator built each inserted node with buildWidgetV3, which refuses a lone tabpage (it is only built as a tabcontainer child); and even past the builder, InsertWidget refuses tab-page siblings by design (refuseTabPageSiblingEdit) because a Forms$TabPage is not a widget and lives in the control's TabPages list", "file": "mdl/executor/cmd_alter_page.go (applyInsertWidgetMutator, buildTabPagesFromAST); mdl/backend/pagemutator/tabpages.go (InsertTabPages)", "insight": "A non-widget child (list view template, DataGrid2 column, tab page) needs its own PageMutator method, not a relaxed check in InsertWidget: route by the node type in the executor before the generic builder runs, and let the mutator decide by the TARGET's $Type whether INTO (the container) or BEFORE/AFTER (a sibling of the same kind) is meant. Serialize by wrapping the new pages in a throwaway container and taking its child list back out — no new Deps method, and the shape is exactly CREATE PAGE's. Control that makes `mx check` meaningful: a Forms$TabPage placed in a tab page's Widgets makes mx unable to load the project (\"TabPage does not contain a constructor with a parameter of type TabPage\"), while the TabPages splice checks 0 errors on 11.14.0. Keep the control's DefaultPagePointer on insert; set it only when the control had no pages.", "refs": ["mendixlabs/mxcli#1215"], "date": "2026-10-07"} diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-10-08-set-caption-wrote-dutch-restating-the-snippet-deleted-the-other.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-08-set-caption-wrote-dutch-restating-the-snippet-deleted-the-other.json new file mode 100644 index 0000000000..3e553c6a1c --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-10-08-set-caption-wrote-dutch-restating-the-snippet-deleted-the-other.json @@ -0,0 +1 @@ +{"date": "2026-10-08", "area": "mdl/backend", "symptom": "mendixlabs/mxcli#1182 (v0.23.0, Mendix 11.12.2): \"SET Caption wrote Dutch, and restating the snippet deleted the other languages\" — alter snippet … set (Caption: …) on a button whose caption stores nl_NL before en_US (default en_US) wrote the English into nl_NL; create or replace snippet with new English and an inserted widget dropped nl_NL", "cause": "Already fixed on main before the report was triaged: SET by 33249a36 (setTextsTextTranslation writes model.AuthoringLanguage's entry instead of the first listed one), restate by CarryTranslations' owning-element pairing (#705). Neither had a test with the default language listed SECOND, or with source and shape changed together, so the reported shape was unpinned", "file": "mdl/backend/pagemutator/text_language_test.go (TestSetCaption_DefaultLanguageListedSecond); modelsdk/canon/translations_anchor_test.go (TestCarryTranslations_ChangedSourceAndShapeKeepTheOtherLanguages)", "insight": "Check the reported version before writing a fix: `git fetch --depth=1 refs/tags/vX:refs/tags/up-vX`, build it in a worktree, and run the same script against both binaries — that turned a vague report into two exact before/after BSON dumps in minutes. The nl_NL-first shape is built in MDL by authoring under a nl_NL default, adding en_US with `create or modify translations … for en_US`, then switching the default (translations INTO the default are refused). Tests whose default language is absent or listed first cannot distinguish 'write the default' from 'write Items[0]'; put the default second. v0.23.0's old MDL dialect (no `mdl 1;`, `set Caption = …`) needs its own copy of the script.", "refs": ["mendixlabs/mxcli#1182", "mendixlabs/mxcli#970", "ako/mxcli#705"]} diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl deleted file mode 100644 index c4b36932f1..0000000000 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ /dev/null @@ -1,892 +0,0 @@ -{"area": "mdl/executor", "date": "2026-07-31", "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", "date": "2026-07-30", "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", "date": "2026-07-30", "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", "date": "2026-07-30", "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", "date": "2026-07-30", "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", "date": "2026-06-21", "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", "date": "2026-04-21", "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", "date": "2026-04-21", "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", "date": "2026-07-29", "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", "date": "2026-07-29", "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", "date": "2026-07-30", "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", "date": "2026-07-30", "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", "date": "2026-07-30", "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", "date": "2026-07-30", "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", "date": "2026-07-30", "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", "date": "2026-07-30", "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", "date": "2026-07-30", "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", "date": "2026-07-24", "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", "date": "2026-04-21", "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", "date": "2026-05-07", "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", "date": "2026-05-08", "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", "date": "2026-05-08", "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", "date": "2026-07-03", "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", "date": "2026-07-03", "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", "date": "2026-05-15", "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", "date": "2026-06-20", "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", "date": "2026-05-15", "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", "date": "2026-07-02", "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", "date": "2026-06-04", "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", "date": "2026-06-18", "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", "date": "2026-06-20", "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", "date": "2026-06-18", "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", "date": "2026-07-02", "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", "date": "2026-07-02", "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", "date": "2026-06-04", "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", "date": "2026-06-04", "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", "date": "2026-06-04", "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", "date": "2026-06-04", "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", "date": "2026-05-15", "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", "date": "2026-05-28", "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", "date": "2026-05-29", "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", "date": "2026-07-01", "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", "date": "2026-05-22", "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", "date": "2026-05-29", "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", "date": "2026-06-02", "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", "date": "2026-05-29", "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", "date": "2026-06-20", "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", "date": "2026-06-19", "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", "date": "2026-07-09", "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", "date": "2026-07-09", "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", "date": "2026-07-02", "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", "date": "2026-07-03", "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", "date": "2026-07-07", "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", "date": "2026-07-08", "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", "date": "2026-07-09", "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", "date": "2026-07-24", "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", "date": "2026-07-25", "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", "date": "2026-07-25", "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", "date": "2026-07-09", "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", "date": "2026-07-09", "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", "date": "2026-07-09", "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", "date": "2026-07-09", "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", "date": "2026-07-10", "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", "date": "2026-07-10", "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", "date": "2026-07-10", "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", "date": "2026-08-28", "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", "date": "2026-07-12", "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", "date": "2026-07-29", "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", "date": "2026-07-11", "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", "date": "2026-07-25", "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", "date": "2026-07-25", "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", "date": "2026-07-26", "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", "date": "2026-07-26", "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", "date": "2026-07-27", "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", "date": "2026-07-27", "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", "date": "2026-07-27", "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", "date": "2026-07-27", "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", "date": "2026-07-27", "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", "date": "2026-07-27", "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", "date": "2026-07-27", "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", "date": "2026-08-24", "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", "date": "2026-07-28", "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", "date": "2026-07-28", "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", "date": "2026-07-28", "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", "date": "2026-07-29", "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", "date": "2026-07-29", "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", "date": "2026-07-29", "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", "date": "2026-07-29", "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", "date": "2026-07-29", "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", "date": "2026-07-29", "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", "date": "2026-07-29", "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", "date": "2026-07-29", "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", "date": "2026-07-29", "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", "date": "2026-07-29", "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", "date": "2026-07-29", "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", "date": "2026-08-01", "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", "date": "2026-08-05", "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", "date": "2026-08-05", "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", "date": "2026-08-06", "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", "date": "2026-08-05", "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", "date": "2026-08-06", "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", "date": "2026-08-06", "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", "date": "2026-08-06", "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", "date": "2026-08-06", "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", "date": "2026-08-06", "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", "date": "2026-08-06", "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", "date": "2026-08-06", "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", "date": "2026-08-07", "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", "date": "2026-08-07", "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", "date": "2026-08-07", "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", "date": "2026-08-07", "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", "date": "2026-08-11", "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", "date": "2026-08-07", "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", "date": "2026-08-08", "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", "date": "2026-08-08", "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", "date": "2026-08-08", "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", "date": "2026-08-08", "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", "date": "2026-08-07", "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", "date": "2026-08-07", "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", "date": "2026-08-07", "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", "date": "2026-08-08", "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", "date": "2026-08-08", "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", "date": "2026-08-08", "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", "date": "2026-08-08", "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", "date": "2026-08-08", "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", "date": "2026-08-08", "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", "date": "2026-08-08", "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", "date": "2026-08-08", "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", "date": "2026-08-09", "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", "date": "2026-08-09", "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/