Skip to content

Sync ako/mxcli: microflow rewrite carries, run --local resolution, version gating, catalog entry-point edges - #1133

Merged
ako merged 27 commits into
mendixlabs:mainfrom
ako:main
Sep 17, 2026
Merged

ako merged 27 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

15 commits from the fork's main, on top of the last sync. Grouped by theme.

Microflow properties silently dropped on rewrite (#1120 and follow-ups)

CREATE OR MODIFY MICROFLOW stamped literals over three properties it never read back, so editing only a microflow's body deleted them. All three were invisible to mxcli check, mx check and mxbuild — the resulting microflow is valid, just diminished.

  • Deep-link URL (48421a1) — Studio Pro's URL field and its search parameters were reset to empty. Carried at all three layers; DESCRIBE now emits a -- URL: comment.
  • Export level (18306e5) — pinned to Hidden by every rewrite, quietly shrinking a module's public surface. Needs a default as well as a carry, since "" is not a member of MicroflowsExportLevel. Measured against three marketplace modules: 55 of 55 documents store Hidden, so the constant was a default in disguise.
  • Concurrency settings (29cc3c0) — re-enabled concurrent execution on a microflow that disallowed it, and dropped the error message with its translations. The round-trip test guarding this has been green since Microflow read/write round-trip drops fields, and default auto-layout misplaces elements #723: the executor stamped its literals one layer above the backend. CE4899 only fires in the opposite direction, so the one relevant checker was structurally blind to it.
  • End-to-end verification (608e4c1) — the first real mxbuild 11.6.6 run found the URL fixture described a document Mendix refuses to build (CE5612: a URL path parameter may not also be a search parameter), and that bin/mxcli was stale, producing a result indistinguishable from a second defect.

run --local resolution and client support

Version resolution and gating

  • Mendix 9/10 CDN build numbers (0c576e6) — mxcli new --version 10.24.25 died with a bare 404. Mendix 9 and 10 publish four-part artifact names carrying a build number the release notes never mention; 11 publishes three. The CDN answers ListObjectsV2, so the build is enumerated rather than guessed. An unresolvable version now lists what is published for that major.minor.
  • Page parameters are 9.4, not 11.0 (15825d0, fixes CREATE PAGE with Params: is refused on Mendix 10 — no way to create a parameterised page #1121) — parameterised pages were refused on every Mendix 10 project. The 11.0 floor was never measured; it matched a proposal's illustrative sample table and nothing else. Measured against the Model SDK's own StructureVersionInfo: the element is 9.4, only its optional/default-value half is 11.5 — so the registry gates the element and the writer gates the tail. 11.0–11.4 passed the old gate and got both 11.5-only keys written, which is the regression control.

Catalog: entry points are no longer reported dead

Two entry-point kinds invoked by the platform had no inbound reference edge, so show callers of, GRAPH_DEAD_ASSETS, impact and lint rule QUAL004 all agreed the microflow was unreachable — with QUAL004 suggesting deletion.

Docs and skills

  • Bootstrap gates aligned (2ee8d83) — the gate list was stated in three places that had drifted; mxcli test was on none of them, so testing was reachable only by asking for it by name. Now one ordered list in Go, rendered into the generated file, held together by a test that every gate is a real command named in all three.
  • fix-issue routed, plus two rules (1db507c) — only .claude/commands/ entries appear in a session's skill listing, so a top-level dev skill was a document you had to already know about. Adds /mxcli-dev:fix-issue. The two rules: run the real thing when the argument is about what a Mendix tool accepts, and treat "cannot be verified here" as a claim needing evidence — a negative uniform across an entire class is evidence about the query, not the class.
  • sdk/mpr swept out of the instructional layer (5351edc) — two of the affected skills are synced into every user project by mxcli init, so users were handed paths that had not existed for two releases. Also replaces a Go snippet importing a module path this repo never had with the bson dump command it exists for, and fixes that command's own --help example, which was wrong in five places. Dated records (proposals, ADRs, plans) are deliberately left alone

ako and others added 27 commits September 17, 2026 06:14
Named datasources for multi-source pluggable widgets (mendixlabs#1109)
…#1120)

A microflow's URL (Mendix 10.6+) — Studio Pro's "URL" field, e.g.
`item/{Key}` — was deleted by any CREATE OR MODIFY MICROFLOW, including
one that edited only the body.

The value had no path across a rewrite at any of the three layers:
`microflowToGen` wrote `SetUrl("")` and `SetUrlSearchParametersQualifiedNames(nil)`
unconditionally, `microflowFromGen` never read either back, and the
semantic microflow had no field to hold them. Carry all three, and seed
the executor's rewrite from the stored microflow, exactly as
AllowConcurrentExecution / MarkAsUsed / ApplyEntityAccess already are.

This one had nothing behind it: a microflow without a URL is a valid
microflow, so `mxcli check`, `mx check` and mxbuild all reported success
before and after, and the loss was visible only in Studio Pro. There is
no refusal to fall back on — preserving is the whole remedy.

DESCRIBE now emits the URL as a `-- URL:` comment. It is not
re-executable MDL because there is nothing to execute, but a
describe -> rename -> exec copy has nothing to preserve from, so the
output says so instead of silently omitting it.

Controls: reverting either half of the backend fix alone fails
TestMicroflowRoundTrip_DeepLinkURL with the reported symptom (Url = ""),
and neutralising the executor carry fails
TestCreateOrModifyMicroflow_PreservesDeepLinkURL. Both directions are
pinned — a microflow with no URL must not acquire one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ssUKiZ9ekBvzSNM5VCVoP
The gate list was stated in three places that each reach a different
reader, and they had drifted apart:

  - the generated CLAUDE.md/AGENTS.md, re-read into every session, so it
    is what an agent does by default without being asked;
  - the bootstrap-app skill, which provisions the project and hands over;
  - the bootstrap-prompt docs page, which a human reads before pasting
    the seed prompt.

`mxcli test` appeared in the skill only as a ports aside ("avoid
8081/8091/6544") and on no gate list at all, so testing was reachable
only by a user asking for it by name — while `check`, repeated in 41
skills, ran almost every time. `docker check` was on the CLAUDE.md list
and in neither of the other two. The docs page's step numbering had also
gone stale ("provisioning step 6" for what is step 7).

Changes:

  - projectGates is now one ordered list in Go, rendered into the
    generated file, with `test` added between `docker check` and
    `run --local`. The gates are stated as the definition of done.
  - A "Finishing a change" section carries the bootstrap's durable
    artifacts into the steady state: run the gates, `brain capture`
    requirements as they arrive, append to FINDINGS.md, commit. Without
    it those files are written once at bootstrap and quietly stop being
    true. Generated file is 5,113 bytes, within the 6,000-byte budget.
  - bootstrap-app gains a quality-baseline step (lint + the scored
    report on the blank app, recorded in FINDINGS.md) — the only moment
    those numbers mean "what the template ships with" — and states the
    same gate list, with the rule that the first test is written with
    the first microflow rather than later.
  - The docs page mirrors the skill's steps, including the baseline, and
    publishes the same gate list.

Held together by init_claudemd_gates_test.go: every gate must be a real
cobra command and must be named in all three. Controls: the skill test
fails against the pre-change skill (lint/report/docker check), and
dropping the `test` line from the docs page fails the docs test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HK2imrM8M5UbzhTvttb5YC
…he cache

`run --local` and `test --local` resolve the mxbuild this host can execute
(ResolveMxBuildForLocal, mendixlabs#916) and then passed it only to the web-client
bundler. Both ServeOptions literals omitted MxBuildPath, so StartServe
re-resolved from ~/.mxcli/mxbuild/ and, on a miss, took the newest cached
version whatever it was — so a Mac with the matching Studio Pro installed
built with a Linux download or with 11.14.0 against an 11.12.2 project.

The half-populated cache directory that causes the miss is mxcli's own:
ensureMxBuildRuntimeSibling creates ~/.mxcli/mxbuild/<ver>/ to hold the
runtime symlink, and on macOS nothing puts a modeler/ beside it. So this is
the guaranteed path for every macOS project whose version was not already
downloaded, not a damaged-cache edge case.

- both call sites construct options through one serveOptionsFor, so they
  cannot drift apart again
- resolveServeMxBuild no longer substitutes a different version for a named
  one; it refuses up front, naming what the cache holds, instead of handing
  mxbuild a binary it rejects minutes later after a cold model load
- test --local now resolves at all: it still called DownloadMxBuild directly,
  the call mendixlabs#916 removed from runlocal.go and left in localapp.go
- the failure message no longer sends macOS users to `setup mxbuild`, which
  prints Studio Pro's path and caches nothing (mendixlabs#1124)

Fixes mendixlabs#1122. Reduces mendixlabs#1124 to its guidance text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qk7hHmsPXfeEMAGphVA4H
`mxcli run --local --mxbuild-path …` answered "unknown flag", while the
shipped run-local skill, runlocal.go's comment and two resolution error
messages all tell users to pass it. On macOS it was the only advertised
way out of a platform mismatch.

The plumbing behind the flag already worked — LocalRunOptions.MxBuildPath
is declared and ResolveMxBuildForLocal honours it — so only registration
and the field assignment were missing.

The regression test asserts the invariant rather than the instance: any
--flag an mxbuild-resolution message tells users to pass must be a flag
`run` accepts. Guidance naming an option the command rejects is worse than
no guidance, because it reads as the user's mistake.

Fixes mendixlabs#1125.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qk7hHmsPXfeEMAGphVA4H
…1120 follow-up)

A microflow's export level — Studio Pro's Hidden/API switch, which decides
whether it is part of the module's public surface when the module is
exported as a package — was pinned to "Hidden" by every rewrite. Same
three-layer gap as the deep-link URL in the same function, and the same
silence behind it: a hidden microflow is a valid microflow, so nothing
downstream notices the module's API getting smaller.

Found by the mechanical audit the URL fix prompted — grepping
microflowToGen for the constants it writes.

Unlike the URL, this one needs a DEFAULT as well as a carry. "" is not a
member of MicroflowsExportLevel, and writing a value the metamodel does
not declare is the unloadable-model hazard: mxbuild tolerates it, Studio
Pro throws. A fresh microflow and a stored document that says nothing
both get "Hidden" — what the line always wrote — so the common case is
byte-identical. The precedent is json_write.go.

Measured, and it is what shaped the fix: across three real marketplace
modules (Business Events 3.12.0, External Database Connector 6.2.3 and
6.3.0) every document of every type stores "Hidden" — 3 of 3 microflows,
55 of 55 documents — all three exporting at module level "Source". So the
hardcoded value was not wrong, it was a default masquerading as a
constant. DESCRIBE therefore emits `-- Export level:` only when the value
is not "Hidden", alongside the URL comment.

Rules keep their hardcoded "Hidden" (2 of 2 reference rules, and a rule
is not independently callable); the now-stale cross-reference in
rule_write.go is corrected and says what would change that.

Controls: pinning the writer back to "Hidden", stubbing the reader, and
neutralising the executor carry each fail a different test with the
reported symptom; stubbing the describe condition fails the output test,
whose own control proves it stays quiet on defaults.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ssUKiZ9ekBvzSNM5VCVoP
`CREATE PAGE ... (Params: ...)` was refused on every Mendix 10 project, so
no parameterised page could be authored from MDL there at all — and without
one, `SHOW PAGE Mod.P ($X = $obj)` has nothing to bind to.

The 11.0 floor was never measured. `pages.page_parameters`,
`microflows.show_page_with_params` and `pages.page_variables` all matched the
illustrative `show features` sample table in
docs/11-proposals/PROPOSAL_version_aware_agent_support.md and nothing else.
Measured against the Mendix Model SDK's own StructureVersionInfo records
(mendixmodelsdk 4.115.0, src/gen/pages.js):

  Pages$PageParameter, Page.parameters       9.4.0
  Pages$PageSettings.parameterMappings       9.7.0
  Pages$LocalVariable                       10.17.0
  Pages$LocalVariable.defaultValue          10.20.0
  Pages$PageParameter.isRequired            11.5.0
  Pages$PageParameter.defaultValue          11.5.0

So the element is 9.4 and only its optional/default-value half is 11.5 — one
floor cannot express that. The registry now gates the element and the writer
gates the tail: pageParameterToGen emits IsRequired/DefaultValue only on
11.5+. MDL can express neither (every parameter it writes is required with no
default), so below 11.5 they are two keys the project's metamodel does not
declare — mxbuild accepts unknown properties, Studio Pro throws
InvalidOperationException at MprProperty.cs. Lifting the gate without that
guard would have traded an honest refusal for a page Studio Pro cannot open.

The same split shows the bug was live on released 11.x too: 11.0–11.4 passed
the old gate and got both 11.5-only keys written. That row is the regression
control in the writer test.

The executor gate itself stays — it is still correct for 9.0–9.3 — and the
floors now carry a `notes:` naming the measurement source, so the next reader
does not re-derive them from a proposal's sample output.

Page variables are set to 10.20.0 rather than 10.17.0 because MDL always
writes a default expression, and DefaultValue is the later of the two.

Tests: sdk/versions/page_parameter_floor_test.go (each floor plus the version
below it), mdl/backend/modelsdk/page_parameter_version_test.go. Both were run
against the unfixed code as controls: the registry test reports
`page_parameters at 10.24.25 = false` (the reported symptom) and the writer
test reports both keys emitted at 10.24.25, 9.4.0, 11.4.0 and unknown.

Fixes mendixlabs#1121

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aHj6mJwKCZD7EX7wcD6jW
The catalog recorded each published REST operation's microflow in
published_rest_operations_data.Microflow and emitted no `refs` edge to it,
so a microflow whose only caller is an endpoint had zero inbound
references. GRAPH_DEAD_ASSETS listed it, SHOW CALLERS OF answered "(no
callers found)", SHOW REFERENCES TO and impact reported nothing, and lint
rule QUAL004 said "is not called from anywhere. Remove if unused." On the
reported model 92 of 93 operations name a microflow and all 92 were listed
dead -- 15 percent of its dead-microflow list, aimed at the most exposed
code in the app.

A published REST operation is an entry point of the same shape as a
scheduled event: the platform invokes it, so nothing in the model calls
the microflow it runs. It now emits a `publish` edge from the operation to
that microflow. The source is the operation rather than the service, so
`show references to` names the one endpoint instead of the service holding
thirty of them, and the edge carries the operation's catalog id, so its
path and summary are one join away.

Two corrections to the report's diagnosis, both measured. GRAPH_DEAD_ASSETS
is kind-agnostic -- it asks only whether any refs row targets the name --
so the edge alone clears it; the comment beside `schedule` in graphRefKinds
claiming otherwise was never true (git log -L on the view) and is fixed.
impact and SHOW REFERENCES TO do not filter by kind either. `publish` is
still added to graphRefKinds, for the analysis graph: without it an API
handler is an unreachable root in communities, layers, cycles and
centrality.

Found while auditing the three vocabularies: `settings` was missing from
`show callers`. The project-settings edge shipped in v0.22.0 into refs and
into QUAL004 but not into callerRefKinds, so `show callers of
<AfterStartupMicroflow>` was still blind to it -- fixed here too. `sync` is
deliberately not a caller kind (it names an entity an offline profile
downloads, which is a use of a type) and the test now pins it in the
excluded set beside `datasource`.

Controls: stubbing extractPublishedRestRefs to emit nothing reproduces
"reported dead" verbatim, and dropping the empty-microflow guard fails the
two-operation test. The dead-assets test carries an unreferenced microflow
as its own control.

Fixes mendixlabs#1126

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACcuXVKhQTiYp1Suv7KTGY
An entity event handler runs a microflow on every create/commit/delete of
its entity. The catalog reduced the whole list to
CATALOG.ENTITIES.HasEventHandlers — a flag saying some exist and nothing
else — and emitted no `refs` row at all, so every consumer of the
reference graph agreed the handler microflow was unreachable:

  show callers of Mod.ACT_Order_Validate  -> (no callers found)
  CATALOG.GRAPH_DEAD_ASSETS               -> lists it
  mxcli lint                              -> [QUAL004] "is not called from
                                             anywhere. Remove if unused"

Same class as the scheduled-event gap and worse in degree: a handler runs
on every commit, so the false "dead" verdict lands on code hotter than
most of what IS reported as live, and the lint suggestion is to delete it.

Both halves land together, as they did for offline sync configs:

* CATALOG.ENTITY_EVENT_HANDLERS — one row per handler with the moment,
  the event, the microflow, RaiseErrorOnFalse and PassEventObject. The
  distinction the flag lost is not cosmetic: a Before handler with
  `raise error` can veto the commit, an After handler cannot.
* An `event` edge in refs (ENTITY -> MICROFLOW), carrying neither the
  moment nor the event. refs has no column for them, and a kind per
  combination would put eight kinds into every consumer's list to say one
  thing.

The three consumers of the reference graph keep independent lists, so the
kind is added to all of them: callerRefKinds, graphRefKinds and the
bundled QUAL004 Starlark rule. CatalogSchemaVersion is bumped 11 -> 12 for
the same reason 11 was: refs are only written by REFRESH CATALOG FULL and
NewFromFile applies the schema with CREATE TABLE IF NOT EXISTS, so without
the bump an existing .mxcli/catalog.db would gain the empty table and keep
serving the pre-fix edge set.

Verified end to end on a project with two handlers, with a control: the
extractor stubbed to emit nothing reproduces "(no callers found)" and two
dead microflows; the fix reports the entity as the caller, no dead assets
and no QUAL004. The same control is inside
TestEventEdgeClearsTheDeadAssetVerdict, and removing the kind from
graphRefKinds or the Starlark rule fails its own guard.

Fixes mendixlabs#1127

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BS9HVaoNfubFxWm53kLwM
…dixlabs#1120 follow-up)

`CREATE OR MODIFY MICROFLOW` re-enabled concurrent execution on a
microflow that disallowed it — removing the running app's concurrency
protection — and dropped the concurrency error message with all its
translations, the error microflow, and MarkAsUsed.

Two things made this the worst of the three drops in this function, and
both are about direction rather than severity.

The backend already read AllowConcurrentExecution and MarkAsUsed back;
TestMicroflowRoundTrip_ConcurrentExecutionFlags has guarded them since
mendixlabs#723 and was green throughout. buildMicroflowFromStmt stamps its own
literals over both before the backend is ever called, so the guard was
one layer below the bug. When a property is reset, the last writer on the
path is the one that matters.

And CE4899 fires on disallow-without-a-message, never on allow. mendixlabs#723's
bug wrote Go's zero value (allow -> disallow) and hit CE4899 at once;
this one writes the opposite (disallow -> allow), so the single error
that covers this area is structurally blind to it. A checker that catches
a loss in one direction is not coverage for that property.

Carried from the stored microflow, with the locals seeded to the
new-microflow defaults so a CREATE is unchanged (asserted). The error
message reuses the existing textFromGen/textToGen pair, so translations
survive; nil still emits the bare empty Texts$Text the writer always
wrote. ConcurrentExecutionSettings is marked Deprecated rather than
removed — nothing reads or writes it and Mendix stores no thread count,
but the type is exported.

microflowToGen now sources every property from the model; no hardcoded
constants remain.

Controls: hardcoding the executor literals back, emptying the writer's
pair, and stubbing the reader each fail a different test with the
reported symptom. Two measurement traps hit while writing those tests and
recorded in the finding: bytes.Equal on two encodes of one microflow
always differs (fresh sub-element $IDs), and canon.Equal on a whole
microflow always differs too (StableId is a fresh GUID value and Equal
does not mask) — compare the sub-element, or use Reconcile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ssUKiZ9ekBvzSNM5VCVoP
Mendix stores the rollback event as "RollBack" — capital B, confirmed
against generated/metamodel (DomainModelsEventRollBack), and disagreeing
with every neighbouring enum in that file, where the same word is
"Rollback". The value is stored verbatim rather than normalised, so a
query spelling it the expected way returns zero rows and not an error.
Documented on CATALOG.ENTITY_EVENT_HANDLERS and pinned by a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BS9HVaoNfubFxWm53kLwM
The unit tests covered the codec and the executor; nothing had written to
a real .mpr or asked mxbuild what it thought. Running it found two things
`go test` could not.

**The URL fixture described a document Mendix refuses to build.** It
paired `Url: "item/{Key}"` with `UrlSearchParameters: ["….Key"]` — the
same parameter — and mxbuild rejects that as CE5612: a parameter used in
the URL path may not also be a search parameter. The two sets are
disjoint. Nothing in a unit test validates the model, so the fixture was
free to be invalid. Both fixtures now use a distinct `Filter` parameter
and say why; the rule is documented in the microflow skill's pitfalls.

**And the first end-to-end run measured a stale binary.** `bin/mxcli`
predated two of the three commits, because each fix was followed by
`go build ./mdl/...` and `make test` but never `make build`. It produced
a result — URL survived, export level did not — indistinguishable from a
genuine second-read-path defect, and sent me looking for a duplicate
resolver that does not exist.

Measured on mxbuild 11.6.6, Mendix 11.6.6 MPR v2 project, microflow
seeded through mpr.NewWriter + UpdateRawUnit the way Studio Pro would
write it:

  seeded control    Url="item/{Key}"  search=[…Filter]  ExportLevel="API"
                    mx check: 0 errors   (the fixture is buildable)
  pre-fix binary    Url=""            search=[]         ExportLevel="Hidden"
                    mx check: 0 errors   (the loss is silent — the report)
  post-fix binary   Url="item/{Key}"  search=[…Filter]  ExportLevel="API"
                    mx check: 0 errors   (mxbuild accepts what mxcli wrote)

The middle row is the reporter's claim reproduced end to end, and the
last row closes the "verified by construction" gap over the non-empty
UrlSearchParameters by-name list, which nothing had ever built.

Recorded in the findings, including why the repo's own integration gate
did not catch any of this: TestMxCheck_DoctypeScripts skips whenever `mx`
is absent, which is every run in a fresh container.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ssUKiZ9ekBvzSNM5VCVoP
`mxcli run --local` exited 1 on any project with Web UI Settings >
OptimizedClient = No, reporting "the build did not produce a client" about a
deployment whose client was in the same directory.

The gate recognised two shapes — a rollup config to run, or a bundle mxbuild
already wrote — and treated everything else as a failed build. A classic
(Dojo) deployment is a third: it has neither, because the classic client has
no bundling step in any Mendix version.

Measured on one blank 11.12.2 app built twice, changing only that setting:
mxbuild swaps which client lands in deployment/web/ and parks the other
beside it. Yes -> React in web/ (+rollup.config.mjs), Dojo in dojo-web/.
No -> Dojo in web/ (index.html loading mxclientsystem/mxui/mxui.js, no
rollup config, no dist), React in react-web/ with its own rollup config.

Detection reads the deployment's own web/index.html, not the model setting:
the deployment is what gets served, the two disagree right after the setting
changes, and it covers MigrationMode without predicting what that emits. It
tests for the classic client on positive evidence — inferring it from the
absence of the React shapes would make every genuinely broken deployment look
classic and silently skip the bundle, which is the black screen this gate
exists to prevent.

The gate had five consumers, not one: the boot bundle, the --watch bundler,
the post-boot re-bundle guard, and ensureClientServed, which probes that
/dist/index.js is served (measured 404 on a classic app) — so fixing only the
boot would have moved the failure to every applied change under --watch. Two
of them carried hand-copied duplicates of the same gate and had drifted once
already (the 11.14 fix landed on BuildWebClient only), so they now switch on
one planWebClient rather than gaining a third copy.

Verified end to end on a real 11.12.2 classic app: boots, renders in Chromium
with the mx global present and no console errors, and hot-applies a model
change under --watch. testdata/webclient/ holds both entry points as mxbuild
writes them.

Fixes mendixlabs#1123.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qk7hHmsPXfeEMAGphVA4H
`mxcli new --version 10.24.25` died with a bare 404:

  HTTP 404 from https://cdn.mendix.com/runtime/mxbuild-10.24.25.tar.gz

Mendix 9 and 10 publish FOUR-part artifact names carrying a build number
the release notes never mention — the release called 10.24.25 is
mxbuild-10.24.25.122571.tar.gz — while Mendix 11 publishes three parts.
MxBuildCDNURL interpolates whatever string it is handed and nothing
resolved a partial version, so a hand-typed 10.x version named no
artifact at all and every probe of one 404'd. That reads as "Mendix 10 is
no longer on the CDN" rather than "that is not its name".

The CDN is an S3 bucket that answers ListObjectsV2, so what exists is
enumerable rather than guessable. ResolveCDNVersion returns a four-part
version untouched, returns a version the CDN serves as given (every
supported Mendix 11) without a listing call, and otherwise lists
runtime/mxbuild-<version>. and takes the highest build. Both the mxbuild
and runtime archives follow the same naming, so one resolved string
serves both.

Wired at the two entry points where a user types a version, `mxcli new`
and `mxcli setup mxbuild`. Project-driven paths were never affected: the
MPR's _ProductVersion already carries all four parts, and resolving a
four-part version is a no-op that does not touch the network. Resolution
happens before step 1 of `new` because the resolved string is what the
rest of the command must use — it checks the created project's stamp
against the requested version, and mx create-project stamps four parts.

An unresolvable version now lists what is published for that major.minor
instead of showing a 404.

Three details are load-bearing and each has a test: the .sha256 sidecar
beside every archive must not be picked as an artifact; the listing
prefix needs its trailing dot or 10.24.2 swallows 10.24.20..10.24.26; and
build numbers are not zero-padded, so a text sort puts 99999 above 122571
and 10.24.9 above 10.24.26. The truncation path is tested too, since a
page treated as complete would silently resolve to the wrong build.

Verified end to end: `mxcli new Verify1121 --version 10.24.25` now prints
"Resolved Mendix 10.24.25 to 10.24.25.122571" and creates a project
stamped 10.24.25.122571.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aHj6mJwKCZD7EX7wcD6jW
…axloot

# Conflicts:
#	.claude/lint-rules/orphaned_elements.star
#	mdl/catalog/builder.go
#	mdl/catalog/builder_graph.go
#	mdl/catalog/builder_references.go
#	mdl/catalog/lint_rule_vocabulary_test.go
#	mdl/executor/cmd_search.go
#	mdl/executor/cmd_search_callers_test.go
fix(run-local): mxbuild resolution and classic-client support for the local loop
…paid for

The guidance was not missing. fix-issue.md step 4 already said to verify at
the layer the symptom lives in and pointed at verify-in-runtime.md; step 5
already said to prove the test detects the bug. What failed on mendixlabs#1121 was
routing and two gaps in the rules themselves.

ROUTING. Only .claude/commands/ entries appear in a session's skill
listing. The .claude/skills/mendix/<name>/SKILL.md files already carry
name/description frontmatter and none of them are listed either — they are
for the user projects mxcli init syncs them into, where Claude Code scans
.claude/skills/. So a top-level dev skill is a document you have to already
know about, and nothing loads it when someone says "investigate issue N".
Adds /mxcli-dev:fix-issue as the routed entry point; the skill keeps the
mechanics and the command holds the order of work. It is under mxcli-dev/,
so it is not synced into user projects.

RULE 1 — run the real thing when the argument is about what a Mendix tool
accepts. verify-in-runtime.md asks whether the symptom is a property of the
running app. That is right for a rendering bug (mendixlabs#812) and does not fire on a
version gate: mendixlabs#1121's unit tests were sound and proved what they claimed.
What they could not touch is the claim the fix rests on — that writing two
11.5-only properties below 11.5 is unsafe. Building a real 10.24.25 app with
the keys forced back in settles it in one run: mxbuild reports 0 errors, so
nothing in the toolchain catches them and the guard is load-bearing rather
than decorative. Trigger is now explicit, with the two-copies recipe.

RULE 2 — "cannot be verified here" is a claim and needs a fix's evidence. It
ends the investigation, so it gets the least scrutiny and does the most
damage. On mendixlabs#1121 a dozen uniform 404s (with 11.x succeeding on the same host)
became "Mendix 10 is not downloadable from this environment", written into a
PR body as fact. It was false: Mendix 9 and 10 publish four-part names with a
build number. The tell was the shape of the evidence — a negative uniform
across an entire class is evidence about the query, not the class.

Also fixes the skill's stale tail, which is review.md rows 5 and 7 wearing a
package name: it sent tests to sdk/mpr (deleted when the legacy engine went),
told the reader to add a row to a symptom table that moved to findings/*.jsonl,
and carried a hardcoded /c/users/... go path. Replaced with the current
layer→package table and a checklist that includes the two rules. Adds
review.md row 24 for the class, since a deletion that leaves its guidance
behind is worse than no guidance — the reader trusts it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aHj6mJwKCZD7EX7wcD6jW
Follow-up to the previous commit, which found fix-issue.md sending tests to
a package deleted with the legacy engine. It was not alone.

The two SYNCED skills were the reason to do this now: mxcli init copies
mendix/debug-bson and mendix/validation-microflows into every user project,
so users were being handed paths that have not existed for two releases.

  sdk/mpr/writer_widgets.go     -> mdl/backend/modelsdk/widget_write.go
  sdk/mpr/writer_microflows.go  -> mdl/backend/modelsdk/microflow_write.go
  sdk/mpr/writer_entities.go    -> mdl/backend/modelsdk/domainmodel_write.go
  sdk/mpr/parser_microflow.go   -> mdl/backend/modelsdk/microflow_read_actions.go
  sdk/mpr/parser_page.go        -> mdl/backend/modelsdk/page.go

plus modelsdk/codec/{encoder,decoder}.go named as the layer underneath, which
the old tables never mentioned. Every target was checked to exist before being
written down.

Three things beyond a path rename:

debug-bson told the reader to write a throwaway Go program importing
github.com/mendix/modelsdk-go/sdk/mpr — a module path this repo has never
had, calling an API it does not export. mxcli ships `bson dump` for exactly
this, so the snippet is replaced by the command, including the compare form
that is the actual technique the skill is about.

That command was then documented wrong in five places as `mxcli dump-bson`,
which does not exist, and its `--compare` was shown as two positional
arguments, which the parser rejects — `--compare` is a StringSlice and needs
"A,B". The wrong form came from the command's own --help example, fixed here
too, since leaving it means the next reader copies it again. Every invocation
the skill now contains was run against a real 10.24.25 project.

Two references were a rule outliving its package rather than a stale path:
"the executor must not import sdk/mpr for writes" is now "must not reach past
ctx.Backend", which is what ADR-0002 actually says and stays true after the
next refactor.

Left alone deliberately: docs/plans/, docs/13-decisions/ and docs/11-proposals/
(~300 references). Those are dated records — several describe retiring sdk/mpr
— and rewriting them would falsify the history. ADRs are immutable by
convention. The sweep covers the layer that tells a reader where to work today.

Adds review.md row 25 for the class the CLI examples fell into: a documented
invocation nobody ran, copied from a --help that had the same error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aHj6mJwKCZD7EX7wcD6jW
docs(skills): route fix-issue, add the two rules mendixlabs#1121 paid for, sweep stale sdk/mpr guidance
@ako
ako merged commit def6d23 into mendixlabs:main Sep 17, 2026
5 checks passed
@github-actions

Copy link
Copy Markdown

AI Code Review

Let me review this PR based on the checklist provided.

Overview of the PR

This PR appears to be a sync from the "ako/mxcli" fork, bringing in various fixes and improvements grouped by theme:

  1. Microflow properties silently dropped on rewrite (CREATE OR MODIFY MICROFLOW silently drops the microflow's URL (deep link) property #1120 and follow-ups) - Fixes for deep-link URL, export level, and concurrency settings being lost during microflow rewrites
  2. run --local resolution and client support - Fixes for serving from resolved binary, classic-client apps, and --mxbuild-path flag
  3. Version resolution and gating - Fixes for Mendix 9/10 CDN build numbers and page parameters version gating
  4. Catalog: entry points are no longer reported dead - Fixes for published REST operations and entity event handlers
  5. Docs and skills - Various documentation and skill updates

Review Against Checklist

Overlap & duplication

  • The PR introduces new functionality, not re-documenting already-shipped features
  • I don't see obvious duplication with existing implementations (though I didn't check every function name)
  • The doctype test files mentioned show existing test coverage areas, and this PR adds new test files for the fixes

MDL syntax design (for new or modified MDL syntax)

  • I don't see any new MDL syntax being introduced in this PR. The changes are mostly about fixing existing functionality (preserving properties that were being dropped, fixing version resolution, etc.)
  • No grammar changes, AST changes, or new MDL statements are visible in the diff

Full-stack consistency (for MDL features)

  • Since there are no new MDL features being added, this section doesn't apply
  • The fixes are about preserving existing properties during rewrites, which is handled through the existing executor/backend pipeline

Test coverage

  • The PR adds numerous test files:
    • mdl/executor/microflow_carried_properties_test.go (229 lines)
    • mdl/backend/modelsdk/microflow_roundtrip_flags_test.go (182 lines)
    • mdl/catalog/builder_entity_events_test.go (203 lines)
    • mdl/catalog/builder_rest_refs_test.go (191 lines)
    • cmd/mxcli/docker/version_resolve_test.go (145 lines)
    • cmd/mxcli/docker/mxserve_resolution_test.go (115 lines)
    • cmd/mxcli/docker/webclient_classic_test.go (173 lines)
    • mdl/backend/modelsdk/page_parameter_version_test.go (108 lines)
    • sdk/versions/page_parameter_floor_test.go (75 lines)
    • cmd/mxcli/init_claudemd_gates_test.go (119 lines)
    • cmd/mxcli/docker/runlocal.go (modified)
    • And several others
  • These tests appear to be unit/integration tests that verify the fixes work correctly
  • I don't see any time.Sleep usage for synchronization in the diff snippets shown

Security & robustness

  • No obvious security issues jump out from the diff
  • The changes seem to be about fixing data loss during rewrites and improving version resolution
  • File I/O changes appear to be in appropriate places (not in hot paths like event loops)
  • No silent side effects on typos visible in the diff

Scope & atomicity

  • The PR has 15 commits grouped by theme, which seems reasonable
  • Each commit appears to focus on a specific aspect:
    • Microflow properties preservation (multiple commits)
    • run --local resolution fixes
    • Version resolution improvements
    • Catalog entry-point fixes
    • Documentation/skill updates
  • This follows the principle of each commit doing one thing

Code quality

  • The diff shows consistent application of fixes (e.g., preserving multiple microflow properties)
  • No obvious outdated patterns still present
  • The design seems to match the implementation (preserving stored values rather than inventing defaults)

Bugs & correctness

  • The PR addresses specific bugs where properties were being silently dropped during microflow rewrites
  • The fixes appear to be correct: preserving stored values rather than hardcoding defaults
  • The version resolution fix addresses a real issue where Mendix 9/10 versions weren't resolving correctly due to build numbers
  • The classic-client fix correctly checks for the actual client presence rather than assuming based on build artifacts

What Looks Good

  1. Comprehensive test coverage - The PR adds extensive tests for the fixes, including both positive and negative test cases
  2. Correct approach to property preservation - Rather than inventing defaults, the fixes preserve stored values when they exist
  3. Proper version gating - The page parameter version fix correctly identifies that the feature is available from 9.4.0, not 11.0.0
  4. Clear documentation of the issues - The commit messages and PR description clearly explain what was broken and how it was fixed
  5. Follows established patterns - The fixes use the existing architecture (executor → backend → codec) rather than bypassing layers

Recommendation

The PR appears to be well-structured, addresses real bugs with appropriate fixes, includes comprehensive tests, and follows the project's architectural patterns. I don't see any critical issues that would prevent merging.

Recommendation: Approve the PR

However, since this is a large PR with many changes, I should note that in a real review process, I would want to:

  1. Run the tests to make sure they pass
  2. Verify that the fixes actually work as described in the PR
  3. Check that no regressions were introduced

But based on the code review alone, the PR looks good to merge.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment