Skip to content

Sync from ako/mxcli: constant value precedence, access-rule reconciliation, and mapping round-trip fixes - #885

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

Sync from ako/mxcli: constant value precedence, access-rule reconciliation, and mapping round-trip fixes#885
ako merged 24 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Title

Sync from ako/mxcli: constant value precedence, access-rule reconciliation, and mapping round-trip fixes

Description

Sync of ako/mxcli:main — 20 non-merge commits since the last sync, none of them upstream yet. mendixlabs/main was merged into the fork first, so this branch already contains everything here and the diff is additive: 88 files, +7178 / −196.

Five independent threads. They are unrelated to each other, which normally means five PRs; a fork sync is the one case where batching is the point, so they are grouped rather than argued for as a unit.


1. Constant values — a precedence chain (8 commits)

The thread starts from a defect worth stating on its own: a configuration's constant values never reached the running app. alter settings constant … in configuration 'Default' executed, reported success, and round-tripped through describe settings — but mxbuild writes each constant's default into deployment/model/config.json, and that map is what run --local hands the runtime. An app ran for hours with an empty encryption key while the model said otherwise.

run --local now resolves a configuration (--configuration, else the only one, else Default) and merges its shared values over the defaults at boot. It merges rather than replaces, because config.json carries defaults for constants the configuration is silent about.

On top of that, one precedence chain applied identically by run --local, test --local and test --attach:

Layer Reaches git? Command
--constant Module.Name=value no — one run only run --local, test --local
machine-local store no — .mxcli/constants.json, mode 0600, gitignored mxcli constant set/unset/list
configuration's shared overrides yes alter settings constant … in configuration
constant default yes the model
  • A name the project does not declare is refused before boot. The runtime silently ignores a MicroflowConstants entry matching no constant, so a typo would otherwise be accepted, reported as applied, and do nothing.
  • The machine-local store is labelled as mxcli's own, not as Mendix's private-configuration slot — that slot is encrypted per user account by Studio Pro from 10.9 and is unreachable headlessly.
  • mxcli constant set/unset --apply pushes a change into an already-running run --local. It is two admin calls: update_configuration is staged (the app keeps its old value and the call still answers result:0) and only the following reload_model applies it.
  • test --local previously booted with each constant's default while test --attach used the configuration's — so the same .test.mdl could pass under one and fail under the other with nothing in either output to explain it. Both now go through one options path.

Two commits are corrections to earlier claims about update_configuration, each re-measured against a live 11.12.1 runtime; the second reads values back through a microflow over the test endpoint — the app's own view rather than the API's — to settle a dispute the first measurement could not. Design and appendix: docs/11-proposals/PROPOSAL_constant_values.md.

2. Entity access rules and inheritance (3 commits)

Three defects that all surface as a model Mendix rejects while mxcli reports success:

  • CE0066 "Entity access is out of date" on a specialization. A specialization has every member of its generalization, associations included, so its access rule needs an entry for each; the grant walk collected them and the reconcile that ran next deleted them again, because it recomputed the expected member set from the entity's own attributes and the module's FROM-side associations.
  • GRANT refused an inherited associationhas no member(s) SomeAssociation for an association declared on an ancestor. The member walk followed the generalization chain for attributes but collected associations only where ParentID == entity.ID. This made the rule OpenAIConnector ships impossible to express in MDL.
  • CE1613 after drop association — creating an association reconciled every access rule in the module; dropping one did nothing, so every MemberAccess naming it stayed behind. Measured: drop-then-check went from 2 errors to 0 on mxbuild 11.12.1.

3. Import/export mappings and the microflow import Range (4 commits) — upstream #881, #882

#881. Mendix stores the import activity's Range as ConstantRange{SingleObject} (All/First) or CustomRange{LimitExpression, OffsetExpression} (Custom). mxcli wrote only the first and read only SingleObject, so Custom was unrepresentable — a bounded import became unbounded on any rewrite — and DESCRIBE emitted no range, so all three settings round-tripped identically.

New syntax: import from mapping M.IMM($src) [all | first | limit <e> [offset <e>]]. Omitting it keeps the pre-existing inference. The Range and the result variable's cardinality are separate axes — conflating them writes a ListType against an object-rooted mapping, which mxbuild rejects with CE0243; Mendix's own FeedbackModule.SUB_Feedback_PostToAppInsights pairs ConstantRange{SingleObject:false} with an ObjectType variable.

