Skip to content

Sync ako/mxcli: Mendix 11.14 + Java 25, navigation profiles, mapping fidelity, clearer errors - #983

Merged
ako merged 44 commits into
mendixlabs:mainfrom
ako:main
Aug 27, 2026
Merged

Sync ako/mxcli: Mendix 11.14 + Java 25, navigation profiles, mapping fidelity, clearer errors#983
ako merged 44 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Seventeen commits since the last sync. Most of it comes from building two real apps against mxcli — ako/mxcli-maintenance and ako/mxcli-ledger — and fixing what they hit.

Mendix 11.14 and Java 25

11.14 ships a blank app with JavaVersion = 25, and mxcli resolved a JDK 21 unconditionally

ako and others added 30 commits August 27, 2026 06:02
…efinition2

Two halves of the same problem: a rebuilt mapping did not carry the same
property SET as the document it replaced, so no export mapping mxcli
wrote matched its Studio Pro original and no 11.10+ document round-tripped
— which kept both in #260's silent-loss set no matter what else was fixed.

#277 — the export writers hardcoded three properties the IMPORT twin
already read off the element:

    object root    MinOccurs 1        hardcoded 0
    value element  MaxLength 0 / -1   hardcoded 0
    value element  IsKey false        not written at all

MaxLength is the one that cannot be a constant: Studio Pro stores 0 for a
string element and -1 for a numeric one, mirroring the bound schema
element exactly as MaxOccurs already did (FeedbackModule.EXM_PostFeedback
has both — screenWidth and screenHeight are the -1s). The export element
model carried none of these fields and the reader populated none of them,
so this is model, reader, builder and three writers, not a writer patch;
patching only the writer would have written zeros from an empty model.

#279 — MessageDefinition2 was never read or written. gen records it as
Introduced 11.10.0 and generates no accessor at all, so the read goes to
raw BSON, the route parameterEntityFromRaw takes.

It is CARRIED, not derived. nil means the key is absent, which is not the
same as present-and-empty — hence the pointer. Writing it onto a
pre-11.10 document is the overlay-rule mistake CLAUDE.md warns about:
mxbuild tolerates the unknown property and Studio Pro refuses to open the
document. The executor decides — carry the stored value on an update,
apply the version gate only on a CREATE where there is nothing to read it
off. A plain version gate in the writer looks equivalent and is not: it
re-adds the key to every older document a rewrite touches, which turned
four previously-clean fixtures red when I tried it.

MappingSourceReference is the same family and deliberately NOT carried.
Its gate (10.16) predates every project in the field, and the codec emits
it through a package-level TypeDefaults registration, so making it
conditional would touch every mapping write to preserve one pre-10.16
fixture. Recorded on the remaining knownLossy entry.

Three pinned Studio Pro documents came off knownLossy and now round-trip
byte-identically: FeedbackModule.IMM_PostResponse,
FeedbackModule.EXM_PostFeedback and MxGenAIConnector.EM_CohereEmbed_Request.
knownLossyFrom goes with them — its only entry was IMM_PostResponse.

Validated: mxbuild 11.13 reports 0 errors; control (hardcodes restored)
fails the new test with the reported values; unit suite, executor
integration suite (417s), 417 MDL scripts and vet all green.

Closes #277
Closes #279

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the config

ako/mxcli-ledger #146. `mxcli run --local` died on every 11.14 app:

    bundling web client: no rollup.config.mjs in …/deployment/web
                         (run a serve Deploy build first)

and the advice was not actionable, because a serve Deploy build had just
run. Under 11.14 it does not produce that file.

BuildWebClient exists to close a gap in Mendix 11.13 and earlier: the
serve Deploy target wrote the client source and a self-contained rollup
config but never ran the bundler, so a standalone-served app 404'd on
/dist/index.js and rendered blank. 11.14 closes that gap upstream — it
writes web/dist/ itself and no longer emits a config, because there is
nothing left to configure.

The gate tested for the CONFIG, so it failed on the absence of a file
whose purpose had been served. Both call sites are fatal, so this is
every 11.14 app rather than one project.

Measured on a blank app, same build target, with the versions as the only
variable:

    11.13.0   rollup.config.mjs PRESENT   dist/index.js ABSENT
    11.14.0   rollup.config.mjs ABSENT    dist/index.js PRESENT

Exactly inverted, which is why a gate written against one is wrong on the
other. Verified against those two real deployment directories: 11.14
returns nil with a skip line, 11.13 still proceeds into the rollup step.

