From 7e04cd63e5fa85e1bb486369e3ac582670b9ff68 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 19:42:04 +0000 Subject: [PATCH 1/3] fix(bug-logs): BUG-141 alter page set RenderMode on dynamic text + upstream PR package Issue draft, format-patch of the fork branch, PR body and submission steps in bug-logs/pending-github-issues/bug141-*. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013uU1FnquwBoB43skj34Xh1 --- CHANGELOG.md | 1 + bug-logs/mxcli-bugs.md | 23 + ...1-alter-page-set-rendermode-dynamictext.md | 64 +++ .../bug141-fix-README.md | 111 +++++ .../pending-github-issues/bug141-fix.patch | 393 ++++++++++++++++++ 5 files changed, 592 insertions(+) create mode 100644 bug-logs/pending-github-issues/bug141-alter-page-set-rendermode-dynamictext.md create mode 100644 bug-logs/pending-github-issues/bug141-fix-README.md create mode 100644 bug-logs/pending-github-issues/bug141-fix.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 51ab0539..ccad4be2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ three commits past it), and a bug report can name a release instead of a sha nob Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased +- fix(bug-logs): **BUG-141 — `alter page … set RenderMode` on a dynamic text, with an upstream-ready fix package** (issue draft, 3-commit patch, PR body, submission steps in `bug-logs/pending-github-issues/bug141-*`; fix revert-proven and `mx check`-clean on 11.14.0) — a requirements-driven RFQ project - docs(pipeline-walks): **`docs/pipeline-walks.html` — a process diagram per entry mode, with the scripts run at every step.** Shared spine, migration, requirements-driven (incl. the docs-ready fast path), greenfield, change-an-existing-app (opening with the app-mapping step: `SHOW STRUCTURE`, `graph-report`, `lint`, `report`, security matrix, `marketplace diff`), à-la-carte tracks A/A2/B, and the Stage 5 BUILD→GATE→PROVE→LOOK→CONFIRM loop, each as a mermaid flowchart plus a stage/what/scripts table. Linked from the README entry-modes paragraph — Maurits Visser - fix(bin/doctor.sh): **doctor told every Podman user "docker is not installed"** — the section advertised Podman in its advice text ("Rancher Desktop or Podman … are common substitutes") while all four probes ran `docker` only: `docker info`, the `command -v docker` gate, the not-installed warning, and a start hint that said `open -a Docker`. So a machine fully able to run the container lane on Podman, but without the docker shim, was reported broken — and on a team that cannot licence Docker Desktop that reads as "go install software you are not allowed to have" (a colleague's machine-ready status carried "Docker not installed" as a known issue; they may have had Podman all along). Detection is now docker-then-podman (`MXTK_CONTAINER_RUNTIME` forces one), the runtime is **named** in the report (`podman responding — …`), the start hint knows `podman machine start` / `podman.socket`, and the not-installed warning names Podman as the licence-free option instead of implying Docker Desktop is required. Same bounded background/poll/kill probe for both, same 0/1/2 exit contract; `mxcli docker check` invocation deliberately untouched (different repo). **Not field-run** — no container runtime in the authoring container; needs one run on a Mac with Podman and no `docker` on PATH. Driver: the Mendix migration team's Docker Desktop licensing constraint — Maurits Visser - learn(skills/doctor-triage.md): **"doctor.sh says red — what now?" is now on disk instead of in a Slack thread.** Three failures that render identically get separated: my environment is wrong / the toolkit's own self-check is wrong / this line does not apply in my lane. Check the machine before naming a fix — a wrong-arch binary, a missing one and a broken self-check all read the same, which is how *"install Studio Pro 10.24.18"* became the first confident answer to a Linux-ELF mxbuild on a Mac, and how a false `fail (unreadable error file)` (the self-test bug fixed in `c0ea53c`) sent people to audit their own machines. Also: the three toolchain lanes (bundled `mx` — macOS ships one only from Mendix 11 — the Linux-only CDN toolchain, and the container lane, runtime-agnostic although doctor still probes only `docker`), a derivation for which FAILs block rather than a list that rots, the N+M-lines-dispositioned bound, and the VM/arch caveat. From the macOS onboarding thread of 2026-09-22 — Yvann, and the four people in it diff --git a/bug-logs/mxcli-bugs.md b/bug-logs/mxcli-bugs.md index c4268be7..7747a6fa 100644 --- a/bug-logs/mxcli-bugs.md +++ b/bug-logs/mxcli-bugs.md @@ -5475,6 +5475,29 @@ and render it in `DESCRIBE MICROFLOW` so the round trip does not silently flip i callee's flag is readable in the model. --- +## BUG-141: `alter page … set RenderMode` is refused on a dynamic text, though `create page` writes it — fix ready on a fork branch + +**Severity:** Low — loud refusal, clean workaround; costs a full widget restatement per heading-level change +**mxcli version:** main `31eee45` / v0.21.0 +**Mendix version:** 11.14.0 +**Discovered:** 2026-09-22, promoting a comparison page's title to H2 on a requirements-driven RFQ project +**Reproducible:** yes — any `dynamictext`, page or snippet + +`set RenderMode = H2 on txtTitle` fails with `property "RenderMode" not found (widget has no +pluggable Object)`. Root cause: the MPR backend's fixed property list in +`setRawWidgetPropertyMut` lacks `RenderMode`, so it falls through to the pluggable setter; the MCP +backend's mutator already handles it (the two lists drifted). + +**Workaround:** `replace txtTitle with { dynamictext txtTitle (Content: '…', RenderMode: H2, Class: '…') }` +— restate content, params and class. + +**Fix:** written, tested (fails-then-passes, revert-proven), validated on a real 11.14.0 model +(`mx check` 0 errors). Upstream package — issue draft, patch, PR body, submission steps — in +`pending-github-issues/bug141-*`. Not yet filed upstream. + +**Not covered by the fix:** `set Content`/`ContentParams` on a dynamic text (parser-level), +container `RenderMode`, button `RenderType` — all still need `replace`. + ## BUG-DRAFT-loop-var-expression-typecheck: the expression type checker is skipped inside a `LOOP` body — the identical expression is caught on a parameter and missed on a loop variable (2026-09-15) > **FILED UPSTREAM 2026-09-15 — https://github.com/mendixlabs/mxcli/issues/1100** diff --git a/bug-logs/pending-github-issues/bug141-alter-page-set-rendermode-dynamictext.md b/bug-logs/pending-github-issues/bug141-alter-page-set-rendermode-dynamictext.md new file mode 100644 index 00000000..7705d338 --- /dev/null +++ b/bug-logs/pending-github-issues/bug141-alter-page-set-rendermode-dynamictext.md @@ -0,0 +1,64 @@ +**Repo:** `mendixlabs/mxcli` +**Source:** `bug-logs/mxcli-bugs.md`, `## BUG-141` — found 2026-09-22 building a supplier-portal +comparison page on a requirements-driven RFQ project (Mendix 11.14.0) +**Status:** NOT YET FILED — fix ready on fork branch `MendixMau/mxcli:fix/alter-page-set-rendermode` +(fork PR MendixMau/mxcli#1); companion `bug141-fix-README.md` has the PR package +**Suggested labels:** bug, alter-page + +--- + +**Title:** `ALTER PAGE … SET RenderMode` is refused on a dynamic text, though `CREATE PAGE` writes it + +**Body:** + +## Summary + +`RenderMode` is how a dynamic text becomes a heading (`H1`–`H6`) or a paragraph. `CREATE PAGE` +writes it (`dynamictext x (Content: 'Title', RenderMode: H1)`), `DESCRIBE PAGE` round-trips it, and +the MCP backend's page mutator already sets it — but the MPR backend's `ALTER PAGE … SET` refuses +it, so fixing one heading level means replacing the whole widget. + +## Environment + +- mxcli main at `31eee45` (also seen on v0.21.0) +- Mendix **11.14.0** +- Linux container + +## Repro + +```sql +alter page MyModule.SomePage { + set RenderMode = H2 on txtTitle -- txtTitle is a DYNAMICTEXT +}; +``` + +**Actual:** + +``` +failed to set RenderMode on txtTitle: property "RenderMode" not found (widget has no pluggable Object) +``` + +(on current main the refusal is worded as "not a property of this built-in widget … use alter +styling" — which is also wrong: RenderMode is not a design property). + +**Expected:** `Altered page`, and `describe page` shows `RenderMode: H2`. + +**Workaround:** `replace txtTitle with { dynamictext txtTitle (Content: '…', RenderMode: H2) }` — +which means restating the content, its parameters and its class just to change one enum. + +## Root cause + +`setRawWidgetPropertyMut` in `mdl/backend/pagemutator/mutator.go` handles a fixed list of built-in +properties and sends everything else to the pluggable-widget setter. `RenderMode` is not on the +list, so it falls through and fails. `mdl/backend/mcp/page_mutator.go` already handles it — the +two property lists have drifted. + +## Proposed fix + +Add a `rendermode` case that applies only to a dynamic text (`Forms$DynamicText`), accepts +`Text | Paragraph | H1..H6` case-insensitively, stores the canonical spelling, and names the +allowed set on any other value or widget. A fix with tests and docs is ready; happy to open the PR +once this is approved. + +**Out of scope (separate issues if wanted):** `set Content` / `ContentParams` on a dynamic text +(parser-level), `RenderMode` on a container, `RenderType` on a button. diff --git a/bug-logs/pending-github-issues/bug141-fix-README.md b/bug-logs/pending-github-issues/bug141-fix-README.md new file mode 100644 index 00000000..dc211b9e --- /dev/null +++ b/bug-logs/pending-github-issues/bug141-fix-README.md @@ -0,0 +1,111 @@ +**Companion to:** `bug141-alter-page-set-rendermode-dynamictext.md` +**Patch:** `bug141-fix.patch` (git format-patch, 3 commits — fix, docs, test comment; author is a placeholder — +`git commit --amend --reset-author` after `git am`) +**Branch, ready to use:** `MendixMau/mxcli:fix/alter-page-set-rendermode` (fork PR MendixMau/mxcli#1) +**Status:** verified 2026-09-22 against `mendixlabs/mxcli` main `31eee45`; NOT YET submitted — +mxcli's CONTRIBUTING requires the issue to be filed and approved before a PR + +--- + +# BUG-141 fix — how to turn it into the upstream PR + +## What is in it (8 files, one concern) + +| File | Change | +|---|---| +| `mdl/backend/pagemutator/mutator.go` | `rendermode` case in `setRawWidgetPropertyMut`: dynamic text only; `Text`, `Paragraph`, `H1`–`H6`, any casing, stored canonical; any other value or widget is an error naming the allowed set | +| `mdl/backend/pagemutator/dynamictext_rendermode_test.go` | `TestSetWidgetProperty_DynamicTextRenderMode` (every value, mixed casing) and `…Invalid` (`H7`, and a non-dynamic-text widget) | +| `mdl-examples/bug-tests/alter-page-set-rendermode-dynamictext.mdl` | page and snippet repro with the expected output | +| `.claude/skills/fix-issue/findings/mdl-backend.jsonl` | the finding (symptom, root cause, the MCP/MPR drift) | +| `.claude/skills/mendix/alter-page/SKILL.md` | RenderMode in the SET table | +| `docs-site/src/language/alter-page.md` | same | +| `docs/01-project/MDL_QUICK_REFERENCE.md` | table row and "Supported SET properties" list | +| `cmd/mxcli/syntax/features_page.go` | `mxcli syntax page.alter` shows the `SET RenderMode` line | + +## Evidence already gathered + +- **Test first:** both new tests fail on `31eee45` and pass with the fix; reverting the + `mutator.go` hunk makes them fail again. +- `make build && make test && make lint` pass (ANTLR 4.13.2, Go toolchain from `go.mod`). +- **Real model:** on a scratch copy of an 11.14.0 project, the unfixed binary refused + `set RenderMode = H2 on compTitle`; the fixed binary printed `Altered page`, `describe page` + showed `RenderMode: H2`, native `mx check` reported 0 errors, and exactly one `.mxunit` changed. +- **Agentic:** the statement was written by Claude Code from the skill text, with no hints. + +## Steps (mxcli CONTRIBUTING, Steps 1–6) + +1. File the issue: paste the companion draft (`bash render-paste-ready.sh` strips the local + header). Wait for the maintainer's go-ahead and assign yourself. Note the number `NNN`. +2. Get the code, either way: + ```bash + # a) straight from the fork branch + git clone https://github.com/MendixMau/mxcli && cd mxcli + git checkout fix/alter-page-set-rendermode + git checkout -b fix/NNN-alter-page-set-rendermode + # b) or onto a fresh upstream checkout + git checkout -b fix/NNN-alter-page-set-rendermode origin/main + git am /bug-logs/pending-github-issues/bug141-fix.patch + ``` + Then `git rebase -i origin/main` → squash to one commit if the maintainer prefers, reword it to + `fix: ALTER PAGE/SNIPPET SET RenderMode on a dynamic text (closes #NNN)`, `--reset-author`, + and drop trailers you do not want upstream. Optionally rename the bug-test to + `NNN-alter-page-set-rendermode-dynamictext.mdl` (upstream names most bug-tests by issue). +3. Re-verify on the current main: + ```bash + make -C mdl/grammar bootstrap && export ANTLR4_TOOLS_ANTLR_VERSION=4.13.2 + make build && make test && make lint + ``` +4. Push to your fork and open the PR against `mendixlabs/mxcli:main`. Compare URL for the + ready-made branch: + https://github.com/mendixlabs/mxcli/compare/main...MendixMau:mxcli:fix/alter-page-set-rendermode +5. PR body — paste this, filling in `NNN`: + +```markdown +Closes #NNN + +## What does it do? + +`ALTER PAGE` / `ALTER SNIPPET` can now set `RenderMode` on a dynamic text: + + alter page Mod.Page { set RenderMode = H2 on txtTitle }; + +`CREATE PAGE` already wrote this property and the MCP backend's mutator already set it; the MPR +backend's fixed property list in `setRawWidgetPropertyMut` did not include it, so it fell through +to the pluggable-widget setter and failed. The new case: + +- applies only to a dynamic text (`Forms$DynamicText`); any other widget gets an error naming it +- accepts `Text`, `Paragraph`, `H1`–`H6` in any casing and stores the canonical spelling +- rejects anything else with `invalid RenderMode "H7" for dynamic text "txtTitle": expected one of Text, Paragraph, H1, H2, H3, H4, H5, H6` + +## Testing + +- New unit tests in `mdl/backend/pagemutator/dynamictext_rendermode_test.go`, written first: + they fail on main and pass with the fix (verified by reverting the fix). +- Bug-test `mdl-examples/bug-tests/…-alter-page-set-rendermode-dynamictext.mdl` (page + snippet). +- `make build`, `make test`, `make lint` pass. + +## Mendix validation + +Mendix 11.14.0: on a copy of a real project, `set RenderMode = H2` on an existing dynamic text → +`Altered page`; `describe page` shows `RenderMode: H2`; `mx check` 0 errors; one `.mxunit` changed. + +## Docs + +Skill `alter-page`, docs-site `alter-page.md`, `MDL_QUICK_REFERENCE.md`, `mxcli syntax page.alter`, +and a finding in `.claude/skills/fix-issue/findings/mdl-backend.jsonl`. + +## Agentic Code Testing + +- [x] Tested with Claude Code in dev container +- [x] Claude can generate correct MDL for this feature +- [x] Skills updated (if applicable) +- [x] error messages are helpful for debugging + +## Out of scope + +`set Content` / `ContentParams` on a dynamic text (grammar-level), `RenderMode` on a container, +`RenderType` on a button. +``` + +6. After merge and release: mark BUG-141 RESOLVED in `mxcli-bugs.md`, and in the consuming + project drop the `replace` workaround in favour of `set RenderMode`. diff --git a/bug-logs/pending-github-issues/bug141-fix.patch b/bug-logs/pending-github-issues/bug141-fix.patch new file mode 100644 index 00000000..5bebf0a2 --- /dev/null +++ b/bug-logs/pending-github-issues/bug141-fix.patch @@ -0,0 +1,393 @@ +From 53944656638869b97269aa5a46d89708e79b07a3 Mon Sep 17 00:00:00 2001 +From: Claude +Date: Tue, 22 Sep 2026 19:16:25 +0000 +Subject: [PATCH 1/3] fix: ALTER PAGE/SNIPPET SET RenderMode on a dynamic text +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +`alter page P { set RenderMode = H1 on }` was refused +("property \"RenderMode\" not found (widget has no pluggable Object)", +now worded as "not a property of this built-in widget … use alter +styling"), although CREATE PAGE and REPLACE accept RenderMode on a +dynamictext. setRawWidgetPropertyMut had no case for it, so it fell +through to the pluggable-property setter. + +Add a RenderMode case dispatched on the stored $Type (Forms$DynamicText): +the value is validated case-insensitively against Text, Paragraph and +H1-H6 and written in its canonical spelling; anything else is refused +with the accepted list, leaving the document untouched. Other widgets +keep the previous path. `check -p` picks this up for free, since it +dry-runs the same setter. + +Verified on a copy of a Mendix 11.14.0 app: exec -> describe shows +RenderMode: H2 -> mx check 0 errors. + +Co-Authored-By: Claude Opus 5.5 +Claude-Session: https://claude.ai/code/session_013uU1FnquwBoB43skj34Xh1 +--- + .../fix-issue/findings/mdl-backend.jsonl | 1 + + .claude/skills/mendix/alter-page/SKILL.md | 1 + + docs-site/src/language/alter-page.md | 1 + + .../alter-page-set-rendermode-dynamictext.mdl | 64 +++++++++++++ + .../dynamictext_rendermode_test.go | 89 +++++++++++++++++++ + mdl/backend/pagemutator/mutator.go | 57 ++++++++++++ + 6 files changed, 213 insertions(+) + create mode 100644 mdl-examples/bug-tests/alter-page-set-rendermode-dynamictext.mdl + create mode 100644 mdl/backend/pagemutator/dynamictext_rendermode_test.go + +diff --git a/.claude/skills/fix-issue/findings/mdl-backend.jsonl b/.claude/skills/fix-issue/findings/mdl-backend.jsonl +index d753553..7269074 100644 +--- a/.claude/skills/fix-issue/findings/mdl-backend.jsonl ++++ b/.claude/skills/fix-issue/findings/mdl-backend.jsonl +@@ -121,3 +121,4 @@ + {"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-22", "symptom": "`alter page P { set RenderMode = H1 on }` is refused: `failed to set RenderMode on compTitle: property \"RenderMode\" not found (widget has no pluggable Object)` (on current main: `not a property of this built-in widget … use alter styling`), while `create page … dynamictext x (RenderMode: H1)` and `replace x with { dynamictext … }` accept it. `check -p` refuses it too, since it dry-runs the same setter", "cause": "setRawWidgetPropertyMut is a hand-kept switch of first-class built-in properties (caption/content/label/class/…); anything not listed falls through to the pluggable-property setter. RenderMode had no case, although the MCP mutator (mdl/backend/mcp/page_mutator.go) has had one all along — the two backends' SET vocabularies are separate lists", "file": "`mdl/backend/pagemutator/mutator.go` (setDynamicTextRenderModeMut)", "insight": "When a CREATE property is refused by ALTER SET on a built-in widget, compare the CREATE builder's property list with the case list in setRawWidgetPropertyMut (and the MCP mutator's) — every such refusal so far (DynamicClasses, lowercase class, RenderMode) was a missing case, not a storage question. Dispatch RenderMode on the stored `$Type` (Forms$DynamicText): containers and ActionButtons store differently-valued RenderMode/RenderType, and a pluggable widget may own a `renderMode` key, so those keep the old path. Validate against pages.TextRenderMode (Text, Paragraph, H1–H6) case-insensitively and store the canonical spelling — the visitor passes `h2` through as typed. Still missing from SET on dynamictext: ContentParams (CREATE accepts it; SET has no case). Control: HEAD's mutator.go makes the new tests fail with the reported refusal; real 11.14.0 copy: exec → describe shows H2 → mx check 0 errors", "refs": []} +diff --git a/.claude/skills/mendix/alter-page/SKILL.md b/.claude/skills/mendix/alter-page/SKILL.md +index 2c1fa2a..46afeaa 100644 +--- a/.claude/skills/mendix/alter-page/SKILL.md ++++ b/.claude/skills/mendix/alter-page/SKILL.md +@@ -147,6 +147,7 @@ so a silent write would build cleanly and then fail to open. + | `Action` | Widgets with an on-click action (ACTIONBUTTON, LINKBUTTON, clickable containers) | Any `create page` action expression | `set Action = microflow M.ACT_Go on btnSave` | + | `caption` | ACTIONBUTTON, LINKBUTTON | String | `set caption = 'Submit' on btnSave` | + | `content` | DYNAMICTEXT | String | `set content = 'New Heading' on txtTitle` | ++| `RenderMode` | DYNAMICTEXT | Text, Paragraph, H1–H6 (any case; anything else is refused) | `set RenderMode = H2 on txtTitle` | + | `label` | TEXTBOX, TEXTAREA, DATEPICKER, COMBOBOX, CHECKBOX, RADIOBUTTONS | String | `set label = 'full Name' on txtName` | + | `buttonstyle` | ACTIONBUTTON, LINKBUTTON | Primary, Default, Success, Danger, Warning, Info | `set buttonstyle = danger on btnDelete` | + | `class` | Any widget | CSS class string | `set class = 'card mx-2' on container1` | +diff --git a/docs-site/src/language/alter-page.md b/docs-site/src/language/alter-page.md +index 819f3cb..8c894f8 100644 +--- a/docs-site/src/language/alter-page.md ++++ b/docs-site/src/language/alter-page.md +@@ -40,6 +40,7 @@ ALTER PAGE Module.EditPage { + |----------|-------------|---------| + | `Caption` | Button/link caption | `SET Caption = 'Submit' ON btnSave` | + | `Label` | Input field label | `SET Label = 'Full Name' ON txtName` | ++| `RenderMode` | Dynamic text rendering: `Text`, `Paragraph`, `H1`–`H6` | `SET RenderMode = H2 ON txtTitle` | + | `ButtonStyle` | Button visual style | `SET ButtonStyle = Danger ON btnDelete` | + | `Class` | CSS class names | `SET Class = 'card p-3' ON cMain` | + | `Style` | Inline CSS | `SET Style = 'margin: 8px;' ON cBox` | +diff --git a/mdl-examples/bug-tests/alter-page-set-rendermode-dynamictext.mdl b/mdl-examples/bug-tests/alter-page-set-rendermode-dynamictext.mdl +new file mode 100644 +index 0000000..c935450 +--- /dev/null ++++ b/mdl-examples/bug-tests/alter-page-set-rendermode-dynamictext.mdl +@@ -0,0 +1,64 @@ ++-- ============================================================================ ++-- ALTER PAGE / ALTER SNIPPET: SET RenderMode on a dynamic text ++-- ============================================================================ ++-- ++-- Reported: ++-- ++-- alter page Portal_UI_Internal.Quote_Comparison { set RenderMode = H2 on compTitle } ++-- -> failed to set RenderMode on compTitle: property "RenderMode" not found ++-- (widget has no pluggable Object) ++-- ++-- (current main words the same refusal as "not a property of this built-in ++-- widget … use alter styling", which is no more true: RenderMode is not a ++-- design property either). ++-- ++-- `create page … dynamictext x (RenderMode: H1)` and `replace x with { … }` ++-- both accept it, because RenderMode is a first-class Forms$DynamicText ++-- property. The ALTER SET switch in setRawWidgetPropertyMut simply had no case ++-- for it, so it fell through to the pluggable-property setter. ++-- ++-- Values: Text, Paragraph, H1..H6 (matched case-insensitively, stored in the ++-- canonical spelling). Anything else is refused by both `check -p` and `exec` ++-- — e.g. `set RenderMode = H7 on title` → ++-- invalid RenderMode "H7" for dynamic text "title": expected one of Text, ++-- Paragraph, H1, H2, H3, H4, H5, H6 ++-- ++-- Measured on a copy of a real Mendix 11.14.0 app: exec → "Altered page", ++-- describe shows RenderMode: H2, mx check → 0 errors; exactly one .mxunit ++-- changed. ++-- ++-- Usage: ++-- mxcli exec mdl-examples/bug-tests/alter-page-set-rendermode-dynamictext.mdl -p app.mpr ++-- ============================================================================ ++ ++create entity MyFirstModule.RmRow ( Name: String ); ++ ++create or replace page MyFirstModule.P_SetRenderMode ++( ++ Title: 'Set RenderMode', ++ Layout: Atlas_Core.Atlas_Default, ++ Params: { $Row: MyFirstModule.RmRow } ++) ++{ ++ dataview dv (datasource: $Row) { ++ dynamictext title (content: 'Title {1}', contentparams: [{1} = Name], rendermode: H1) ++ dynamictext body (content: 'Body') ++ } ++} ++ ++alter page MyFirstModule.P_SetRenderMode { ++ set RenderMode = H2 on title; ++ set rendermode = paragraph on body; ++} ++ ++create or replace snippet MyFirstModule.S_SetRenderMode ++{ ++ dynamictext snipTitle (content: 'Snippet title', rendermode: H3) ++} ++ ++alter snippet MyFirstModule.S_SetRenderMode { ++ set RenderMode = H4 on snipTitle; ++} ++ ++describe page MyFirstModule.P_SetRenderMode; ++describe snippet MyFirstModule.S_SetRenderMode; +diff --git a/mdl/backend/pagemutator/dynamictext_rendermode_test.go b/mdl/backend/pagemutator/dynamictext_rendermode_test.go +new file mode 100644 +index 0000000..2c5b365 +--- /dev/null ++++ b/mdl/backend/pagemutator/dynamictext_rendermode_test.go +@@ -0,0 +1,89 @@ ++// SPDX-License-Identifier: Apache-2.0 ++ ++package pagemutator ++ ++import ( ++ "strings" ++ "testing" ++ ++ "go.mongodb.org/mongo-driver/bson" ++ ++ "github.com/mendixlabs/mxcli/mdl/backend/bsonnav" ++) ++ ++// makeDynamicText builds a stored dynamic text widget the way Studio Pro writes ++// it: a RenderMode string alongside the Content client template. ++func makeDynamicText(name, renderMode string) bson.D { ++ return bson.D{ ++ {Key: "$Type", Value: "Forms$DynamicText"}, ++ {Key: "Name", Value: name}, ++ {Key: "NativeTextStyle", Value: "Text"}, ++ {Key: "RenderMode", Value: renderMode}, ++ } ++} ++ ++// `alter page P { set RenderMode = H1 on compTitle }` on a dynamic text failed ++// with ++// ++// failed to set RenderMode on compTitle: property "RenderMode" not found (widget has no pluggable Object) ++// ++// while `create page … dynamictext x (…, RenderMode: H1)` and `replace x with ++// { dynamictext x (…, RenderMode: H1) }` both accept it. RenderMode is a ++// first-class Forms$DynamicText property, so the SET must write it in place. ++func TestSetWidgetProperty_DynamicTextRenderMode(t *testing.T) { ++ cases := []struct{ in, want string }{ ++ {"H1", "H1"}, ++ {"H6", "H6"}, ++ {"h2", "H2"}, // property values arrive as typed — case-insensitive, like the name ++ {"Paragraph", "Paragraph"}, ++ {"text", "Text"}, ++ } ++ for _, tc := range cases { ++ t.Run(tc.in, func(t *testing.T) { ++ rawData := makeRawPage(makeDynamicText("compTitle", "Text")) ++ m := &Mutator{rawData: rawData, widgetFinder: findBsonWidget} ++ if err := m.SetWidgetProperty("compTitle", "RenderMode", tc.in); err != nil { ++ t.Fatalf("SetWidgetProperty(RenderMode=%s) failed: %v", tc.in, err) ++ } ++ got := bsonnav.DGetString(findBsonWidget(rawData, "compTitle").widget, "RenderMode") ++ if got != tc.want { ++ t.Errorf("RenderMode = %q, want %q", got, tc.want) ++ } ++ }) ++ } ++ ++ t.Run("lowercase property name", func(t *testing.T) { ++ rawData := makeRawPage(makeDynamicText("compTitle", "Text")) ++ m := &Mutator{rawData: rawData, widgetFinder: findBsonWidget} ++ if err := m.SetWidgetProperty("compTitle", "rendermode", "H3"); err != nil { ++ t.Fatalf("SetWidgetProperty(rendermode) failed: %v", err) ++ } ++ if got := bsonnav.DGetString(findBsonWidget(rawData, "compTitle").widget, "RenderMode"); got != "H3" { ++ t.Errorf("RenderMode = %q, want H3", got) ++ } ++ }) ++} ++ ++// An invalid value is refused with the accepted list, and the stored value is ++// left alone — writing an unknown enum member gives a document Studio Pro ++// cannot open. ++func TestSetWidgetProperty_DynamicTextRenderModeInvalid(t *testing.T) { ++ for _, bad := range []any{"H7", "Div", "", 1, true} { ++ rawData := makeRawPage(makeDynamicText("compTitle", "H1")) ++ m := &Mutator{rawData: rawData, widgetFinder: findBsonWidget} ++ err := m.SetWidgetProperty("compTitle", "RenderMode", bad) ++ if err == nil { ++ t.Fatalf("RenderMode = %v must be refused", bad) ++ } ++ msg := err.Error() ++ if strings.Contains(msg, "pluggable Object") { ++ t.Errorf("RenderMode = %v: the message describes mxcli internals: %q", bad, msg) ++ } ++ if !strings.Contains(msg, "Paragraph") || !strings.Contains(msg, "H6") { ++ t.Errorf("RenderMode = %v: the message does not list the accepted values: %q", bad, msg) ++ } ++ if got := bsonnav.DGetString(findBsonWidget(rawData, "compTitle").widget, "RenderMode"); got != "H1" { ++ t.Errorf("RenderMode = %v: stored value changed to %q on a refused write", bad, got) ++ } ++ } ++} +diff --git a/mdl/backend/pagemutator/mutator.go b/mdl/backend/pagemutator/mutator.go +index d1fa60d..9d9d449 100644 +--- a/mdl/backend/pagemutator/mutator.go ++++ b/mdl/backend/pagemutator/mutator.go +@@ -2622,12 +2622,69 @@ func setRawWidgetPropertyMut(widget bson.D, propName string, value any) error { + return nil + case "attribute": + return setWidgetAttributeRefMut(widget, value) ++ case "rendermode": ++ // RenderMode is a first-class property of a dynamic text (Text, Paragraph, ++ // H1–H6). Other widgets that store a RenderMode (a container's Div/Section/…) ++ // or a pluggable widget with its own "renderMode" key keep the existing path. ++ if isDynamicTextWidget(widget) { ++ return setDynamicTextRenderModeMut(widget, value) ++ } ++ return setPluggableWidgetPropertyMut(widget, propName, value) + default: + // Try as pluggable widget property + return setPluggableWidgetPropertyMut(widget, propName, value) + } + } + ++// dynamicTextRenderModes are the values a Forms$DynamicText's RenderMode takes — ++// the same set CREATE PAGE writes from `dynamictext x (RenderMode: …)`. ++var dynamicTextRenderModes = []pages.TextRenderMode{ ++ pages.TextRenderModeText, ++ pages.TextRenderModeParagraph, ++ pages.TextRenderModeH1, ++ pages.TextRenderModeH2, ++ pages.TextRenderModeH3, ++ pages.TextRenderModeH4, ++ pages.TextRenderModeH5, ++ pages.TextRenderModeH6, ++} ++ ++// isDynamicTextWidget reports whether a stored widget is a dynamic text. Stored ++// documents carry Forms$DynamicText; Pages$DynamicText is the SDK spelling. ++func isDynamicTextWidget(widget bson.D) bool { ++ switch bsonnav.DGetString(widget, "$Type") { ++ case "Forms$DynamicText", "Pages$DynamicText": ++ return true ++ } ++ return false ++} ++ ++// setDynamicTextRenderModeMut writes a dynamic text's RenderMode in place. The ++// value is matched case-insensitively (MDL passes it as typed: `H1`, `h1`) and ++// stored in the canonical spelling; anything outside the enumeration is refused ++// before the document is touched — an unknown enum member is a document Studio ++// Pro cannot open. ++func setDynamicTextRenderModeMut(widget bson.D, value any) error { ++ s, _ := value.(string) ++ for _, mode := range dynamicTextRenderModes { ++ if s != "" && strings.EqualFold(s, string(mode)) { ++ if !bsonnav.DSet(widget, "RenderMode", string(mode)) { ++ // Studio Pro writes RenderMode on every dynamic text; a document ++ // without it is not one this setter should guess a position for. ++ return fmt.Errorf("dynamic text %q has no stored RenderMode property to set", ++ bsonnav.DGetString(widget, "Name")) ++ } ++ return nil ++ } ++ } ++ names := make([]string, len(dynamicTextRenderModes)) ++ for i, mode := range dynamicTextRenderModes { ++ names[i] = string(mode) ++ } ++ return fmt.Errorf("invalid RenderMode %q for dynamic text %q: expected one of %s", ++ fmt.Sprint(value), bsonnav.DGetString(widget, "Name"), strings.Join(names, ", ")) ++} ++ + // --------------------------------------------------------------------------- + // Design property (Atlas styling) mutation + // --------------------------------------------------------------------------- +-- +2.43.0 + + +From e91ec656e10d34da7b27112b3f6e73cc4842a171 Mon Sep 17 00:00:00 2001 +From: Claude +Date: Tue, 22 Sep 2026 19:40:14 +0000 +Subject: [PATCH 2/3] docs: list SET RenderMode in quick reference and syntax + help + +Co-Authored-By: Claude Opus 5.5 +Claude-Session: https://claude.ai/code/session_013uU1FnquwBoB43skj34Xh1 +--- + cmd/mxcli/syntax/features_page.go | 2 +- + docs/01-project/MDL_QUICK_REFERENCE.md | 3 ++- + 2 files changed, 3 insertions(+), 2 deletions(-) + +diff --git a/cmd/mxcli/syntax/features_page.go b/cmd/mxcli/syntax/features_page.go +index 8cfd6b9..44c5818 100644 +--- a/cmd/mxcli/syntax/features_page.go ++++ b/cmd/mxcli/syntax/features_page.go +@@ -277,7 +277,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { + "popup width", "popup height", "popup resizable", + "drop template", "insert template", "list view template", + }, +- Syntax: "ALTER PAGE Module.Name {\n SET property = value ON widgetName; -- widget property names: any casing\n SET Action = MICROFLOW Module.MF ON btnSave; -- any CREATE PAGE action form\n SET DataSource = $Param ON dvOrder; -- parameter/microflow/nanoflow/selection;\n -- DATABASE and association are REPLACE-only,\n -- and a data view takes no database source\n SET (prop1 = val1, prop2 = val2) ON widgetName;\n SET Title = 'New Title'; -- page-level (case-sensitive)\n SET Documentation = 'What this page is for.';\n SET Class = 'css-class'; -- page-level CSS class / style\n SET Style = 'css: rule';\n SET PopupWidth = 800; -- page-level pop-up dimensions\n SET PopupHeight = 480;\n SET PopupResizable = true;\n INSERT AFTER widgetName { };\n INSERT BEFORE widgetName { };\n INSERT INTO containerName { };\n DROP WIDGET name1, name2;\n DROP TEMPLATE FOR Module.Specialization IN listViewName;\n REPLACE widgetName WITH { };\n};", ++ Syntax: "ALTER PAGE Module.Name {\n SET property = value ON widgetName; -- widget property names: any casing\n SET Action = MICROFLOW Module.MF ON btnSave; -- any CREATE PAGE action form\n SET DataSource = $Param ON dvOrder; -- parameter/microflow/nanoflow/selection;\n -- DATABASE and association are REPLACE-only,\n -- and a data view takes no database source\n SET RenderMode = H2 ON txtTitle; -- dynamic text: Text | Paragraph | H1..H6\n SET (prop1 = val1, prop2 = val2) ON widgetName;\n SET Title = 'New Title'; -- page-level (case-sensitive)\n SET Documentation = 'What this page is for.';\n SET Class = 'css-class'; -- page-level CSS class / style\n SET Style = 'css: rule';\n SET PopupWidth = 800; -- page-level pop-up dimensions\n SET PopupHeight = 480;\n SET PopupResizable = true;\n INSERT AFTER widgetName { };\n INSERT BEFORE widgetName { };\n INSERT INTO containerName { };\n DROP WIDGET name1, name2;\n DROP TEMPLATE FOR Module.Specialization IN listViewName;\n REPLACE widgetName WITH { };\n};", + Example: "ALTER PAGE Module.EditPage {\n SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;\n INSERT AFTER txtName {\n TEXTBOX txtMiddleName (Label: 'Middle Name', Attribute: MiddleName)\n };\n DROP WIDGET txtUnused;\n};", + SeeAlso: []string{"page.create", "page.show", "snippet.alter"}, + }) +diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md +index d59ef8a..da5fcd9 100644 +--- a/docs/01-project/MDL_QUICK_REFERENCE.md ++++ b/docs/01-project/MDL_QUICK_REFERENCE.md +@@ -1595,6 +1595,7 @@ Modify an existing page or snippet's widget tree in-place without full `create o + | Documentation | `set Documentation = 'What this page is for.'` | Page-level. Same property the `/** … */` doc comment on `CREATE PAGE` writes, so an existing page can be documented without restating it. `''` clears it | + | Pop-up dimensions | `set PopupWidth = 800` / `set PopupHeight = 480` / `set PopupResizable = true` | Page-level; apply when the page opens in a pop-up | + | Page CSS class / style | `set Class = 'css-class'` / `set Style = 'css: rule'` | Page-level (no ON clause); sets the page's Appearance | ++| Dynamic text heading level | `set RenderMode = H2 on txtTitle` | Dynamic text only: Text, Paragraph, H1–H6 (any casing). Any other widget or value is an error naming the allowed set | + | Widget dynamic classes | `set DynamicClasses = 'expr' on widgetName` | Runtime-computed classes on a widget — the surgical alternative to a bulk `update widgets` | + | Insert after | `insert after widgetName { widgets }` | Add widgets after target | + | Insert before | `insert before widgetName { widgets }` | Add widgets before target | +@@ -1610,7 +1611,7 @@ Modify an existing page or snippet's widget tree in-place without full `create o + | Set layout | `set layout = Module.LayoutName` | Change page layout, auto-maps placeholders | + | Set layout + map | `set layout = Module.Layout map (Old as New)` | Explicit placeholder mapping | + +-**Supported SET properties:** Caption, Label, ButtonStyle, Class, Style, DynamicClasses, Editable, Visible, Name, Title (page-level), Documentation (page-level), Layout (page-level), PopupWidth / PopupHeight / PopupResizable (page-level), and quoted pluggable widget properties. ++**Supported SET properties:** Caption, Label, ButtonStyle, Class, Style, DynamicClasses, RenderMode (dynamic text), Editable, Visible, Name, Title (page-level), Documentation (page-level), Layout (page-level), PopupWidth / PopupHeight / PopupResizable (page-level), and quoted pluggable widget properties. + + **Example:** + ```sql +-- +2.43.0 + + +From a587f80df306112cc3a53ac3b91c1a7dad2834f5 Mon Sep 17 00:00:00 2001 +From: Claude +Date: Tue, 22 Sep 2026 19:41:49 +0000 +Subject: [PATCH 3/3] test: use a generic page name in the RenderMode bug-test + comment + +Co-Authored-By: Claude Opus 5.5 +Claude-Session: https://claude.ai/code/session_013uU1FnquwBoB43skj34Xh1 +--- + .../bug-tests/alter-page-set-rendermode-dynamictext.mdl | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/mdl-examples/bug-tests/alter-page-set-rendermode-dynamictext.mdl b/mdl-examples/bug-tests/alter-page-set-rendermode-dynamictext.mdl +index c935450..3ad76ac 100644 +--- a/mdl-examples/bug-tests/alter-page-set-rendermode-dynamictext.mdl ++++ b/mdl-examples/bug-tests/alter-page-set-rendermode-dynamictext.mdl +@@ -4,7 +4,7 @@ + -- + -- Reported: + -- +--- alter page Portal_UI_Internal.Quote_Comparison { set RenderMode = H2 on compTitle } ++-- alter page MyModule.Comparison_Page { set RenderMode = H2 on compTitle } + -- -> failed to set RenderMode on compTitle: property "RenderMode" not found + -- (widget has no pluggable Object) + -- +-- +2.43.0 + From 022a6042931b05c67759cd6fc607b15b7550aeaa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 08:36:32 +0000 Subject: [PATCH 2/3] learn(microflows): validation collects every field error, then stops once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validation microflows that did `validation feedback` + `return` per field flagged one input per submit: an empty form turned only the first field red. learned-microflow-patterns.md "Validation Feedback — Correct Pattern" now leads with the collect-all rule and a before/after (early return vs $IsValid accumulation, cross-field check guarded on its own inputs, state guards still allowed to return early). The cookbook's guard-chain example now says it is for state guards only. Both MDL snippets pass `mxcli check` (v0.22.0, syntax-only). Reported by the kybapp project's product owner, 2026-09-25. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013uU1FnquwBoB43skj34Xh1 --- CHANGELOG.md | 1 + skills/learned-microflow-patterns.md | 68 +++++++++++++++++++++++++--- skills/mdl-cookbook-microflows.md | 4 +- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30d0bce8..c4b94143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ three commits past it), and a bug report can name a release instead of a sha nob Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased +- learn(learned-microflow-patterns, mdl-cookbook-microflows): **validation microflows collect every field error, then stop once — never `validation feedback` + `return` per field.** An empty form flagged one field per submit: the first input turned red, the user fixed it and resubmitted, and only then saw the second, because each check returned early and the later ones never ran (44 feedback-then-`return` sites across 11 scripts in the reporting project). The "Validation Feedback — Correct Pattern" section now leads with the rule and a before/after (early-return vs `$IsValid` collect-all, a cross-field check guarded on its own inputs, state guards still allowed to return early), and the cookbook's guard-chain example says it is for state guards only. `mxcli check` passes both shapes. — kybapp project (product owner report, 2026-09-25) - fix(bug-logs): **BUG-141 — `alter page … set RenderMode` on a dynamic text, with an upstream-ready fix package** (issue draft, 3-commit patch, PR body, submission steps in `bug-logs/pending-github-issues/bug141-*`; fix revert-proven and `mx check`-clean on 11.14.0) — a requirements-driven RFQ project - fix(project-bin/exec.sh): **the lint ratchet now runs after every clean mxbuild, and its verdict lands in the same BUILD-LOG row.** mxbuild proves the model compiles, not how it is built: on a field project a migration microflow shipped three commits inside loops (`mxcli lint` CONV011) under "✅ applied · mxbuild clean", because lint lived only in the gate-agent definition as an optional step and that agent had been spawned 0 times against 21 `exec.sh` runs. `exec.sh` now calls `bin/lint-gate.sh` after a clean build; a rise vs the committed baseline writes `⚠️ applied, LINT ROSE: ` and exits 1 while keeping the write (shape, not corruption), `SKIP_LINT=` is the only override and is recorded in the row, and a project without `lint-gate.sh` gets `lint not installed` rather than a blank cell. `agents/gate-agent.md` and `agents/agent-roles.md` now read the row instead of treating lint as optional; `learned-detection-gaps.md` gains the register row. Field run: a marketplace guest-group migration, 2026-09-24, ~13 s per run on a 10k-finding project — Maurits Visser - learn(ui-loop, learned-detection-gaps): **the screenshot harness is an instrument, and nobody diff --git a/skills/learned-microflow-patterns.md b/skills/learned-microflow-patterns.md index 1868adc4..5c2705a5 100644 --- a/skills/learned-microflow-patterns.md +++ b/skills/learned-microflow-patterns.md @@ -408,15 +408,71 @@ end; ## Validation Feedback — Correct Pattern (from ACT_OrderDetail_Save) -**Rule:** Use `validation feedback` directly — no `log error` alongside it, no annotations. +**Trigger:** writing any VAL_/ACT_ microflow that puts `validation feedback` on user input. -```mdl -IF trim($Dto/FieldName) = '' THEN - SET $IsValid = false; - VALIDATION FEEDBACK $Dto/FieldName MESSAGE 'non-English message'; -END IF; +**Rule: collect every field error, then stop once.** Declare `$IsValid Boolean = true`; give +each field its own independent `if` that fires `validation feedback` and `set $IsValid = false` +— no `return` inside it, no `else` chaining. After the last check, `if not($IsValid) then return +false; end if;`, then save. A cross-field check guards on *its own* inputs being non-empty, +never on `$IsValid`. Use `validation feedback` directly — no `log error` alongside it, no +annotations. + +**Why:** early return flags one field per submit — an empty form turns the first input red, the +user fixes it, resubmits, and only then sees the second (kybapp, 2026-09-25, reported by the +product owner; a grep found 44 feedback-then-`return` sites across 11 of its scripts). + +Wrong — the second check never runs while the first field is empty: + +```sql +if $Quote/TotalPrice = empty then + validation feedback $Quote/TotalPrice message 'Total price is required.'; + return false; +end if; +if $Quote/ValidUntil = empty then + validation feedback $Quote/ValidUntil message 'Valid until is required.'; + return false; +end if; ``` +Right — every field flags red on the same submit: + +```sql +create or modify microflow Sales."ACT_Quote_Submit" ( + $Quote: Sales."Quote" +) +returns Boolean as $IsSubmitted +begin + declare $IsSubmitted Boolean = false; + declare $IsValid Boolean = true; + if $Quote/TotalPrice = empty then + validation feedback $Quote/TotalPrice message 'Total price is required.'; + set $IsValid = false; + end if; + if $Quote/ValidUntil = empty then + validation feedback $Quote/ValidUntil message 'Valid until is required.'; + set $IsValid = false; + end if; + -- cross-field: guard on ValidUntil itself, not on $IsValid + if $Quote/ValidUntil != empty and $Quote/ValidUntil < [%CurrentDateTime%] then + validation feedback $Quote/ValidUntil message 'Valid until must be in the future.'; + set $IsValid = false; + end if; + if not($IsValid) then + return false; + end if; + commit $Quote refresh; + set $IsSubmitted = true; + return $IsSubmitted; +end; +/ +``` + +Early return stays right for **state guards** that are not about a field (wrong status, record +missing, deadline passed → `show message`, `return false`) — run those first, then the +collect-all field block. Attribute paths stay unquoted (`$Quote/TotalPrice`); see +`learned-mdl-preflight.md`. `mxcli check` passes both shapes — only a reader or a browser +submit of an empty form tells them apart. + **GRANT syntax:** Short role names only — `Admin, User` NOT `OrderRegistration.Admin`. **CE0639 is unavoidable via mxcli:** mxcli does not wire the Variable property in validation feedback BSON. After exec, open the microflow in Studio Pro → for each feedback activity → set Variable = $Dto, Member = attribute. One Studio Pro session fixes all. diff --git a/skills/mdl-cookbook-microflows.md b/skills/mdl-cookbook-microflows.md index f46f1315..283906d7 100644 --- a/skills/mdl-cookbook-microflows.md +++ b/skills/mdl-cookbook-microflows.md @@ -300,7 +300,9 @@ duplicate check → WF stub submission → status update. Uses `$currentUser/Nam applicant field. **Patterns demonstrated:** -- Guard chain pattern (early-return at each step, no deep nesting) +- Guard chain pattern (early-return at each step, no deep nesting) — for *state* guards only; + per-field input checks never return early, they collect-all as in §2 (see + `learned-microflow-patterns.md` → "Validation Feedback — Correct Pattern") - `$currentUser/Name` — built-in variable for the logged-in user's name - XPath retrieve chained across two modules (same as GET_OrderDetail_Dto) - `$Obj/Attr` path navigation after retrieve From 217906236d5c35cb43b988fd3557a237a89f9728 Mon Sep 17 00:00:00 2001 From: r Date: Tue, 29 Sep 2026 07:01:32 +0000 Subject: [PATCH 3/3] fix(bug-logs): renumber BUG-141 to BUG-155 (BUG-141..154 claimed by another PR) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 2 +- bug-logs/mxcli-bugs.md | 4 ++-- ...=> bug155-alter-page-set-rendermode-dynamictext.md} | 4 ++-- .../{bug141-fix-README.md => bug155-fix-README.md} | 10 +++++----- .../{bug141-fix.patch => bug155-fix.patch} | 0 5 files changed, 10 insertions(+), 10 deletions(-) rename bug-logs/pending-github-issues/{bug141-alter-page-set-rendermode-dynamictext.md => bug155-alter-page-set-rendermode-dynamictext.md} (94%) rename bug-logs/pending-github-issues/{bug141-fix-README.md => bug155-fix-README.md} (94%) rename bug-logs/pending-github-issues/{bug141-fix.patch => bug155-fix.patch} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b94143..da306b20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased - learn(learned-microflow-patterns, mdl-cookbook-microflows): **validation microflows collect every field error, then stop once — never `validation feedback` + `return` per field.** An empty form flagged one field per submit: the first input turned red, the user fixed it and resubmitted, and only then saw the second, because each check returned early and the later ones never ran (44 feedback-then-`return` sites across 11 scripts in the reporting project). The "Validation Feedback — Correct Pattern" section now leads with the rule and a before/after (early-return vs `$IsValid` collect-all, a cross-field check guarded on its own inputs, state guards still allowed to return early), and the cookbook's guard-chain example says it is for state guards only. `mxcli check` passes both shapes. — kybapp project (product owner report, 2026-09-25) -- fix(bug-logs): **BUG-141 — `alter page … set RenderMode` on a dynamic text, with an upstream-ready fix package** (issue draft, 3-commit patch, PR body, submission steps in `bug-logs/pending-github-issues/bug141-*`; fix revert-proven and `mx check`-clean on 11.14.0) — a requirements-driven RFQ project +- fix(bug-logs): **BUG-155 — `alter page … set RenderMode` on a dynamic text, with an upstream-ready fix package** (issue draft, 3-commit patch, PR body, submission steps in `bug-logs/pending-github-issues/bug155-*`; fix revert-proven and `mx check`-clean on 11.14.0) — a requirements-driven RFQ project - fix(project-bin/exec.sh): **the lint ratchet now runs after every clean mxbuild, and its verdict lands in the same BUILD-LOG row.** mxbuild proves the model compiles, not how it is built: on a field project a migration microflow shipped three commits inside loops (`mxcli lint` CONV011) under "✅ applied · mxbuild clean", because lint lived only in the gate-agent definition as an optional step and that agent had been spawned 0 times against 21 `exec.sh` runs. `exec.sh` now calls `bin/lint-gate.sh` after a clean build; a rise vs the committed baseline writes `⚠️ applied, LINT ROSE: ` and exits 1 while keeping the write (shape, not corruption), `SKIP_LINT=` is the only override and is recorded in the row, and a project without `lint-gate.sh` gets `lint not installed` rather than a blank cell. `agents/gate-agent.md` and `agents/agent-roles.md` now read the row instead of treating lint as optional; `learned-detection-gaps.md` gains the register row. Field run: a marketplace guest-group migration, 2026-09-24, ~13 s per run on a 10k-finding project — Maurits Visser - learn(ui-loop, learned-detection-gaps): **the screenshot harness is an instrument, and nobody scores it.** Two harness defects on the PRD benchmark's Arm A produced screenshots that were diff --git a/bug-logs/mxcli-bugs.md b/bug-logs/mxcli-bugs.md index 7747a6fa..7f852cb0 100644 --- a/bug-logs/mxcli-bugs.md +++ b/bug-logs/mxcli-bugs.md @@ -5475,7 +5475,7 @@ and render it in `DESCRIBE MICROFLOW` so the round trip does not silently flip i callee's flag is readable in the model. --- -## BUG-141: `alter page … set RenderMode` is refused on a dynamic text, though `create page` writes it — fix ready on a fork branch +## BUG-155: `alter page … set RenderMode` is refused on a dynamic text, though `create page` writes it — fix ready on a fork branch **Severity:** Low — loud refusal, clean workaround; costs a full widget restatement per heading-level change **mxcli version:** main `31eee45` / v0.21.0 @@ -5493,7 +5493,7 @@ backend's mutator already handles it (the two lists drifted). **Fix:** written, tested (fails-then-passes, revert-proven), validated on a real 11.14.0 model (`mx check` 0 errors). Upstream package — issue draft, patch, PR body, submission steps — in -`pending-github-issues/bug141-*`. Not yet filed upstream. +`pending-github-issues/bug155-*`. Not yet filed upstream. **Not covered by the fix:** `set Content`/`ContentParams` on a dynamic text (parser-level), container `RenderMode`, button `RenderType` — all still need `replace`. diff --git a/bug-logs/pending-github-issues/bug141-alter-page-set-rendermode-dynamictext.md b/bug-logs/pending-github-issues/bug155-alter-page-set-rendermode-dynamictext.md similarity index 94% rename from bug-logs/pending-github-issues/bug141-alter-page-set-rendermode-dynamictext.md rename to bug-logs/pending-github-issues/bug155-alter-page-set-rendermode-dynamictext.md index 7705d338..835818eb 100644 --- a/bug-logs/pending-github-issues/bug141-alter-page-set-rendermode-dynamictext.md +++ b/bug-logs/pending-github-issues/bug155-alter-page-set-rendermode-dynamictext.md @@ -1,8 +1,8 @@ **Repo:** `mendixlabs/mxcli` -**Source:** `bug-logs/mxcli-bugs.md`, `## BUG-141` — found 2026-09-22 building a supplier-portal +**Source:** `bug-logs/mxcli-bugs.md`, `## BUG-155` — found 2026-09-22 building a supplier-portal comparison page on a requirements-driven RFQ project (Mendix 11.14.0) **Status:** NOT YET FILED — fix ready on fork branch `MendixMau/mxcli:fix/alter-page-set-rendermode` -(fork PR MendixMau/mxcli#1); companion `bug141-fix-README.md` has the PR package +(fork PR MendixMau/mxcli#1); companion `bug155-fix-README.md` has the PR package **Suggested labels:** bug, alter-page --- diff --git a/bug-logs/pending-github-issues/bug141-fix-README.md b/bug-logs/pending-github-issues/bug155-fix-README.md similarity index 94% rename from bug-logs/pending-github-issues/bug141-fix-README.md rename to bug-logs/pending-github-issues/bug155-fix-README.md index dc211b9e..c9bd9a08 100644 --- a/bug-logs/pending-github-issues/bug141-fix-README.md +++ b/bug-logs/pending-github-issues/bug155-fix-README.md @@ -1,5 +1,5 @@ -**Companion to:** `bug141-alter-page-set-rendermode-dynamictext.md` -**Patch:** `bug141-fix.patch` (git format-patch, 3 commits — fix, docs, test comment; author is a placeholder — +**Companion to:** `bug155-alter-page-set-rendermode-dynamictext.md` +**Patch:** `bug155-fix.patch` (git format-patch, 3 commits — fix, docs, test comment; author is a placeholder — `git commit --amend --reset-author` after `git am`) **Branch, ready to use:** `MendixMau/mxcli:fix/alter-page-set-rendermode` (fork PR MendixMau/mxcli#1) **Status:** verified 2026-09-22 against `mendixlabs/mxcli` main `31eee45`; NOT YET submitted — @@ -7,7 +7,7 @@ mxcli's CONTRIBUTING requires the issue to be filed and approved before a PR --- -# BUG-141 fix — how to turn it into the upstream PR +# BUG-155 fix — how to turn it into the upstream PR ## What is in it (8 files, one concern) @@ -44,7 +44,7 @@ mxcli's CONTRIBUTING requires the issue to be filed and approved before a PR git checkout -b fix/NNN-alter-page-set-rendermode # b) or onto a fresh upstream checkout git checkout -b fix/NNN-alter-page-set-rendermode origin/main - git am /bug-logs/pending-github-issues/bug141-fix.patch + git am /bug-logs/pending-github-issues/bug155-fix.patch ``` Then `git rebase -i origin/main` → squash to one commit if the maintainer prefers, reword it to `fix: ALTER PAGE/SNIPPET SET RenderMode on a dynamic text (closes #NNN)`, `--reset-author`, @@ -107,5 +107,5 @@ and a finding in `.claude/skills/fix-issue/findings/mdl-backend.jsonl`. `RenderType` on a button. ``` -6. After merge and release: mark BUG-141 RESOLVED in `mxcli-bugs.md`, and in the consuming +6. After merge and release: mark BUG-155 RESOLVED in `mxcli-bugs.md`, and in the consuming project drop the `replace` workaround in favour of `set RenderMode`. diff --git a/bug-logs/pending-github-issues/bug141-fix.patch b/bug-logs/pending-github-issues/bug155-fix.patch similarity index 100% rename from bug-logs/pending-github-issues/bug141-fix.patch rename to bug-logs/pending-github-issues/bug155-fix.patch