Sync from ako/mxcli: constant value precedence, access-rule reconciliation, and mapping round-trip fixes - #885
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/mainwas 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 throughdescribe settings— but mxbuild writes each constant's default intodeployment/model/config.json, and that map is whatrun --localhands the runtime. An app ran for hours with an empty encryption key while the model said otherwise.run --localnow resolves a configuration (--configuration, else the only one, elseDefault) and merges its shared values over the defaults at boot. It merges rather than replaces, becauseconfig.jsoncarries defaults for constants the configuration is silent about.On top of that, one precedence chain applied identically by
run --local,test --localandtest --attach:--constant Module.Name=valuerun --local,test --local.mxcli/constants.json, mode 0600, gitignoredmxcli constant set/unset/listalter settings constant … in configurationMicroflowConstantsentry matching no constant, so a typo would otherwise be accepted, reported as applied, and do nothing.mxcli constant set/unset --applypushes a change into an already-runningrun --local. It is two admin calls:update_configurationis staged (the app keeps its old value and the call still answersresult:0) and only the followingreload_modelapplies it.test --localpreviously booted with each constant's default whiletest --attachused the configuration's — so the same.test.mdlcould 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
mxclireports success:has no member(s) SomeAssociationfor an association declared on an ancestor. The member walk followed the generalization chain for attributes but collected associations only whereParentID == entity.ID. This made the rule OpenAIConnector ships impossible to express in MDL.drop association— creating an association reconciled every access rule in the module; dropping one did nothing, so everyMemberAccessnaming 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) orCustomRange{LimitExpression, OffsetExpression}(Custom). mxcli wrote only the first and read onlySingleObject, 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 aListTypeagainst an object-rooted mapping, which mxbuild rejects with CE0243; Mendix's ownFeedbackModule.SUB_Feedback_PostToAppInsightspairsConstantRange{SingleObject:false}with anObjectTypevariable.#882. A
JsonElementcarries two names —Path(the raw JSON key, which the runtime resolves by) andExposedName(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:
IsDefaultTypewas written on value elements (a property only the object element types own) andOriginalValuecopied 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 theirmx checkis 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)
.mpkcarries a copy of every widget its pages use, pinned at its author's release time, andInstallPackageFilescopied 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_Content4.3.0 ships five Data Widgets at 3.4.0 thatDataWidgets3.11.3 ships at 3.11.3. Reading the right version is the trick —package.xmlcarries a version on<package>(the manifest schema, 1.0 on every widget ever published) and one on<clientModule>(the widget's own).marketplace diff/updateanswer 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)
StringType), so a callback was authored as aBasicCodeActionParameterValuestring literal and Mendix reported CE0115. Read and write fixed together.mxcli check --referencesreported "java action not found" for an action the same script created a few statements earlier —scriptContexthad no category for code actions.log 'msg' with ()panicked with a nil dereference, socheckproduced 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— cleango test ./mdl/... ./sdk/... ./cmd/...— greengit merge-treeagainstmendixlabs/main— zero conflictsPer-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 checkon 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-integrationis 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 onako/mxcliCI, but it is the failure mode worth watching if anything here regresses.