#882. A JsonElement carries two names — Path (the raw JSON key, which the runtime resolves by) and ExposedName (Mendix's derived name, which Studio Pro displays). The reported cause, capitalisation, is Mendix's own convention. The real defect: DESCRIBE emits the exposed name while the builder resolved only raw keys, so mxcli's own output did not round-trip — re-executing a DESCRIBE fabricated a path ((Object)|Total; for arrays the |(Object) item marker vanished entirely) and mxbuild reported CE5015. Export mappings had it identically.

Members now resolve by either spelling; an unresolvable member is refused with the valid spellings listed rather than given an invented path. Two further divergences from Studio Pro are fixed: IsDefaultType was written on value elements (a property only the object element types own) and OriginalValue copied the JSON snippet's sample value.

Not fixed, and stated in the issue: the reporter's runtime error key not found: Path(QName(None,),None,). Their standalone repro, executed by mxcli, imports cleanly on both 11.6.6 and 11.13.0 — bracketing their 11.12.1 — and their mx check is clean, which rules out the fabricated-path defect. Something their real endpoint carries and the repro does not is still unaccounted for.

4. Marketplace (2 commits)

  • A module's bundled widgets rolled back newer ones. A .mpk carries a copy of every widget its pages use, pinned at its author's release time, and InstallPackageFiles copied everything — so the last module installed decided the project's widget versions, silently, since an out-of-date widget is not a check error. Measured on published packages: Atlas_Web_Content 4.3.0 ships five Data Widgets at 3.4.0 that DataWidgets 3.11.3 ships at 3.11.3. Reading the right version is the trick — package.xml carries a version on <package> (the manifest schema, 1.0 on every widget ever published) and one on <clientModule> (the widget's own).
  • Reference-project caching. marketplace diff/update answer by building a blank app of the consuming project's version with the module imported; a run updating six modules built twelve identical blank apps. Two caches under ~/.mxcli/marketplace-refs/, keyed differently because they miss in different places.

5. Robustness (3 commits)

  • A java action parameter typed Microflow read back as a String on the modelsdk engine (the gen→semantic converter had no case and defaulted to StringType), so a callback was authored as a BasicCodeActionParameterValue string literal and Mendix reported CE0115. Read and write fixed together.
  • mxcli check --references reported "java action not found" for an action the same script created a few statements earlier — scriptContext had no category for code actions.
  • log 'msg' with () panicked with a nil dereference, so check produced a stack trace instead of a diagnostic and every other command that parses the script died the same way.

Verification

What I ran on the merged tree, at ako/mxcli@074c6e1:

  • make grammar, go build ./..., go vet — clean
  • go test ./mdl/... ./sdk/... ./cmd/... — green
  • git merge-tree against mendixlabs/mainzero conflicts

Per-thread verification is in the individual commit messages, which record what was measured and against which mxbuild. For the mapping work (thread 3) I also confirmed 0 errors from mx check on both engines, exact describe round-trips, and each guard reverted in turn to confirm the reported symptom returns.

Scope of my own testing: I authored and verified thread 3 end-to-end. Threads 1, 2, 4 and 5 came from other sessions; I have confirmed they build and pass the unit suite in this merge, but the runtime measurements they describe are theirs, not re-run by me.

Note

test-integration is the suite that matters most here — thread 3's first cut passed every unit test and broke eight round-trip integration tests, because the unit tests all built against a populated JSON-structure index. That is fixed (8071df6) and green on ako/mxcli CI, but it is the failure mode worth watching if anything here regresses.

claude and others added 24 commits August 12, 2026 20:32
A module .mpk carries a copy of every widget its pages use, pinned at its
author's release time, and different modules pin different versions of
the same widget. InstallPackageFiles copied everything the package
shipped, so the last module installed decided the project's widget
versions — and nothing reported it, because an out-of-date widget is not
a check error. Measured on the published packages: Atlas_Web_Content
4.3.0 ships five Data Widgets at 3.4.0 that DataWidgets 3.11.3 ships at
3.11.3, so updating Atlas after DataWidgets rolled all five back.

A bundled widget now never replaces a newer copy, and the skip is
reported with both versions. Verified against the real packages:
installing DataWidgets 3.11.3 then Atlas_Web_Content 4.3.0 keeps exactly
those five at 3.11.3.

Reading the right version is the whole trick. package.xml carries a
version on <package> (the manifest schema, 1.0 on every widget ever
published) and one on <clientModule> (the widget's own). Comparing the
first makes every widget look equal, which is as broken as not comparing.
An unparseable version is never treated as older: the default is to
install, and a wrong "older" verdict withholds a file the package
shipped.

Same pass handles a package that ships a widget twice — FeedbackModule
5.0.0 carries SprintrFeedbackWidget as both a .mpk and an unpacked tree
of the same version, and installing both left a duplicate. The .mpk
wins; an unpacked widget with no packaged twin still installs.

Separately: `marketplace update --no-baseline`. Both update and diff
download the *installed* version to establish the local-edit baseline, so
both fail when that version has been unpublished — a blank 11.13 app
ships NanoflowCommons 6.0.0 and the 6.x line now starts at 6.1.1, so the
module most in need of updating is the one whose baseline cannot be
built. --force was never going to help: it overrides a finding, and the
comparison never ran. The refusal now names --no-baseline, and that flag
says plainly that local edits go without being named.

Both reported from a real 11.13.0 provisioning run (mxcli-chat FINDINGS
§14, §15, §18). Each new test verified to fail with the reported symptom
when the fix is stubbed out.
§33. `alter settings constant 'Encryption.EncryptionKey' value '…' in
configuration 'Default'` executed, reported success, round-tripped
through `describe settings` — and never reached the app. mxbuild writes
each constant's *default* into deployment/model/config.json, and that map
is what `run --local` hands the runtime as MicroflowConstants; the
configuration's values were not in it and nothing read them. An app ran
for hours with an empty encryption key while the model said otherwise.

Studio Pro runs a configuration; so does this now. The run resolves one
(--configuration, else the only one, else "Default") and merges its
shared constant values over the defaults at boot. Verified on a real
project: the run prints `Applying 1 constant value(s) from configuration
"Default": MyFirstModule.ApiKey`.

Three judgements worth stating. It merges rather than replaces, because
config.json carries the defaults for constants a configuration is silent
about — replacing drops them and the app 530s on the first microflow that
reads one, which is exactly the shape `--runtime-setting
MicroflowConstants={…}` has. A private override has no value in the model
at all, so applying it would blank the constant; it is skipped and named.
And with several configurations and no "Default" it applies none and says
why, rather than guessing which environment a local run means.

It also prints when it applies nothing. The bug was invisible because the
run said nothing about constants either way, so silence had to stop
meaning "your override is in effect".

§16. `marketplace diff` accused an untouched blank app of editing an
Atlas_Core snippet and an Atlas_Web_Content building block, and
--save-edits wrote the snippet as an empty `{ }` body — a file offered as
a rescue that would have emptied it on replay. The comparison only asked
whether DESCRIBE *errored*; output that succeeds while saying nothing
about the element still counted as evidence, and two such renderings can
differ over one unresolved name.

A difference now has to come from output that could carry an edit:
non-empty, and not the informational text a read-only handler emits.
Equality is still checked first, so identical renderings stay Unchanged
whatever the type — inverting that would mark every Atlas building block
unknown and drain `verified` of its meaning. The same rule gates
--save-edits, including OnlyInstalled findings, which never pass through
classify.

Each new test verified to fail with the reported symptom when the fix is
stubbed out.
§26. `GRANT … ON Module.Specialization (READ (…, SomeAssociation))` was
refused with "entity X has no member(s) SomeAssociation; grant only names
members of the entity or of an entity it inherits from" — for an
association declared on exactly such an entity. The member walk resolved
inherited attributes through the generalization chain (mendixlabs#758) but
collected associations only from the entity's own domain model where
ParentID == entity.ID, so anything declared on an ancestor was invisible.
That made the rule OpenAIConnector ships impossible to express in MDL:
OpenAIDeployedModel extends GenAICommons.DeployedModel, and
DeployedModel_InputModality is declared on the parent.

The walk now follows the same chain the attribute walk does, qualifying
each reference against the module that declares the association. It finds
the declaring domain model by looking for the ancestor entity rather than
by matching module names — the name lookup goes through the hierarchy
cache and returns "" often enough that filtering on it collected nothing,
which is how the first cut of this looked right and did nothing.

Verified on a real project: the grant that was refused is accepted.

Not fixed, and now localised. The emitted entry does not reach storage.
On a two-entity fixture built for this — Derived EXTENDS Base, the
association declared on Base — the executor passes three MemberAccess
entries and the stored rule holds two, so `GRANT … ON Derived (READ *,
WRITE *)` still reports CE0066 while the same rule on Base checks clean.
That reproduces FINDINGS §25 with no marketplace module involved, and
places the loss between EntityAccessRuleParams.MemberAccesses and the
persisted DomainModels$MemberAccess list. Recorded in the symptom table
as the place to pick it up.

Tests verified to fail with the reported symptom when the walk is stubbed
out, including the negative case: an unrelated entity must not receive
the entry, since a walk collecting every association in the module passes
the positive tests and puts entries exactly where Mendix reports CE0066
for having them.
A java action parameter typed Microflow — MCPServer.AddTool's
ExecutingMicroflow, and every other "register a callback" action — read
back from the model as a String, because the modelsdk engine's
gen->semantic converter had no case for it and its default returned
StringType. The microflow builder's microflowTypeParams branch was
therefore never taken: it authored the callback as

  Microflows$BasicCodeActionParameterValue{Argument: "'M.MyFlow'"}

instead of Microflows$MicroflowParameterValue{Microflow: "M.MyFlow"},
and Mendix reported CE0115. The legacy parser and the executor both
handled this correctly; only the converter degraded silently, so every
other layer looked right in isolation.

Fix read and write together. Without the write case an update of such an
action would rewrite the parameter as a String, which is worse than the
read bug. The stored shape is a direct
JavaActions$MicroflowJavaActionParameterType (not wrapped in a
BasicParameterType), measured against MCP Server 5.1.0.

DESCRIBE now prints the honest `Microflow` for these parameters, which
round-tripped into an entity type with an empty module (`.Microflow`)
until astDataTypeToJavaActionParamType learned the bare word; only the
unqualified name is treated this way, so a real Module.Microflow entity
is unaffected.

Verified end to end: the call now serializes as MicroflowParameterValue
and `mx check` on 11.12.1 goes from CE0115 to 0 errors.

Reported in mxcli-chat FINDINGS §36.
`mxcli check --references` reported "java action not found" for an action
the same script created a few statements earlier. scriptContext — the set
of objects a script defines, consulted exactly so a script can be checked
against its own output — had categories for modules, entities,
enumerations, microflows, nanoflows, pages and snippets, but not for java
or JavaScript actions, so their branch went straight to the project.

Store the declared parameter names rather than a bool: exempting the
action from "not found" must not also exempt it from the parameter-name
check, which the script has everything it needs to run. allNames() and
has() gain the categories together — annotateForwardRef reads both, and
updating one alone would make a created action look "defined later in
this script" forever.

Reported in mxcli-chat FINDINGS §37.
…s rules

GRANT on a specialization wrote a model Mendix rejects with CE0066
"Entity access is out of date", while the same grant on the
generalization checked clean. A specialization has every member of its
generalization, associations included, so its access rule needs an entry
for each of them; the grant walk collected them, but the reconcile that
runs next deleted them again — the executor passed 3 MemberAccess
entries and storage held 2.

ReconcileMemberAccesses recomputed each rule's expected member set from
the entity's own attributes and the module's FROM-side associations, so
an inherited association matched neither and was classed stale. It now
walks the generalization chain within the module, and preserves a
reference qualified with another module the way the attribute branch has
since mendixlabs#758 — an association is qualified by the module that declares it,
so an ancestor elsewhere (the reported OpenAIDeployedModel extends
GenAICommons.DeployedModel) names a domain model that is not loaded here
and cannot be validated at all.

Reconcile also has to ADD such an association, or a rule written before
the ancestor gained one never catches up.

Stale detection is unchanged for everything it could already judge, and
the TO side of an OWNER Default association still gets no entry — having
one is itself CE0066, so a walk that simply collected every association
in the module would pass the positive cases and fail there.

Measured on a fixture with no marketplace module involved (Base <-
Derived, association FROM Base): 1 error -> 0 on mxbuild 11.12.1.

Reported in mxcli-chat FINDINGS §25.
Creating an association reconciles every access rule in the module so the
new member gets an entry; dropping one did nothing, so every MemberAccess
that named it stayed behind and Mendix rejected the model with CE1613
"The selected association 'X' no longer exists".

This is independent of inheritance — it hit the entity that declared the
association just as hard — but it only became visible while verifying the
specialization fix, because until then a specialization never carried the
entry to begin with. DROP now runs the same reconcile CREATE does, and
tracks the domain model as modified so the finalize step sees it.

Measured: drop-then-check went from 2 errors to 0 on mxbuild 11.12.1.
The comment claimed the admin update_configuration action "REPLACES
rather than merges". Measured on 11.12.1 against a running standalone
runtime, that is not what the runtime does with MicroflowConstants: a
payload carrying one constant left every omitted constant resolving to
its deployment default, and the database kept working.

What is true is narrower, and is the actual reason to fold settings into
one boot call: the Go map below overwrites by key, so
--runtime-setting MicroflowConstants=... replaces the map mxcli just
built, and at boot there is no prior configuration to fall back on for
BasePath/DatabaseName (neither is in config.json).

Also records two findings for anyone tempted to drive this API live: the
call is staged, not applied — the running app keeps the old value until
the next reload_model while still answering result:0 — and there is no
read-back to merge against (get_configuration,
get_current_configuration, runtime_config and get_current_runtime_status
are all "Action not found").

Comment only; no behaviour change.
A Mendix constant has a value in four possible places, mxcli can write
two of them, and only one of those two reaches a running app. The
proposal defines one chain -- --constant, a per-machine gitignored store,
the configuration's shared overrides, the default -- applied identically
by run --local, test --local and test --attach.

It carries three findings measured against a live 11.12.1 runtime, in an
appendix so the design can be checked against them: update_configuration
is staged rather than applied (the app keeps the old value until the next
reload_model, while answering result:0), the runtime treats the payload's
constants as an overlay on the deployment defaults rather than a
replacement, and the admin API has no read-back to merge against.

It also records why Mendix's own private-constant slot cannot serve a
headless agent -- per-user encrypted, off-model, Studio Pro only -- which
is the reason mxcli needs a store of its own rather than a way to write
that one.

Four slices, smallest first: the test --local divergence is a bug and
ships alone.
A `.test.mdl` asserting on something a constant feeds could pass under
`mxcli test --attach` and fail under `mxcli test --local`, with nothing in
either output to explain it. `--local` boots an app of its own through
StartLocalApp, whose options had no ConstantOverrides field, so it ran
with each constant's default out of deployment/model/config.json;
`--attach` runs against an app `run --local` booted, which applies the
configuration's shared overrides. A constant resolving to the wrong value
is not an error, so both runs reported success and only the assertion
differed.

The two --local runners each built their own LocalAppOptions literal, so
a field added for one would not have reached the other. Both now go
through localAppOptions, and the LocalAppOptions -> LocalRuntimeOptions
step is a runtimeOptions() method, so the forwarding is assertable
without booting anything — a field dropped there is otherwise invisible
until an app runs with the wrong configuration.

`mxcli test` gains --configuration, resolved by the same code `run
--local` uses and reported the same way. Only a --local run resolves it:
--attach inherits the constants of the app it attached to, and printing a
resolution it does not use would be a confident lie.

Verified at the layer the symptom lives in, on 11.12.1: one project with
a constant defaulting to DEFAULT-KEY and a configuration override of
RUNTIME-KEY, the same suite run both ways. With the wiring the --local
run passes on RUNTIME-KEY; with the wiring reverted it fails with exactly
the reported symptom.

docs/11-proposals/PROPOSAL_constant_values.md slice 1.
Every route mxcli offered for setting a constant wrote to the model, and
therefore to git, so running once with a different API key meant
committing it or remembering to revert. `--constant Module.Name=value`
(repeatable, on `run --local` and `test --local`) wins over the
configuration and is never written anywhere.

A name the project does not declare is refused before anything boots. The
runtime silently ignores a MicroflowConstants entry matching no constant,
so a typo would otherwise be accepted, reported as applied, and do
nothing — the mxcli-chat §33 failure shape, reintroduced by the flag
meant to help with it. A missing "=" is refused for the same reason:
`--constant M.C` almost certainly meant the value to be the next
argument, and quietly setting the constant to "" is the same class of
silent wrong value.

The report now names the layer each value came from rather than assuming
one source, and a flag that covers a private override stops that constant
being reported as private-and-defaulted — which would contradict what the
app is about to do.

--attach refuses --constant rather than ignoring it: it runs against an
app someone else booted and inherits that app's constants.

Verified on 11.12.1 against a real runtime: with the project's Default
configuration setting RUNTIME-KEY, a suite asserting FLAG-KEY passes
under --constant MyFirstModule.ApiKey=FLAG-KEY, so the flag reached the
app and beat the configuration. The three refusals were checked to fire
before any boot.

docs/11-proposals/PROPOSAL_constant_values.md slice 2.
…mitted

A constant's default and a shared configuration override both go to git.
Mendix's own private configuration value is the correct slot and is
unreachable: from 10.9 it is encrypted per user account by Studio Pro, so
nothing headless can read or write it. Until now a value that had to
persist without being committed had nowhere to live.

mxcli constant set/unset/list writes <project>/.mxcli/constants.json,
mode 0600, sitting between --constant and the configuration. It is
labelled as mxcli's own store rather than pretending to be Mendix's, and
its security is file permissions, not encryption.

The promise is made true and then checked. `mxcli init` writes a
.gitignore only when the project has none, and a Mendix project usually
already has one, so the entry the whole layer rests on could simply be
absent: `constant set` appends it and then asks git whether the path is
really ignored, refusing to write the value if it is not. Which rule
defeats an entry is not guessable and was measured -- `!.mxcli/**` does
not re-include anything, because git cannot re-include a file whose
parent directory is excluded, while `!.mxcli` does.

Two asymmetries worth knowing. A corrupt store is fatal, because it means
values the author deliberately set are about to be silently absent; a
stale entry naming a constant the project no longer declares is skipped
and named, because refusing would fail every run until the user's own
file was hand-edited. And an empty store is deleted rather than written
as {} -- a file that configures nothing should not exist.

Verified on 11.12.1 against a real runtime: with the project's Default
configuration setting one value and the store another, a suite asserting
the store's value passes; adding --constant makes the same suite pass on
the flag's value instead. git status never sees the file.

docs/11-proposals/PROPOSAL_constant_values.md slice 3.
Constants are a boot-time layer: an app already up keeps serving the old
value until someone restarts it. `mxcli constant set/unset --apply`
pushes the change into a `mxcli run --local` that is already running.

It is two admin calls, not one. Measured on 11.12.1, update_configuration
is STAGED -- the running app keeps its old values and the call still
answers result:0 -- and only the following reload_model applies them, so
a version sending just the first would report success and change nothing.
Both live in ApplyConstants rather than at the call site for that reason.

The whole boot payload is re-sent, not the constants alone, because the
admin API has no read-back: whatever is not sent is simply gone from the
configuration afterwards. A second process cannot ask what the runtime
was booted with, so `run --local` now publishes it -- with the ports and
admin credential -- in a 0600 handshake beside the project, removed when
the loop exits. A handshake whose process is gone is refused rather than
used: its ports may since have been taken by something else.

One correction to the proposal, found while building it. "Verify by
observation" is not achievable from outside the app: no admin action
exposes a constant's value. So --apply performs both calls, reports what
it did, and names what would actually confirm it, rather than claiming a
success it cannot check.

Failure to apply is not failure to set -- the value is on disk and the
next boot uses it, so --apply reports and returns instead of exiting
non-zero over a half-succeeded command.

Verified on 11.12.1 against a warm dev loop, reading the constant through
a microflow over the test endpoint so no model change is involved:
boot RUNTIME-KEY; set without --apply still RUNTIME-KEY (the control);
set --apply STORE-APPLIED with no restart; unset --apply back to
RUNTIME-KEY.

docs/11-proposals/PROPOSAL_constant_values.md slice 4.
Mendix stores the import activity's Range on the ImportMappingCall in two
variants:

  Microflows$ConstantRange{SingleObject}                     All / First
  Microflows$CustomRange{LimitExpression, OffsetExpression}  Custom

mxcli only ever wrote the first and read only SingleObject, so "Custom" was
not merely undescribed but unrepresentable: a bounded import became unbounded
the moment anything rewrote the activity. DESCRIBE emitted no range at all,
so all three settings round-tripped identically and describe -> edit -> exec
silently changed the activity's meaning.

Adds the trailing clause:

  import from mapping M.IMM($src) [all | first | limit <e> [offset <e>]]

Omitting it keeps the pre-existing inference from the mapping's root shape,
so scripts that predate the syntax write exactly what they always did.
DESCRIBE always emits one of the three, because silence re-enters that
inference and an object-rooted mapping set to All -- Studio Pro's own default,
shipped in the blank app -- comes back as First.

The Range and the result variable's cardinality are separate axes. Folding
them writes a ListType against an object-rooted mapping, which mxbuild rejects
with CE0243; Mendix's own FeedbackModule.SUB_Feedback_PostToAppInsights pairs
ConstantRange{SingleObject:false} with an ObjectType variable. The stored
VariableType is the authority on cardinality, the range is its own flag
(RangeSingleObject, nil = follow the inference), and only `first` pins both.

The ImportMappingCall is built at three sites -- the import statement, REST
result handling, and the legacy writer -- so the range selection is one shared
helper per engine; fixing two of the three let a limit reach the model while a
ConstantRange was still written. Both engines are fixed: they share the
semantic model, and a fix in one is invisible to a user on the other.

Verified end-to-end on mxbuild 11.6.6 (mx check: 0 errors on both engines,
all three ranges re-describing verbatim). `offset` is rejected by Mendix with
CE6100 unless the mapping's root is a list, while `limit` is accepted either
way; that is documented rather than validated, because mxcli cannot currently
author a list-rooted import mapping to test the positive case against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
`log 'msg' with ()` panicked mxcli with a nil dereference in
buildTemplateParams, so `check` produced a stack trace instead of a
diagnostic and every other command that parses the script died the same
way.

The grammar requires at least one templateParam, which is a statement
about valid input, not about what reaches the walker: ANTLR error-recovers
by handing it a TemplateParamContext with no index token rather than by
skipping the rule. The builder called NUMBER_LITERAL().GetText() on it.

Guarded at the dereference. The syntax error is already reported, and
`check` now says what it always should have:

  line 3:20 mismatched input ')' expecting '{'

The other 11 NUMBER_LITERAL().GetText() sites under mdl/visitor/ were
checked and every one already nil-guards, so this was the only bare one
and no sweep is needed.

Two tests, because the obvious guard is wrong in a way the crash test
cannot see: skipping on nil also skips well-formed parameters, so the
control asserts `with ({1} = …, {2} = …)` still yields both.

Reported in mxcli-chat FINDINGS §55.

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

mxcli-chat FINDINGS §57 reports that an update_configuration carrying one
constant blanks every constant omitted from the map, and that no reload is
needed. Both halves contradict the comment this file gained in c766a76,
and the constants feature rests on that reading, so it was re-measured.

Measured on 11.12.1, reading the values back through a microflow that
returns two constants over the test endpoint — the app's own view rather
than the API's, which is what the earlier measurement lacked:

  - Staged, confirmed. Without reload_model the app keeps the old value
    while update_configuration still answers result:0.
  - Merge, not replace. A payload carrying only ApiKey left
    ClientIdentifier at the value an EARLIER update_configuration had
    given it — not at its deployment default, and not blank. So it
    overlays the running configuration, which is stronger than the
    "overlay on the deployment defaults" the comment claimed.
  - Payload shape is not the difference: params carrying only
    MicroflowConstants, exactly as reported, behaved the same as the
    full boot config.

§57's blanking did not reproduce here in either shape. It is recorded as
disputed rather than as wrong: it was inferred from a downstream symptom
on 11.13.0, and this is one version and one runtime.

The comment now says why the disagreement costs nothing either way:
ApplyConstants re-sends the whole resolved chain, which is correct under
both readings. Nobody should have to re-run this to find that out.

Comment only; no behaviour change.

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

A JsonStructures$JsonElement carries TWO names, and for any lowercase-initial
key they differ:

  Path         "(Object)|uuid"   the raw JSON key — what the RUNTIME resolves by
  ExposedName  "Uuid"            Mendix's derived name — what Studio Pro DISPLAYS

Mendix derives the exposed name by capitalising the initial, and by suffixing
"Item" on an array's item object. Both are Mendix's own: the blank app's
Studio-Pro-authored FeedbackModule.JSON_AppInsightsResponse stores ExposedName
"Uuid" against Path "(Object)|uuid", and its IMM_PostResponse binds JsonPath
"(Object)|uuid". So the capitalisation DESCRIBE shows is faithful rendering.

The defect is that mxcli's DESCRIBE emits the exposed name while its builder
resolved only raw keys, so mxcli's own output did not round-trip. Re-executing a
DESCRIBE fabricated a path from the exposed name:

  (Object)|total                             ->  (Object)|Total
  (Object)|entityInstances|__Value|(Object)  ->  (Object)|EntityInstances|__ValueItem

The array's "|(Object)" item marker vanished entirely and MaxOccurs went to 0.
`mxcli check` passed; mxbuild reported CE5015. Export mappings had the identical
bug.

Members now resolve by raw key or exposed name — including an array addressed by
its item's exposed name, which resolves to the array so the "|(Object)" step is
still taken. A member matching neither is REFUSED, listing the spellings that
would have worked, instead of being written with an invented path: that path
passed `mxcli check` and surfaced only later, which is the worst failure mode
because the tool that wrote it reported success.

Separately, both engines wrote IsDefaultType on every ValueMappingElement. The
generated metamodel declares it on Import/ExportObjectMappingElement and on
neither value type, and Studio Pro's own mappings carry it on the object element
alone. Per the overlay-writes rule in CLAUDE.md, a property the type does not own
is the shape mxbuild tolerates and Studio Pro refuses to open, so a green build
is not evidence — it is dropped from the value writers in both engines.

Verified on mxbuild 11.6.6, both engines: the raw-key and exposed-name spellings
now produce byte-identical stored paths, and `mx check` reports 0 errors where it
previously reported CE5015. Each fix was reverted in turn to confirm the symptom
returns.

Refs upstream mendixlabs#882.

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

A JSON structure element's OriginalValue is the SAMPLE parsed out of the snippet
("42", "\"Widget\""). It describes the structure, not the mapping, and Studio Pro
leaves it empty on every mapping element.

Measured across the two Studio-Pro-authored mappings a blank app ships —
FeedbackModule.IMM_PostResponse and EMM_PostFeedback, ~15 value elements between
them — all write OriginalValue "", while their JSON structures carry 17 non-empty
samples. mxcli cloned the sample in, so an mxcli-written mapping differed from a
Studio-Pro-written one over the same structure by the snippet's example data,
which is the first thing a side-by-side comparison of the two shows.

Refs upstream mendixlabs#882.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
`marketplace diff` and `marketplace update` answer their question by
building a reference project: a blank Mendix app of the consuming
project's version with the published module imported into it. That costs
a download plus two `mx` invocations, and `update` builds two — the
installed version, to establish the local-edit baseline, and the target.
Neither result depends on anything but its inputs, and both were thrown
away at the end of every run.

Two caches under ~/.mxcli/marketplace-refs/, because they miss in
different places. blank/ is keyed by Mendix version and takes the
`mx create-project` out of EVERY reference build — a run updating six
modules built twelve identical blank apps. ref/ is keyed by (published
version UUID, Mendix version) and holds the finished reference, so a
`diff` followed by an `update` does not build the same base twice.

Measured on Administration (23513, 4.3.2 → 4.5.0, Mendix 11.12.1), with
byte-identical output across all four runs — 21 of 21 elements unchanged,
6 elements an upgrade would touch:

  diff, no cache      66s
  diff, cold cache    49s
  diff, warm          13s
  update 4.3.2→4.4.0  24s   (base cached, target built; 9 identities
                              preserved, 2 role grants restored)

The version UUID is the key, never the version number: numbers collide
across content, so a blank 11.12.1 app has Atlas_Web_Content 4.1.0 while
Administration has also published a 4.1.0. The Mendix version is in both
keys because a reference built at another version reports Mendix's own
conversions as user edits, and the project's version stamp is re-verified
on the way out of the cache as well as before it goes in — an entry
written by an older mxcli is dropped rather than trusted.

An entry is a directory tree, so its presence proves nothing: the
completion marker is written last, after an atomic rename, and its absence
means rebuild. Anything unreadable is deleted and rebuilt rather than
diagnosed, because a partial reference does not fail loudly — it reads as
local edits.

ref/ is bounded to the 6 most recently used entries (34 MB each, and
twelve of them is ~400 MB of a container that may have 2 GB free).
Running out of disk part way through an update is worse than rebuilding a
reference, because `marketplace update` does not roll back. blank/ is
unbounded — one entry per Mendix version, and it is the one that pays on
every build. MXCLI_REF_CACHE_MAX changes the bound; MXCLI_NO_REF_CACHE=1
builds everything from scratch, so a suspected stale-cache problem can be
ruled out without destroying the evidence.

Not done, and deliberately: FINDINGS §59 also asks for the baseline to be
skipped under --force, on the grounds that it is computed and thrown away.
It is not — gateOnLocalEdits PRINTS the locally changed elements it is
about to replace, and that list is the point of the check. --no-baseline
already exists for "I accept that you cannot tell", which is the case §15
needs. Caching makes the baseline cheap instead of removing the warning.

Reported in mxcli-chat FINDINGS §59.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The refusal added for mendixlabs#882 fired whenever a member failed to resolve, including
when there was nothing to resolve against. `create import mapping X { ... }` with
no `with json structure` clause is legal MDL, and an XML-schema or
message-definition mapping resolves no JSON elements either — so every
schema-less mapping was rejected:

  "id" is not a member of the JSON structure at (Object),
  which has no members there

That broke eight round-trip integration tests, which unit tests did not cover
because they all build against a populated index.

The refusal now applies only where a schema exists to contradict the name. With
no schema loaded, the authored name is taken at face value and becomes both the
exposed name and the path segment — the pre-mendixlabs#882 behaviour, unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
fix(mappings): import-mapping Range authoring (mendixlabs#881) and JSON member-name resolution (mendixlabs#882)
Constant values that actually reach the app, plus five FINDINGS fixes
@ako
ako merged commit e21d465 into mendixlabs:main Aug 13, 2026
4 checks passed
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