Fixes for #812, #808, #779 and mxcli new; mandatory microflow semicolons; runtime verification workflow - #816
Merged
Merged
Conversation
These two rows were written with the mendixlabs#791 / mendixlabs#790 fixes but dropped from that PR so it would stop touching the shared symptom table: every split PR appended at the same anchor, so each merge invalidated the rest and the branch had to be re-synced twice. With both merged there is nothing left to conflict with, so they go back in unchanged. They cover the break/continue serialization gap — including the "dump the microflow and check every *Pointer resolves" recipe for diagnosing a dangling reference without Studio Pro, and the note that a check-time rule existing only as a stopgap for a write-path bug should be removed with the real fix — and the loop-box over-measurement, with the warning not to guess the placement advance for compound elements. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…endixlabs#808) `mx update-widgets` rewrites an MPRv2 project into the self-contained MPRv1 storage format: it inlines every unit into the .mpr (adding a Unit.Contents column) and deletes mprcontents/. A command that checks or builds must not mutate the source project's on-disk format — doing so silently desyncs the working tree from a Git repository that tracks the mprcontents/ files, breaks a running `mxcli run --local` watch loop, and has been observed to leave Studio Pro unable to open the project. mendixlabs#763 reported this for `docker check` and PR mendixlabs#764 fixed it there, by wrapping the invocation inside Check with a snapshot/restore. But build.go carried its own bare copy of the same invocation, so the conversion continued through `mxcli docker build`, `docker run` and `docker reload` — all three reporting success while rewriting the project. The guard now lives on the operation rather than on a call site. runUpdateWidgets snapshots the v2 storage, runs the step, and returns the restore func for the caller to defer; both call sites are one line and there is exactly one place in the tree that may exec `update-widgets`. A third caller cannot reintroduce the bug by forgetting the snapshot. Restore stays deferred rather than immediate: the caller's `mx check` and MxBuild have to see the widget-normalized model, or the false CE0463 errors the step exists to suppress come straight back. Only the on-disk format is put back, after the caller is done with the model. Note on scope: the issue reports `mxcli test` as affected, giving this more exposure than mendixlabs#763. It is not — update-widgets sits inside build.go's `if !opts.SkipCheck` block and testrunner invokes `docker build --skip-check`. `run --local` is also unaffected; it goes through mxserve and never calls update-widgets. The three affected commands are `docker build`, `docker run` and `docker reload`, all at their default settings. Tests: unit coverage for the four paths through runUpdateWidgets (v2 conversion undone; v1 needs no snapshot and restore is a no-op; an unsnapshottable v2 project skips the step entirely rather than risking an unrecoverable conversion; a failed step still restores). Removing the protection fails three of the four. Plus an integration counterpart to TestCheck_PreservesMPRv2StorageFormat that drives Build with DryRun — which stops right after the check step, exercising the whole buggy path without paying for a full MxBuild. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
docs(fix-issue): restore the microflow loop symptom rows
fix(docker): stop `docker build` converting MPRv2 projects to MPRv1 (mendixlabs#808)
BREAKING CHANGE: every statement inside a microflow or nanoflow body must now end with `;`, including block terminators (`end if;`, `end loop;`, `end while;`, `end case;`). Omitting one is a parse error, where it was previously accepted. Semicolons were optional on all 54 body-statement alternatives, which made statement boundaries ambiguous whenever a keyword can both continue a statement and start one. The concrete case that surfaced it: adding a trailing `commit` modifier to create/change for mendixlabs#779 would let $a = create Mod.E commit $b; bind `commit` to the create rather than starting a commit activity. Diagnosing that per keyword does not scale; requiring the terminator removes the class. Scoped deliberately to microflow/nanoflow BODIES. The document terminator (`end;` / `end` then `/`) stays optional, as it is for pages and every other document type — measured, that variant breaks 88 of 234 example files, 75 of them pages, because block-bodied documents have never required a terminator and three styles coexist in the corpus (168 bare `}`, 56 `};`, 62 `}` + `/`). Tightening that is a separate, much larger decision. Impact measured over all 273 files in mdl-examples: 2 needed fixing, both genuine omissions (`end loop` with no `;`, a `log` line with no `;`), plus a `raise error` inside an on-error block. Example corpus is back at baseline parity (234 pass / 39 fail, 0 regressions). Test fixtures embedding MDL needed the same treatment in enginecompare, executor and visitor. Doc and skill code blocks were extracted and parse-checked against both the old and new parser: 0 regressions, so no documentation migration was required. Diagnostics were already precise for this ("line 143:2 missing ';' at 'return'"), so no error-reporting work was needed. Documented in write-microflows.md, write-nanoflows.md and MDL_QUICK_REFERENCE.md, each stating that the definition terminator remains optional. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
feat(grammar)!: require semicolons on microflow and nanoflow statements
mendixlabs#812) Every Forms$FormSettings / Forms$PageSettings mxcli wrote carried an empty Microflows$TextTemplate as its TitleOverride, where Studio Pro writes null. An empty template is not the absence of an override — it IS an override, to the empty string — so every popup opened by an mxcli-authored button or Show Page action rendered with a blank caption and just the close button. The reporter found 10 broken popups (all mxcli-authored) against 58 correct ones in the same project. The same unconditional write hid a second defect: an override the author did ask for, `show page M.P with title = 'X'`, was discarded. The syntax parses, the visitor sets ast.ShowPageStmt.Title and the builder sets ShowPageAction.OverridePageTitle — but no writer ever read that field. Before this change `grep -rn OverridePageTitle` matched exactly two lines: the struct field and its assignment. Both halves now round-trip. Two traps made this hard to see, and both are recorded in the symptom table. The "must be non-nil" comments cited issue mendixlabs#295 as justification. mendixlabs#295 was about Forms$PageVariable on a PageParameterMapping — a different field — and the conclusion was generalised to TitleOverride without ever being tested. It is false: with the null in place, against Mendix 11.6.6, mxcli docker check -> "The app contains: 0 errors." mxcli docker build -> "Build complete." so the loader accepts it, which is precisely what the old comments said it would not. This repo's own .claude/skills/debug-bson.md already documented `{Key: "TitleOverride", Value: nil}` as the correct Forms$FormSettings shape. Two tests asserted the incorrect behaviour and have been inverted, with the reasoning recorded so the reversal is not silently re-reverted. codec.RegisterTypeDefaults OVERWRITES rather than merges, and Forms$FormSettings was registered twice — in microflow_write.go and widget_write.go. Registrations resolve by init order, so the NullFields entry added beside the microflow writer was clobbered and the null never reached the output. Consolidated to a single registration, with a comment at the surviving site. Scope: the microflow Show Page and widget-action paths, where the evidence is. Navigation writes the same empty template but the reported evidence is about popups, so it is left alone rather than changed on a guess. Verified end-to-end against a real project, not just in unit tests: show page P; -> TitleOverride: null show page P with title = 'X'; -> Microflows$TextTemplate containing 'X' actionbutton (action: show_page P) -> TitleOverride: null plus the mx check and MxBuild runs above. Reverting the fix makes the new tests fail with exactly the BSON shape quoted in the issue. Repro script in mdl-examples/bug-tests/. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
`mxcli new --version 11.12.2` printed "Step 1/4: Resolving MxBuild 11.12.2..."
and then produced a Mendix 11.6.6 project, with no warning that the requested
version had been ignored. `mxcli setup mxbuild --version 11.12.2` downloads that
version fine, so the CDN was never the problem — the resolver simply never asked.
ResolveMxForNewProject delegated to ResolveMxForVersion, whose last resort is
AnyCachedMxPath(): any cached mx, of any version. That fallback is defensible
when the project already exists and its version is a preference. It is wrong for
`new`, where the requested version is not a preference but the definition of the
output — `mx create-project` stamps the new project with the version of the
binary that ran it, and every later step (init, mxbuild, runtime, run --local)
then follows the wrong version.
Two changes, because either alone leaves a silent path.
Resolution is now exact. localMxForVersion looks only for the requested version:
an exact-version Studio Pro install, an exact-version binary in the OS-specific
install locations, then the exact-version download cache. PATH is deliberately
excluded — an `mx` on PATH carries no version guarantee. Anything else
downloads. Windows and macOS still prefer an exact Studio Pro install, so they
do not fetch a Linux CDN binary that cannot execute.
And `new` now checks its postcondition rather than trusting the resolution: it
reopens the created .mpr, compares ProductVersion against --version, and exits
with the mismatch, the binary that caused it, and the setup command that fixes
it. A resolution bug is invisible without this — which is exactly how the
original went unnoticed.
Verified by reproducing the reported state (only 11.6.6 cached, 11.12.2
requested):
before -> silently created a Mendix 11.6.6 project
after -> downloads 11.12.2, creates a Mendix 11.12.2 project
and by reverting only the resolver, which now fails loudly instead of silently:
Error: requested Mendix 11.12.2 but the created project is 11.6.6.
mx create-project stamps the project with the version of the binary that ran it (.../11.6.6/modeler/mx).
Run 'mxcli setup mxbuild --version 11.12.2' and try again.
Unit tests cover the resolution decision without touching the network: a cached
11.6.6 must not satisfy a request for 11.12.2, an exact match must still be
reused rather than re-downloaded, and an empty version matches nothing.
Found while building a project to verify mendixlabs#812 in a browser; it cost a full
project rebuild before the wrong version was spotted.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
Adds .claude/skills/verify-in-runtime.md and the checklist rule that routes to it, so browser-level verification is a normal step for the bugs that need it rather than something rediscovered per incident. The motivating pattern: twice this week a green test suite coexisted with a live bug. In mendixlabs#812 a duplicate codec.RegisterTypeDefaults for the same $Type silently clobbered the fix — registrations overwrite rather than merge and resolve by init order — so the unit test passed while the emitted BSON stayed wrong. In mendixlabs#808 the integration test had only ever skipped, for want of an mx binary, and a skip reads exactly like a pass. A green suite is evidence about the layer it tests and nothing more. mendixlabs#812 is the case the new tier exists for: every popup opened by an mxcli-authored button rendered a blank caption, yet the BSON was structurally valid, mx check reported 0 errors and MxBuild completed. Nothing below the browser could see it, because the defect WAS the rendering — an empty Microflows$TextTemplate is not "no title override", it is an override to the empty string. The trigger rule is deliberately narrow: use the layer the symptom lives in. Parser to unit test, BSON to a test on the encoded document, on-disk effects to an integration test, and only render-time behaviour to the runtime tier. Of four fixes this week exactly one qualified; mendixlabs#808, mendixlabs#779 and the semicolon change did not. The skill records the procedure and, more usefully, the traps that cost the most time: cache MxBuild for the version under test explicitly; create the scratch project at a SHORT path, because Mendix throws PathTooLongException on deep ones and in Go tests that turns into a silent skip (set TMPDIR); install the Playwright package but never the browsers; and A/B against a pre-fix binary, since a run that only shows the fixed state demonstrates the app works, not that the change is why. Also the mundane one that nearly landed in a commit: restore files with absolute paths after a mutation test, and finish with git status. Adds a companion checklist item requiring every bug fix to demonstrate its test detects the bug, by reverting the fix and watching it fail. Contributor-only: the top-level .claude/skills/ are not synced into user projects by mxcli init (only .claude/skills/mendix/ is). Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
Resolves the recurring conflict in .claude/skills/fix-issue.md: every fix appends a symptom row at the same anchor, so any two branches that both add one collide. Both sides kept — this branch's mendixlabs#812 row plus the two loop rows that arrived from #63. Main has since gained #64 (runUpdateWidgets) and #66 (mandatory microflow semicolons). Parser regenerated against the new grammar; full suite green, and the example corpus is unchanged at 235/39 including this branch's repro script.
Same recurring conflict as #68: .claude/skills/fix-issue.md, where every fix appends a symptom row at the identical anchor. Both sides kept. check.go auto-merged cleanly — #64's runUpdateWidgets and this branch's localMxForVersion touch different functions. Parser regenerated against #66's grammar; full suite green.
fix(pages): write a null TitleOverride, unblanking every popup caption (mendixlabs#812)
Every bug fix touches .claude/skills/fix-issue.md, and new rows went in directly under the table header. Two branches fixing unrelated bugs therefore inserted at the same line, which git cannot merge — a conflict by construction rather than by bad luck. It cost five separate resolution rounds in one week (#59-#62 three times, then #68 and #69), and each round risks silently dropping a row: one earlier round was "resolved" by deleting a PR's rows entirely and restoring them in a follow-up. Appending puts each branch's insert at a different offset, which git merges without help. The table is unordered — it is looked up by matching a symptom, not read top to bottom — so position carries no meaning and appending costs nothing. Recorded in both places a contributor might look: the skill's How to Use, with the reasoning so it is not "fixed" back, and the PR checklist item in CLAUDE.md. Existing rows are deliberately left where they are. Reordering them would conflict with every open branch at once, which is the problem this change exists to avoid. Folded into this branch rather than opened separately: it edits the same How to Use block, so a sixth branch would have manufactured exactly the conflict it is meant to prevent. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
#68 landed, putting its symptom row at the top of the table — the same line this branch inserts at, so the conflict recurred immediately after the previous resolution. That is the pattern, not bad luck. Resolved by keeping main's rows and moving this branch's row to the END of the table, which is the convention #70 introduces. Applying it here rather than waiting: this branch is the exact case it exists for.
fix(new): resolve the exact Mendix version, and verify what was created
docs(skills): verify a fix at the layer its symptom lives in
CommitType declared two values that do not exist in the Mendix metamodel: "YesWithEvents" and "NoEvent". The canonical set is Yes / YesWithoutEvents / No (modelsdk/gen/microflows CommitEnum, generated from Mendix reflection data, and docs/05-mdl-specification/10-bson-mapping.md). Nothing crashed, because the read path passes the stored string through untouched — but the named constants matched nothing in a real project, so any code switching on them fell through to its default. The giveaway was mdl/backend/mcp, whose mfCommitType had to translate CommitTypeNoEvent into "YesWithoutEvents" to produce a correct PED value, mapping a constant named "no events" onto "yes, without events". Replaces the set with the canonical three and simplifies mfCommitType, which no longer needs the translation. A new test pins CommitType against the generated CommitEnum in both directions, so neither a missing value nor an invented one can drift back in. Groundwork for mendixlabs#779 (exposing the flag in DESCRIBE MICROFLOW), which needs a trustworthy value set to render. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…mendixlabs#779) MDL had no syntax for the Commit setting, and the builder hardcoded CommitTypeNo at both call sites. Every create/change authored through MDL was therefore written as Commit: No regardless of intent — and a describe → edit → re-exec round-trip silently turned committing activities into non-committing ones. That is a data-fidelity bug, not just a missing feature: it is exactly the object-orphaning failure mode mendixlabs#779 reports chasing in Studio Pro. Syntax follows the existing `refresh` modifier on change, spelling all three values of Mendix's Microflows$Commit enum: $Order = create Sales.Order (Number = 'X'); -- No (default) $Order = create Sales.Order (Number = 'X') commit; -- Yes $Order = create Sales.Order (Number = 'X') commit without events; change $Order (Status = 'Paid') commit refresh; The default stays unwritten, so existing scripts are unaffected and turning the flag on is a one-word diff. `COMMIT` both modifies a create/change and starts a standalone commit activity, so the two could be confused. Mandatory statement semicolons make it decidable: the modifier cannot absorb a following `commit $Var` across a terminator. Two tests pin that specifically — a create followed by a commit activity, and a create carrying the modifier AND followed by one. Wired grammar → AST (CommitFlag) → visitor (buildCommitClause) → builder (commitTypeOf). DESCRIBE emission is the next commit; until then the flag is authorable and persisted but not yet rendered. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…abs#779) The reported symptom: DESCRIBE rendered a create/change activity with Commit enabled identically to one without, so transaction boundaries were invisible to automated analysis, linting and impact tooling — and confirming them meant opening Studio Pro activity by activity. The value was already read (both engines populate CreateObjectAction.Commit and ChangeObjectAction.Commit); the formatter simply never looked at it. It now renders the modifier introduced in the previous commit, omitting it for Mendix's default (No) so existing describe output is unchanged for the common case. Canonical order on change is `commit [without events]` then `refresh`, matching the grammar, so re-executing the output rebuilds the same activity. Round-trip is asserted rather than assumed: TestFormatAction_CommitRoundTrips formats each of the three values, re-parses the emitted MDL through the visitor, and requires the flag to come back identical — which is mendixlabs#779's second acceptance criterion. A formatter emitting syntax the grammar cannot read would be worse than not emitting it at all. Adds mdl-examples/doctype-tests/779-commit-flag.mdl covering all three values on both create and change, with and without member lists, and the modifier sitting next to a standalone COMMIT activity. Docs: MDL_QUICK_REFERENCE.md create/change rows, write-microflows.md CREATE and CHANGE sections, and the `microflow.object-operations` syntax topic — each distinguishing the modifier from the standalone COMMIT activity, which is the easy thing to confuse. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
feat(microflows): expose the Commit flag on create/change activities (mendixlabs#779) — recovered from #67
… → env → prompt) The existing web docs were scattered (bootstrap prompt + a run-local aside) and didn't describe the end-to-end path. Add a dedicated getting-started page and wire the surrounding workflow together. - docs-site: new tutorial page "Developing on the Web (Claude Code)" covering the whole workflow — create a GitHub repo, get a hub key from hub.mxcli.org for browser preview, create a Claude Code environment (MXCLI_HUB_KEY + network policy), paste the bootstrap prompt, iterate with the warm loop / --hub / screenshots, and how sessions self-bootstrap after idle. Wired into SUMMARY. - docs-site: the bootstrap prompt now creates a FINDINGS.md and logs to it, commits it, and includes an optional --hub browser-preview step. - CONTRIBUTING.md: document the end-to-end web dogfooding loop (build a real app in Claude Code web, keep a FINDINGS.md, feed it back) under Agentic Code Testing. - README: rewrite the "From the web or an iPad" section into a numbered workflow and link the new page. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
docs: document the full Claude Code web workflow (repo → hub key → env → prompt)
This was referenced Aug 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Eleven commits from
ako/mxcli. Four issue fixes, one breaking language change, and the process work that came out of verifying them.Fixes
Show Page actions get an empty TitleOverride instead of null, blanking every popup's caption #812 — Show Page actions blanked every popup caption.
TitleOverridewas written as an emptyMicroflows$TextTemplateinstead ofnull. An empty template is not the absence of an override, it is an override to the empty string. Reproduced and closed in a browser on Mendix 11.12.2 (run --local+ Playwright): pre-fix caption""with header content just×, post-fix"Order Details Popup".show page M.P with title = 'X'reached the model and was then discarded — no writer ever readOverridePageTitle. Both cases produced identical BSON, which is why it went unnoticed. Both now round-trip.must be non-nilcomments cited issue [Bug] CREATE SNIPPET with Params + $currentObject page call creates null PageParameterMapping — project fails to open in Studio Pro #295, which was aboutForms$PageVariable— a different field. Disproved: with the null in place,mx checkreports 0 errors and MxBuild completes. Two tests asserting the old behaviour are inverted, with the reasoning recorded inline so the reversal is not silently re-reverted.docker build (and therefore docker run / mxcli test) silently converts MPRv2 projects to MPRv1 via mx update-widgets, deleting mprcontents — same root cause as #763, different call site #808 —
docker buildconverted MPRv2 projects to MPRv1, deletingmprcontents/. mxcli docker check silently converts MPRv2 projects to MPRv1 (via mx update-widgets), deleting mprcontents #763/fix(docker): preserve MPRv2 storage format across docker check #764 fixed this forcheck;build.gocarried its own baremx update-widgetsinvocation, sodocker build,docker runanddocker reloadkept converting projects while reporting success. The guard now lives on the operation (runUpdateWidgets), not on a call site — there is exactly one place in the tree that may execupdate-widgets. Verified against a realmx: the integration test fails on pre-fix code with the reported symptom and passes after.mxcli testas affected. It is not —update-widgetssits insideif !opts.SkipCheckand the test runner passes--skip-check.run --localis also unaffected.Feature Request: Expose Commit flag on CREATE/CHANGE activities in DESCRIBE MICROFLOW #779 — Commit flag invisible in
DESCRIBE MICROFLOW. The larger half was that MDL had no syntax for it and the builder hardcodedCommit: Noat both call sites, so every create/change mxcli authored was non-committing regardless of intent, and describe → edit → re-exec silently cleared commit boundaries. New syntax follows the existingrefreshmodifier:create ... commit,commit without events,change ... commit refresh; default omitted, so existing scripts are unaffected. Round-trip is asserted, not assumed.CommitType, which declared two values Mendix does not define (YesWithEvents,NoEvent); the canonical set isYes/YesWithoutEvents/No, now pinned against the generated enum in both directions.mxcli new --version Xsilently produced a project at a different version. Resolution fell through to "any cached mx, any version", so with only 11.6.6 cached,--version 11.12.2printed "Resolving MxBuild 11.12.2…" and created an 11.6.6 project. Resolution is now exact, andnewverifies its postcondition by reopening the created.mprand comparing versions.Breaking
end if;,end loop;). The document terminator (end;/end+/) is unchanged and still optional, as for pages.#779commit modifier decidable only once this landed.Process
.claude/skills/verify-in-runtime.md— proving a fix in a real app in a real browser, with a narrow trigger rule: use the layer the symptom lives in, and reach for the browser only when the symptom is a property of the running app rather than of the file we write. One of four fixes here qualified.codec.RegisterTypeDefaultssilently clobbering a fix (registrations overwrite, resolved by init order), and an integration test that had only ever skipped for want of anmxbinary.Also included
Full Go suite green; example corpus at parity plus the two new repro fixtures