The fix gates on the gap the step closes rather than on the shape one
version happened to leave behind. Two things it deliberately does NOT do:

- It does not become an unconditional no-op when dist/ exists.
  runlocal.go:1091 re-bundles when the bundle is PRESENT but not SERVED,
  and a blanket skip would silently break that recovery path.
- It does not gate on the Mendix version. The condition is about the
  bundle, and a version check would need updating for every release.

Still an error when there is neither a bundle nor a config, because then
the build genuinely produced no client and continuing would serve a blank
page — with a message that names both halves, so a user on 11.14 does not
read the old text and go looking for a build that already ran.

The doc comment called this "a no-op-safe prerequisite". It was not
no-op-safe; it is now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ako/mxcli-ledger #143. Replaying one `create or replace snippet` reset a
translated label to its source language in all six languages of a
project. Silently: the run reported "Created snippet", mx check reported
0 errors, and the app rendered in one language.

The replace path was an unconditional delete-then-create. Nothing
canon.Reconcile does can survive that — by the time the create runs there
is no stored document left to transplant identities from or to compare
against, so both the preservation and the unchanged-elision have nothing
to work from, and the translations go with the deleted unit along with
everything else MDL cannot express.

The verb was the tell. A path that prints "Created" on a replace of an
existing document is not going through the write choke point that every
other document type uses.

The control is free and lands in the same run: put a page and a snippet
carrying the same shape of content in one script and replay it.

  page     "Unchanged page"      translations survive
  snippet  "Created snippet"     translation destroyed

Same project, same statement shape, one run — which is what makes this a
snippet defect rather than a fact about replacing documents. The page
path 40 lines above already did it correctly, so the fix is to do what it
does: reuse the stored unit's ID, call UpdateSnippet (which already
existed and was called from nowhere), and delete only the genuine
duplicates. Reporting follows the same rule, so a replay of an unchanged
snippet now says "Unchanged" instead of claiming a creation every time.

That also fixes the second consequence the page path documents: a
delete+add churns the file in git and crashes Studio Pro's
RevStatusCache.

This is the live half of ledger #144, which reported duplicate GUIDs that
only `mx convert` notices. That symptom does NOT reproduce on current
main — measured over a simple page, a rich page with seven translations
and an inserted widget shifting the element sequence, and the ledger
project's own committed model (0 duplicates across 81,374 elements with
an $ID, with a planted duplicate proving the scanner detects them). #143
is its stated cause, so this is where the fix belongs.

Verified end to end: after the fix a replay reports Unchanged for both
documents and all three nl_NL translations remain. Reverting the change
fails the regression test with the reported call pattern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(mappings): mirror the schema facets on export, and carry MessageDefinition2
fix(run): start a Mendix 11.14 app — gate the bundler on the gap, not the config
fix(snippets): a replace keeps the document's translations
ako/mxcli-ledger #131, open across six consecutive builds.

`mxcli check -p app.mpr` reported every string literal containing a
doubled apostrophe as malformed:

    Unexpected token after expression — the expression appears incomplete
    or malformed (possible missing space between keywords).

Nothing was glued and no keyword was involved. `''` is how the Mendix
expression language escapes an apostrophe inside a string literal;
CLAUDE.md states the rule, exec writes it, DESCRIBE round-trips it
byte-for-byte, and mx check reports 0 errors. Only exprcheck disagreed.

exprcheck's lexer scanned to the next quote with no notion of an escape,
so 'a''b' lexed as TWO strings: parseOr consumed the first and the second
was reported as a leftover token. The message is a red herring by
construction — the leftover reporter cannot know WHY a token is left
over, so any lexer gap surfaces as "glued keywords", which is what made
this cost a hand bisect of a 600-line file.

A doubled quote is now consumed as part of the literal; a single one ends
it. Unterminated input still lexes to TokError, and that control is
asserted at the lexer rather than the parser: a lexer that simply
swallowed to end-of-input would satisfy every escape case above while
silently accepting a broken literal.

One thing worth recording about the reproduction. It only happens WITH
`-p` — exprcheck runs when a project is supplied, and `make check-mdl`
runs without one. So this repo's own MDL gate is blind to the whole
class, and the mdl-examples script added here documents the defect but
would NOT have caught it; the regression guard is the unit test.

Verified end to end: both the minimal 'a''b' and the reporter's real
OData filter now pass check with a project, describe returns them
unchanged, and the package's existing leftover/glued-keyword tests still
pass. Reverting the lexer arm fails the new tests with the reported
message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`describe {import,export} mapping` over the 327 demo-app documents: 83
parsed and were faithful, 112 parsed but rebuilt a DIFFERENT mapping, 132
did not parse. Four independent describer/grammar defects, all fixed here
except where noted.

1. An OBJECT element could not take a nested member path. Studio Pro maps
   an object several levels down with nothing mapped in between —
   `= meta/pagination` — and mendixlabs#927 gave that to VALUE elements only.

   An object path may step THROUGH an array where a value may not: one
   object per item is legal, while a value pulled through many items is
   CE0256. So the occurrence guard is now keyed on the element having an
   entity, and intermediate markers are dropped when printing and stepped
   through when resolving.

2. An Array or Wrapper printed as an EMPTY value binding with its whole
   subtree dropped, because the printer branched on `Kind == "Object"`
   alone — the export half was fixed under #268, the import half was not.

   Fixing the dispatch was not enough: the two-level import array, where
   container and item each carry their own entity and association, is now
   honoured as written, detected the same way #262 detects it on export.
   The item is built by hand, because the generic builder would try to
   resolve the stored marker as a member — and building by hand means
   remembering what the generic path does, which the first attempt did
   not: it dropped the item's own custom handler and its CE0281 value
   elements.

3. `create ./Entity = x` becomes the bare entity, which the grammar now
   accepts. Both printers were also dropping the `by` and backup clauses
   on that branch, so a Custom-handled element rebuilt as a plain Create.

4. Members MDL cannot spell bare — `$type` (43 occurrences),
   `research%3Aread`, `https%3A//…` — were emitted raw. This needed no
   grammar at all: identifierOrKeyword already accepts a quoted
   identifier, so only the describer had to quote.

The entity-less IMPORT container is deliberately NOT given syntax. There
is no such element in any pinned Studio Pro document (0 of 12, both
directions), and the 10 the census found are all EXPORT, which `group as`
already covers. Inventing a spelling whose stored shape cannot be checked
against a real document is how a mapping ends up valid to mxbuild and
unopenable in Studio Pro, so DESCRIBE emits a comment naming it instead.
I wrote that syntax first and reverted it.

Two more pinned documents now round-trip byte-identically:
KrogerAPI.IM_ProductList and MxGenAIConnector.IM_CohereEmbed_Response.
IM_Collection_RetrieveNearestNeighbors keeps a knownLossy entry for one
value element's data type — stored Integer, attribute Decimal — which
this structure records no schema type for, so the rule cannot be settled
from it.

Grammar-ordering trap worth knowing: `group` is a keyword qualifiedName
accepts, so the new bare-entity alternative placed before `GROUP AS`
swallowed `group as x` and stored an entity named "Module.group". No
parse error; only TestExportGroupElement caught it.

Validated: mxbuild 11.13 reports 0 errors on the repro, which also
round-trips as "Unchanged"; unit suite, executor integration suite (374s),
418 MDL scripts and vet all green.

Refs #260

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(check): accept Mendix's own apostrophe escape in expressions
fix(mappings): make DESCRIBE emit MDL its own grammar accepts
…urs 1

Two of #272's three measured differences from Studio Pro. Both leak into
every mapping built over the structure, since a mapping element mirrors
the schema element it resolves against.

1. resolveExposedName only CAPITALISED a member name, never sanitised it,
   so any character outside [A-Za-z0-9_] reached the stored ExposedName
   and Mendix refused to build:

       CE9524 "JSON element '(Object)/$type' has an invalid custom name
               '$type'. Custom name should start with a letter or
               underscore followed by either letters, digits or
               underscores."

   Unlike the reserved-name case in #300, that message is accurate.
   `$type` occurs 43 times in the demo apps, alongside `research%3Aread`
   and `https%3A//sws.siemens.com/sam/claims/tenantId`.

   The rule is Studio Pro's, read off CRS_ModelSimulator's structures:
   every character outside the set becomes `_`, then the initial is
   capitalised — confidence(No) -> Confidence_No_, first(Importance)_
   Payload (kg) -> First_Importance__Payload__kg_. A LEADING DIGIT is
   prefixed rather than replaced, since the name must start with a letter
   or underscore and `1st` -> `_st` would lose a character.

   This also completes #260's item 4: such a member can now be CREATED as
   well as described.

2. The root carried MinOccurs 0; Studio Pro writes 1, uniformly across
   all nine Studio Pro-authored structures in the round-trip fixture.

   It was pinned to 0 with a documented reason — writing 1 used to fail
   every bound mapping with CE5015 "Attribute 'MinOccurs' does not match
   schema element", because the mapping serializers hardcoded 0. That
   blocker was removed by #277/#279, which made every mapping element
   mirror the bound schema element. Both sides now agree, so the root can
   carry the 1. The code and the test both say the two must move
   together.

NOT reproduced, and the issue's premise does not hold: #272 also asks for
Studio Pro's array-item naming (Data -> Datum) instead of mxcli's `Item`
suffix. Across the nine pinned structures Studio Pro produces BOTH real
singularisation (Datum, Department, Category, Image, Size, Choice) and a
flat JsonObject / JsonObject_2 / JsonObject_3 (Chunks, Metadata, Labels,
Embeddings) — and it is not a collision fallback: nothing named
`Embedding` exists in the structure whose item is `JsonObject`. The two
families come from different modules, so the behaviour varies by Studio
Pro version or creation path, and picking either would match some real
documents while breaking others. Evidence recorded on the issue.

Validated: mxbuild 11.13 reports 0 errors on a structure whose members
previously made the project unbuildable, and both mappings bound to it
round-trip as "Unchanged"; unit suite, executor integration suite (352s),
421 MDL scripts and vet all green.

Refs #272
Closes #260

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ako/mxcli-maintenance #1 and #3. `mxcli new --version 11.14.0` produced a
project whose very first build failed:

    Execution failed for task ':compile'
    > Java compilation initialization error
        error: release version 25 not supported

Mendix 11.14's mx create-project writes JavaVersion = 25. mxcli builds and
launches the runtime on JDK 21 and validates it — resolveJDK21 refuses a
JAVA_HOME that is anything else — so a blank 11.14 app could not build,
and could not boot either.

Installing a JDK 25 is the fix that looks obvious and is wrong. It gets
past the compile and then fails at boot with UnsupportedClassVersionError
… class file version 69.0, because the runtime is still launched on 21.
Compiling for 25 and running on 21 cannot work in either direction. The
maintenance repo checked the other half directly: 11.14's own runtime
bundles are still Java 21 class files (major version 65), so 25 buys
nothing today.

So the project is lowered, not the JDK raised. Three things about how:

- It lowers ONLY when the project asks for more than the runtime launcher
  uses. On 11.13, whose blank app is already 21, the step does not fire —
  and it disappears by itself the day the launcher becomes version-aware,
  rather than becoming a second thing to remember.
- It runs BEFORE SettleGeneratedSources. That step is a build, so with the
  order reversed it is the thing that fails, and the fix would arrive one
  step too late to matter.
- A version it cannot parse is left alone. Overwriting a setting that is
  merely unfamiliar would replace a value the user can see with one they
  did not choose.

Mendix then warns "Java versions below 25 are deprecated for deployment".
That is a deprecation notice on a project that builds and runs, which
beats an error on one that does neither.

runtimeMaxJavaVersion and resolveJDK21 have to move together, so there is
a test asserting it — raising one without the other would lower projects
to a version the launcher no longer uses, which is worse than not
lowering them at all.

Verified end to end on a real 11.14 project, with the same mxbuild and the
same JDK as the failing run: `mxcli new --version 11.14.0` now reports the
change, stores Java 21, and `mxbuild --target=deploy` reports BUILD
SUCCEEDED where it previously refused the compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(json): sanitise exposed names, and give the structure root MinOccurs 1
…ava 25

Mendix Studio Pro 11.14 supports Java 25, and its blank app is created
with JavaVersion = 25. mxcli resolved a JDK 21 unconditionally and
refused any other JAVA_HOME, so such a project could not be built:

    error: release version 25 not supported

and, if built elsewhere, could not boot — compiled for 25, launched on
21, UnsupportedClassVersionError … class file version 69.0.

The Java release is now a per-project variable rather than a constant.
resolveJDK takes the major, ProjectJavaMajor reads it from the project's
own Settings > Model > JavaVersion, and the four places that launch a
toolchain — build, settle, mxbuild --serve and the runtime — all resolve
from it. Using the SAME major for the build and the runtime is the point:
class files from a newer release cannot be loaded by an older JVM, so
resolving them independently would reintroduce the boot failure.

Measured end to end on a real 11.14 project left exactly as
mx create-project wrote it, with a Temurin 25 present:

  mxbuild --target=deploy            BUILD SUCCEEDED, no deprecation warning
  mxcli run --local                  Runtime started; app serving at :8080
  curl /  and  /dist/index.js        200 and 200
  the runtime process                /tmp/jdk25/bin/java

The no-regression control is the stronger half, and it is deliberately
adversarial: a Java 21 project booted with JAVA_HOME pointing at the JDK
25 must IGNORE it and launch on the 21. It does — verified per process
(/usr/lib/jvm/temurin-21-jdk-arm64), not from the model setting. Worth
recording that a stale JVM from the previous run made `ps | head -1`
report the wrong answer first; only the per-pid mapping settles it.

Two traps, both covered by tests:

- The major is matched against `java -version` TEXT, which has never had
  a machine-readable form. It must be followed by a dot, whitespace or
  the closing quote, or a request for 21 also matches a future 210.
- Zero means "the caller did not say" and resolves to 21, so an options
  struct that forgets the new field behaves exactly as before rather than
  silently changing which JDK is used.

When the required JDK is missing the error names the version, what was
searched, and the second way out that a bare "install a JDK" hides: the
requirement is a model setting, and a project can be built for a release
the machine already has. That advice is only shown when the requirement
is not the default, where it would otherwise be noise.

mxcli run --help no longer calls version-aware JDK selection a follow-up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(new): a Mendix 11.14 project that builds as generated
feat(run): build and run on the JDK the project asks for, including Java 25
ako/mxcli-maintenance §7. `create or replace navigation Phone` failed:

    navigation profile not found: Phone (available: Responsive)

and nothing else in mxcli added one. Mendix routes a phone to a Phone
PROFILE on the User-Agent, not on viewport width, so this put a whole
class of app out of reach: phone-specific pages could be written but no
phone browser could land on them, and a layout naming the missing profile
failed CE1613.

A profile is now created when the name is one of Mendix's fixed web kinds
— Responsive, Phone, Tablet — and re-running the statement updates it as
before, so a script stays re-runnable.

THE SHAPE IS PINNED AGAINST A REAL DOCUMENT, and that is the whole of
this change. Read over PED from a Studio Pro 11.14 project carrying all
three web profiles:

                              Responsive  Phone                        Tablet
  ProgressiveWebAppSettings   null        {Precaching, InstallPrompt}  null
  AppIcon                     Atlas icon  ""                           ""
  Name / Kind                 Responsive  Phone                        Tablet

All three store the same fourteen keys. Only Phone carries PWA settings.
So the obvious implementation — clone the Responsive profile already in
the project and change its name, which is how this was first going to be
written — produces a Phone profile with no PWA settings, and mx check
reports 0 errors on it. That is the failure mode CLAUDE.md warns about,
and the reason the reference was worth asking for rather than working
around.

Three boundaries, each with a control:

- The kind set is CLOSED. Creating "Phone" adds the profile the platform
  routes to; creating "Mobile" would add one that never can, so an
  unknown name is still an error.
- Offline (PhoneOffline, …) and native profiles are excluded. Both are
  real Mendix values, but no reference document was available for either
  and a guessed profile is exactly what this commit exists to avoid.
- The legacy engine refuses rather than approximating, and MCP refuses
  because nothing was verified on that path.

The kind table lives in mdl/types because both sides need it and the
executor must not import a backend implementation (ADR-0002).

Verified end to end: Phone and Tablet created on a blank app produce the
same 13 keys + $ID as the Studio Pro-authored Responsive profile beside
them, with the PWA asymmetry reproduced; mx check reports 0 errors; and a
layout with `menubar (profile: 'Phone')` — the CE1613 the reporter had to
comment out — now builds clean. Reverting the PWA rule fails the test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t build it

#311 and #313 both merged, and together they contradict each other on
main: the JDK is now resolved from the project (so Java 25 builds and
runs), while `mxcli new` still lowered every 11.14 project to 21.

Measured on main with a JDK 25 installed and JAVA_HOME pointing at it:

    $ mxcli new X14 --version 11.14.0
      Java 25 → 21: mxcli builds and runs on JDK 21, and Mendix 11.14+'s
      runtime bundles are still Java 21 class files.

Both clauses of that message are false as of #313. mxcli runs on whatever
the project asks for, and the runtime bundles' class-file version was
never evidence that 25 was unusable — a JVM 25 loads Java 21 class files
perfectly well. I retracted that argument on #311 and should have closed
it rather than leaving it to merge.

The cost is not cosmetic: lowering downgrades the project against the
platform's direction and earns Mendix's "Java versions below 25 are
deprecated for deployment" warning, when leaving it at 25 builds clean.

So the step now asks the question that matters — can THIS machine build
what the project asks for — and lowers only when the answer is no. That
keeps the reason `mxcli new` had for touching the version at all (a
generated project whose first build fails is a bad starting point) while
dropping the assumption that only 21 is runnable.

Measured both ways with the same command:

  JDK 25 visible     stored Java: 25, nothing printed
  no JDK 25          "Java 25 → 21: no JDK 25 on this machine, and the
                      project would not build", stored Java: 21

Three cases are deliberately left alone rather than lowered: an
unreadable or unparseable version (overwriting a setting the user can see
with one they did not choose), a project already at the fallback, and a
machine with neither JDK — where lowering would swap one missing JDK for
another and hide that none is installed.

The version parsing moves to the docker package beside the resolver that
answers the question, so `new` and the launcher cannot drift on what
"Java25" means; a test guards that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…file

MDL-OFFLINE01. Mendix rejects an attribute bound across two or more
associations on any page an offline navigation profile can reach:

    CE6206  Attribute paths with multiple steps cannot be used on pages
            that are accessible through an offline-based navigation

It bites at a distance. The pages were valid; adding the PROFILE is what
invalidated them, so the error surfaces nowhere near the statement that
wrote the path — and mxcli writes such paths happily. Verified: a
datagrid column bound Req_Asset/Asset_Site/SiteName stores an
IndirectEntityRef with two steps and mx check reports 0 errors, which is
correct, because that project has no offline profile.

THE THRESHOLD IS TWO HOPS, NOT ONE, and the reference page proved it by
carrying both with only one flagged:

    MaintenanceRequest_Asset → AssetName               1 step   accepted
    MaintenanceRequest_Asset → Asset_Site → SiteName   2 steps  CE6206

Read over MCP from the project that hit it. Without that control the
obvious reading of "multiple steps" is "any indirect reference", which
would flag every association-bound column in an offline app.

In MDL a hop is a slash, so the count is strings.Count(path, "/") and no
BSON walk is needed. Two traps the tests pin:

- An XPath constraint is also full of slashes and is NOT an attribute
  path, so only attribute bindings are scanned.
- A dynamic text binds through TEMPLATE PARAMETERS rather than
  `Attribute:`, which is how the second reported error was written — a
  scan of `Attribute:` alone would have found one of the two.

It is a WARNING, not an error, and that is the design rather than a
shortcut. Deciding whether a page is reachable from an offline profile
means walking the whole page graph — home pages, menu items, and every
page those open. Studio Pro does that; `check` cannot, because it does
not build the catalog that holds the edges. The entry points alone would
have caught NEITHER reported error, since that profile's menu had one
item. So the diagnostic names the profile and states the condition
instead of asserting a reachability it has not established: being wrong
in the confident direction is how a checker teaches people to ignore it.

A project with no offline profile pays one navigation read and sees
nothing, which is the overwhelmingly common case.

Not covered: an end-to-end run against a project that HAS an offline
profile — mxcli cannot create one yet (#314 excludes offline kinds), so
the project-reading half is exercised by the negative case and the
decision logic by unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#260 took DESCRIBE from 195 of 327 real mappings parsing to 317. This
closes the remaining 10, which reduce to two causes across four distinct
documents — and the corpus now round-trips at 327/327.

1. An entity-less export ROOT printed as `. {`. `group as` covers a
   nested grouping node (#262), but a root has no member name, so it
   needed its own spelling: `group {`. The real document is
   CapitalConnector.EM_AttachedDataRequest (OneHarness), whose root is
   et=Object ent='' assoc='' oh=Parameter.

2. A custom handler's value-path parameter printed Mendix's storage
   markers raw — `Suggestion: (Value)`. The stored path is
   "…|suggestions|(Wrapper)|(Value)": an array of PRIMITIVES, whose value
   is named by the element's exposed name, `Value` (#268).

Printing the member is only half of (2). Emitting `Value` while the
builder concatenates it back as "…|(Wrapper)|Value" gives a path that
resolves to nothing, so the parameter path is now resolved THROUGH the
schema index, the way a member reference is — which is also how it steps
through the array to reach the wrapper.

Two things only a real build caught, both invisible to `mxcli check`:

  - The entity-less root fell through to the VALUE branch, because the
    builder decides object-vs-value on `def.Entity != ""`. That produced
    a project mxbuild cannot even LOAD: "Type
    ExportValueMappingElement does not contain a constructor with a
    parameter of type ExportMapping."

  - An element with NO association cannot be Find — mxbuild says so with
    CE0224 "No association selected for obtaining objects." It is
    Parameter, which is what the real document stores on both its
    elements. I would have guessed Find from the surrounding code.

Validated: 327/327 of the demo-app mappings describe into MDL that
parses (from 317); mxbuild 11.13 reports 0 errors on the repro, which
round-trips as "Unchanged"; unit suite, executor integration suite (337s),
422 MDL scripts and vet all green.

Refs #260

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s PWA settings

Each of Mendix's web profile kinds has an offline twin, and an offline
profile is not a different document: measured over PED on Studio Pro 11.14,
a freshly created PhoneOffline stores exactly the same fourteen keys as a
Phone and differs only in Kind and Name. So the six kinds share one builder
rather than needing a second, guessed shape.

The correction in the same change is the more important half.
ProgressiveWebAppSettings was written as {Precaching: true, InstallPrompt:
true} for Phone, copied from the reference project's Phone profile. Those
are the USER'S settings, not the platform's: the schema defaults Precaching
to false and makes the whole element optional, and a profile added through
Studio Pro's own model API materialises null for every kind — Phone and the
offline kinds included. mxcli would have turned precaching silently on for
every profile it created, with mx check reporting 0 errors either way.

The general lesson, recorded in the file and the symptom table: measure what
Studio Pro CREATES, by adding a profile over PED and reading back what it
filled in. Reading a profile the project already has measures the user.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dated

An offline navigation profile restricts every page it can reach: an
attribute may be bound across at most ONE association hop, and a longer path
is CE6206. This bites at a distance. The pages were valid, the statement
that broke them never mentions them, and mxcli writes multi-step paths
happily — so `create or replace navigation TabletOffline` printed one
cheerful line and the next build failed somewhere with no visible connection
to it. That is the finding from ako/mxcli-maintenance.

Creating an offline profile now scans the project's stored pages and
snippets and names the bindings that exceed the limit.

The scan keys on the stored DomainModels$AttributeRef rather than on a table
of widget property names, which is what makes it cover pluggable widgets: a
DataGrid2 column's binding sits several levels inside a
CustomWidgets$WidgetValue, at a key path no property table would have
listed. It deliberately does NOT match every IndirectEntityRef — a data
source that navigates an association is a different element and CE6206 does
not reject it.

It is a warning, not a refusal: whether a page is actually REACHABLE from
the new profile takes the whole page graph, which mxcli does not walk here.
Verified end to end on an 11.13 project — mxbuild reports 0 errors while the
flagged page is unreachable, and exactly the flagged column
(Req_Asset/Asset_Site/SiteName, 2 of 2) as CE6206 once the profile's home
page points at it. The sibling one-hop column in the same grid is accepted
throughout, which is the threshold control, confirmed by mxbuild rather than
by the rule that predicts it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t through

`create import mapping X with json structure Module.NoSuchThing` was
accepted and written verbatim. mxbuild reports the dangling reference as
CE1613 "… no longer exists", but only at build time — `mxcli check` and
even `check --references` passed.

The second failure is the one with teeth. The schema index is empty
whenever the structure cannot be loaded FOR ANY REASON, and resolvable()
reads an empty index as "there is nothing to validate against" — its
correct meaning for a schema-less mapping. So one typo in the source name
switched off every member check in that mapping, and this went through
with no complaint at all:

    create import mapping M.IM with json structure M.NoSuchThing
    { create M.Loc { LocId = nonsense } };     -- both wrong, both accepted

So the refusal is at the SOURCE rather than in resolvable(): a NAMED but
unresolvable source is an error, while NO source at all — XML schema,
message definition, the tests that create a schema-less mapping — must
keep working. The error names the structures that would have worked, the
shape mendixlabs#882 established for members.

Also: `null values` is an enum, not free text.
ExportMappings$NullValueOption is {LeaveOutElement, SendAsNil}, and mxcli
wrote whatever identifier it was given, so `null values Banana` reached
the stored document verbatim.

An EMPTY project and a backend that CANNOT LIST look identical from here.
Refusing is right for the first and wrong for the second, so the refusal
only fires when the listing succeeds. That distinction has teeth: two
pre-existing mock tests named a structure their mock did not provide and
started failing. The right fix was to give the mock the structure, not to
weaken the check — a mapping whose named source does not exist is an
inconsistent model to be asserting ID preservation against.

Still written through, and out of scope: `with xml schema
Module.NoSuchThing`. A gen type exists (modelsdk/gen/xmlschemas) so it is
verifiable in principle, but it needs a new ListXmlSchemas method across
the backend interface, both engines and the mock — a lot of plumbing for
a dangling-reference check on a document type mxcli cannot create. The
silent-disabling half does not apply there: an XML mapping never had
member validation, because mxcli cannot read XML schemas at all.

Validated: mxbuild 11.13 reports 0 errors on the repro; the three bad
forms are refused with the structures that would have worked; unit suite,
executor integration suite (317s), 421 MDL scripts and vet all green.

Refs #259

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ako and others added 14 commits August 27, 2026 19:00
…ee it

`cmd/mxcli/docker/build_integration_test.go` still called `resolveJDK21`,
which #311/#313 renamed to `resolveJDK(major)` when the Java release became
a per-project setting. The file is behind `//go:build integration`, so
neither `go build ./...` nor `go test ./...` compiles it: the break landed on
main with a green gate, and every open PR inherited it. It read as three
separate PR failures.

Two changes.

The call site now resolves the JDK the SCAFFOLDED PROJECT asks for, which
means moving the skip guard below `mx create-project`. Passing the old
constant back in as `resolveJDK(21)` would compile and would reinstate
exactly the assumption the rename removed — on an 11.14 row the guard would
look for a JDK 21 while the build needs 25.

`make vet` now vets once per build-tag set, `""` and `integration`. Vet only
type-checks, so this costs seconds and needs no mx, no JDK and no Docker —
against a CI job that spent half an hour reaching a compile error the
type-checker finds immediately. Verified in both directions: reintroducing
the old call fails `make vet` with the same message CI produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(ci): restore the integration build, and make `make vet` able to see it
feat(navigation): create a navigation profile — online and offline kinds
fix(new): only lower a project's Java version when this machine cannot build it
feat(check): warn on a multi-step attribute path under an offline profile
fix(mappings): describe the last two shapes that did not parse
All three passed every gate and were wrong only in what they told the reader,
which is the class of defect that costs the most and shows up in no test.

1. A reserved word now says whether QUOTING is the fix.

`Title: String(100)` is accepted in `create entity`; `write (Title)` in a
grant list is a parse error, and mxcli answered with three renames. Quoting
works — `write ("Title")` executes with the name intact — and
QUOTED_IDENTIFIER is in the expectation set the raw message already printed.
The maintenance app renamed a stored attribute to RequestTitle on that
advice, which is a schema change where quoting costs nothing.

The hint now branches on whether the platform reserves the name, not just
the parser, because the two have opposite remedies: a platform-reserved name
is rejected after the quotes are stripped (CE7247), so recommending quoting
there produces something that parses and then fails the build. Measured
against Mendix's own list: 38 of 41 hinted keywords are rescued by quoting,
and exactly three — Type, Default, Owner — are not. The old advice was wrong
for the 38 and right for the 3 by accident.

The reserved-word lists move to mdl/types so the validator (mdl/executor)
and the hinting (mdl/visitor) share one copy; neither may import the other.

2. `grant … on System.User` explains itself.

It failed with a UUID and a missing .mxunit path, which reads like a corrupt
project. It is not: the System module's domain model is not a stored unit in
ANY Mendix project. Refused up front by name, which also avoids the
half-applied state a late failure leaves — the roles are validated by then.
The message states that the limitation is universal, and names the
consequence otherwise rediscovered the hard way: a constraint that traverses
System is equally unusable.

3. `mxcli test` says why it found nothing.

"Found 0 test(s) in 1 file(s)" counted the DIRECTORY as a file, so it read
as though something had been opened and found wanting. The count is now of
files actually read, and an empty suite names the .mdl/.md files skipped for
their name — a .mdl in a test directory is almost always the intended test
misnamed, so listing it turns a dead end into a rename.

Each has a control: that the quoted form actually parses (without which the
first test passes against advice that does not work), that an ordinary
module is not caught by the System guard, and that FilesRead is not passing
by never being incremented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(mappings): resolve a mapping's schema source instead of writing it through
fix(errors): three messages that sent the maintenance app the wrong way
@ako
ako merged commit 15bca2d into mendixlabs:main Aug 27, 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.

1 participant