Skip to content

Fixes for #812, #808, #779 and mxcli new; mandatory microflow semicolons; runtime verification workflow - #816

Merged
ako merged 24 commits into
mendixlabs:mainfrom
ako:main
Aug 1, 2026
Merged

Fixes for #812, #808, #779 and mxcli new; mandatory microflow semicolons; runtime verification workflow#816
ako merged 24 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Eleven commits from ako/mxcli. Four issue fixes, one breaking language change, and the process work that came out of verifying them.

⚠️ Breaking change — semicolons are now mandatory inside microflow and nanoflow bodies. Details below; migration was 2 files across the whole 273-file example corpus.

Fixes

Breaking

  • Semicolons are mandatory in microflow/nanoflow bodies, including block terminators (end if;, end loop;). The document terminator (end; / end + /) is unchanged and still optional, as for pages.
    • Measured before committing: breaks 2 of 273 example files, both genuine omissions. Tightening the top-level terminator instead would have broken 88, of which 75 are pages — deliberately not done.
    • Motivation: statement boundaries were ambiguous wherever a keyword can both continue and start a statement, which is what made the #779 commit 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.
    • Motivating evidence: twice this week a green suite coexisted with a live bug — a duplicate codec.RegisterTypeDefaults silently clobbering a fix (registrations overwrite, resolved by init order), and an integration test that had only ever skipped for want of an mx binary.
  • New checklist item: every bug fix must show its test detects the bug — revert the fix, watch it fail.
  • Symptom rows now append at the end of the table. Every fix touches that one file and rows went in under the header, so concurrent fixes conflicted at the same line by construction — five resolution rounds in one week.

Also included

Full Go suite green; example corpus at parity plus the two new repro fixtures

claude and others added 24 commits July 31, 2026 03:06
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)
@ako
ako merged commit 504aec6 into mendixlabs:main Aug 1, 2026
8 checks passed
This was referenced Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants