Skip to content

Sync ako/mxcli: LIST FOLDERS, OData publishing + external entities, test-runner cleanup, check-rule corrections - #861

Merged
ako merged 42 commits into
mendixlabs:mainfrom
ako:main
Aug 8, 2026
Merged

Sync ako/mxcli: LIST FOLDERS, OData publishing + external entities, test-runner cleanup, check-rule corrections#861
ako merged 42 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Thirty-one commits from the mxcli-formula1 findings loop, grouped by theme.

Folders: read the layout back

  • LIST FOLDERS [IN Module]MOVE could place a document in a folder, but nothing could read the placement back: SHOW STRUCTURE groups by document type at every depth and never names a folder, DESCRIBE answers for one document at a time. So a move could not be confirmed, and an intended layout could not be diffed against the real one, without opening the .mpr as SQLite. Empty folders ([0]) and documents still at the module root are listed too — a listing that hid them could not round-trip against an intended layout.
  • MOVE JAVA ACTION and MOVE ODATA SERVICE — neither CREATE form takes a folder clause, so MOVE is the only way those documents ever leave the module root. Both reduce to the existing reparent primitive. Watch the discriminator: MOVE FOLDER is told apart by the absence of a doctype keyword, so every keyword added to the rule must also be added to that condition.

Published OData services & external entities

  • Publish Integer as Edm.Int64, not Int32 — and say when an enum is published as a string (EnumerationAsString paired with Edm.String). Found by publishing one attribute of every Mendix type in a single service and letting mxbuild adjudicate the whole table at once; that also surfaced the enum case, which was only suspected.
  • Resolve constant credentials for the $metadata fetch — follow-up to the literal-credential fix. HttpUsername: '@Module.ApiUser' is a STRING_LITERAL, so the visitor's isLiteral flag said "literal" and the fifteen characters @Module.ApiUser went out as the username. A syntactic classification is not a semantic one: only the executor can say whether a quoted string names a constant. Now resolves the constant's design-time default, which is exactly what Studio Pro sends on the same fetch.
  • Use the client's own credentials to fetch $metadata — previously the fetch went out unauthenticated and returned an empty client.
  • Apply published-entity changes on modify — the modify branch now rebuilds EntityTypes/EntitySets and carries existing roles through.
  • Honour the contract's capability annotationsCountable, NonFilterableProperties, NonSortableProperties.
  • Read an attribute's OData mapping backODataMappedValue and ODataMappedPrimitiveCollectionValue were dropped on read.
  • Stop renaming an external attribute called namename was on the reserved list with the comment "Mendix system-managed attribute for the object name". It is not. Testing the whole list at once against mxbuild earned the seven entries that stay (id, owner, changedBy, changedDate, createdDate, type, context, all real CE7247s) and removed the one that was folklore. Two existing tests pinned the old answer and were corrected.

Microflows & pages

  • Write the InheritanceSplit and its case values — previously serialised without them.
  • Stop inventing an else on a type split in DESCRIBE.
  • Pass a dynamic query expression through unquoted — a computed SQL string was being quoted as a literal.
  • ALTER PAGE: reach widgets inside a customContent column.
  • ALTER PAGE: set an action button's caption via CaptionTemplate.

Security & structure

  • CREATE OR MODIFY MODULE ROLE — the idempotent form the other security statements already had.
  • Don't tell a statement to move before itself.

Check rules

Test runner

  • Implement @cleanup rollback — the annotation's documented default, previously parsed and ignored. The endpoint now wraps each call in startTransaction()/rollbackTransaction(), verified against Postgres with @cleanup none as the in-run control. A rollback that fails is reported per test, never silent.
  • Run the app's own after-startup microflow during --local — without it, a suite depending on startup state passed under --attach and failed under --local. --skip-app-startup opts out; the run always prints which of the two it did.
  • Say which after-startup microflow a local run displaces.
  • Resolve a project-relative path for --list too — listing that could not find a path execution finds is a confusing split.

Theme & docs

  • Add the widget-module layer, so Data Grid 2 follows the palette.
  • Re-point the filter-operator popovers at the palette — four rules in _datagrid-filters.scss baked a light-mode shadow while taking their background from the palette, which is why it read as a partial fix rather than an untouched widget.
  • Fix the type-split microflow examples that fail CE0090.
  • Ten symptom rows appended to the fix-issue table.

Both theme fixes were verified in the compiled CSS by line number, not in the source — a rule must land after the widget module's own or the cascade eats it, and a theme-cache file older than the SCSS you just wrote is a stale compile.

Verification

Every change is covered by unit tests; grammar changes additionally went through the doctype integration gate (go test -tags integration -run TestMxCheck_DoctypeScripts) on both the modelsdk and legacy engines, which builds each script with real mxbuild. The lesson recorded from an earlier miss: for a grammar change the control sweep must run that gate, not mxcli check.

claude and others added 30 commits August 7, 2026 21:29
The @cleanup annotation has documented rollback as its default since the
runner shipped, but TestCase.Cleanup was parsed and then used nowhere: every
test committed. The after-startup runner had no seam to implement it — tests
execute inside the startup action, so there is no context the runner owns.
The test endpoint creates that seam, because it builds the IContext each test
runs on.

The handler now wraps the call in startTransaction()/rollbackTransaction()
when the runner asks for it, in a finally so a throwing test — the one most
likely to leave half-written data — is rolled back too. @cleanup none commits,
for when the writes are the point.

Verified against Postgres rather than the endpoint's own claim: a suite with
one rollback test and one @cleanup none test, run against an emptied table,
leaves exactly the "none" row behind. Same microflow, same run, only the
annotation differs.

Two failure modes this closes rather than opens:

  - An unknown strategy (@cleanup rollbak) is now a parse error. Treating it
    as "not rollback" would leave the data behind while the run still reported
    a clean pass. Rejected at parse time, so --list catches it and no runtime
    is booted for a file that cannot run correctly. The .mdl and .md parsers
    are separate code paths and both are covered — the first version of this
    only reached one of them.
  - A rollback that fails is reported per test and summarised at the end,
    never swallowed. --verbose tags every result [rolled back] / [committed] /
    [ROLLBACK FAILED]. An endpoint too old to know the parameter is called out
    specifically, since --attach can meet one.

Rollback applies to --local and --attach; Docker keeps committing, and the
docs say so. It matters most under --attach, where the database belongs to
the developer's running app.

Each new test was verified to fail against a stubbed guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Reported from a real project (mxcli-formula1 findings #19): a suite passed
under --attach and failed under --local, with cached-service assertions
seeing zero rows. The app loads its cache from an after-startup microflow,
and the --local runner displaced that microflow with its own.

That was a deliberate choice — a test run wants a known baseline — but it
was invisible. The run printed only "After-startup set to
MxTest.RegisterEndpoint", never that the user's startup logic had been
displaced, so the failure looked like a bug in the code under test.

It was also inconsistent: the hosted --test-endpoint path already chained
the project's own microflow, which is precisely why the two modes disagreed.
--local now chains it too, so a suite behaves the same either way and tests
see the app in the state it really boots into. --skip-app-startup opts out
for a deterministic empty baseline, and the run always prints which of the
two it did:

  … (registers the endpoint; runs no tests, then runs your MyModule.ASU_Startup)
  … (registers the endpoint; runs no tests; --skip-app-startup, so … will NOT run)

Verified live with a seeding after-startup microflow and a test asserting on
its row: PASS chained, FAIL under --skip-app-startup, from an emptied table.
Note the startup microflow's writes are not covered by @cleanup rollback —
they happen at boot, outside any test's transaction.

Also from the same report (#15): mxcli test --list bypassed resolveTestPaths,
so a project-relative path resolved for execution but not for listing.
Confirmed against the pre-fix binary, which fails with "stat tests/: no such
file or directory" on the command that now works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
`create non-persistent entity X ( Name: String(100) not null error '…' )`
passed both `mxcli check` and `mxcli exec`, and only a real build caught it:

    [error] [CE0070] "Validations rules are not allowed on entity 'X',
                      because it is not persistable."

`not null` and `unique` ARE validation rules — Studio Pro models "required"
and "uniqueness" as rules on the entity rather than as column constraints — so
Mendix rejects both on a non-persistable entity. Nothing in mxcli connected the
attribute constraint to the entity's persistence kind.

The construct matrix was established against mxbuild 11.6.6 rather than taken
from the issue text: `not null` with a message, `not null` bare, and `unique`
each produce CE0070, while a plain attribute does not. The bare form matters —
the report only showed the message form, and treating the message as the
trigger would have left half the bug in place.

Scoped to the CREATE path, where the persistence kind is known. An
`ALTER ENTITY … ADD ATTRIBUTE` does not carry it and cannot be told apart from
a persistent entity without a project — the same limitation MDL020 has, and the
rule comment says so rather than pretending otherwise.

Checked for false positives before committing: no file in mdl-examples/ trips
the new rule, and scripts/check-skill-mdl.sh still passes all 189 checkable
blocks. The negative test is a .fail.mdl (must fail check, enforced by
`make check-mdl`), paired with an -ok.mdl that pins the other edge — the same
constraints on a persistent entity, which mxbuild confirms builds clean.

Fixes mendixlabs#832

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
A statement's own name is "defined in the script but not yet created" at the
moment it fails, so annotateForwardRef matched it and appended

    hint: X is defined later in this script — move its create statement
          before this one

to any validation error whose message named its own subject. The advice is
impossible to follow: the statement it points at is the one that failed.

Surfaced by MDL054, whose message names the entity being created, but the
misfire is general — it applies to any create statement whose error mentions
itself.

The fix uses the ast.Statement parameter the function already took and
deliberately ignored (`_ ast.Statement`): collect the names the failing
statement defines and skip them. A genuine forward reference — a name some
LATER statement defines — is still annotated, which the test pins alongside
the regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
Brings the branch up to date with main (PR #112) so the PR merges cleanly and
CI runs against the current base. No conflicts; the fix-issue.md symptom table
merged via the union driver with both sides' rows intact and no duplicates.
Full suite green on the merged tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
`retrieve $L from Mod.Entity where [Name = $RefProduct/Mod.Product_Category/Name]`
passed both `mxcli check` and `mxcli exec`, and the build then failed with
CE0161 "Error(s) in XPath constraint". Mendix XPath reaches at most one hop off
a variable, and nothing checked the hop count.

The valid/invalid boundary was established against mxbuild 11.6.6 rather than
inferred, and it is narrower than it first appears:

    $Var/Attr             VALID   the parameter's own attribute
    $Var/Mod.Assoc        VALID   one hop, the associated object
    $Var/Mod.Assoc/Attr   CE0161  two or more hops

so the rule keys on the number of segments. The obvious formulation — flag a
module-qualified segment following a variable — would have rejected the middle
form, which builds clean. Confirmed by building all three and then dropping the
offender to verify the remaining two report 0 errors.

This is a rejection rather than a smarter serializer because there is no valid
XPath for the two-hop form: the constraint has to be restructured, and only the
author knows which of the two shapes they meant.

Both rewrites the message recommends were built and confirmed at 0 errors
before the text claimed they work — retrieving the associated object first
(one hop is a legal retrieve SOURCE) and constraining on that variable's own
attribute, or inverting so the traversal starts at the entity being retrieved.

No positive example in mdl-examples/ trips the rule, and
scripts/check-skill-mdl.sh still passes all 189 checkable blocks. Negative test
is a .fail.mdl paired with an -ok.mdl carrying both rewrites plus the two
one-hop forms that must not be flagged.

Fixes mendixlabs#831

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
`mxcli test tests/ -p app/App.mpr` ran from the solution root and
`--list` on the same command line did not:

    Error: stat tests/: no such file or directory

resolveTestPaths was called below the --list branch, which returned first.
Listing and running now accept the same paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Every whole-number attribute in a published service failed the build, one
CE5016 each:

    Attribute …Stg_Season.Year is has type Integer, but is published as
    Edm.Int32.

Mendix publishes Integer as Int64, same as Long. The mapping's own comment
flagged Integer as an unverified guess, and the existing test pinned the guess.

Publishing every attribute type on 11.12.1 and reading the errors off the build
also caught a second wrong pair the report had only suspected: an enumeration
was written as Edm.String with EnumerationAsString hardcoded false, which is the
one combination Mendix rejects — CE5016 plus CE4583 "Enumeration 'Edm.Colour' is
not published in this service". The type and the flag are one setting, so the
flag now travels with the attribute.

Verified: the same all-types service builds 0 errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`create or modify external entity` touching only an entity-level property
detonated every attribute of the entity:

    [CE6612] "Attribute 'circuitId' of external entity 'Stg_Circuit' is not supported."

one per attribute, leaving a project that cannot build.

The executor already preserves attributes it was not asked to change, so the
loss was a layer down: attributeFromGen handled StoredValue and OqlViewValue but
not Rest$ODataMappedValue. Every attribute of an external entity therefore came
back with no RemoteName, and the writer's `isExternal && a.RemoteName != ""` arm
fell through to a plain StoredValue on the next read-modify-write. The
per-attribute Filterable/Sortable/Creatable/Updatable flags live on the same
value and were lost with it.

This is the attribute-level half of mendixlabs#782, which fixed the entity level only.

Verified on 11.12.1 against a real contract import: three CE6612 before, none
after, and the modify's own change still lands.

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

`mxcli check` reported MDL048 for `where [id = $GuidText]` and `mxcli exec`
wrote the microflow anyway, so a script that skips check produced a project the
build fails with CE0161. The cause is two validators: the exec path ran
ValidateMicroflowBody (semantic errors), while the MDL0xx rule set lives in
ValidateMicroflow, wired only into cmd_check.go and the LSP. Same shape as
mendixlabs#836, where a guard existed on every exec path but was never reached from
validate.

Enforced at exec via an explicit allowlist of rules whose claims were verified
against mxbuild 11.6.6 — MDL047, MDL048 and MDL055, all XPath-constraint rules
whose constructs were built and confirmed to fail CE0161.

Blanket promotion of all 17 error-severity rules was implemented first and then
reverted, because it makes every rule a write barrier and at least one rule is
wrong: MDL009 ("enumeration splits require exactly one value per branch") is a
FALSE POSITIVE — a multi-value branch covering every enum value builds at 0
errors, and the shipped write-microflows skill documents exactly that form. It
also broke an existing test whose fixture uses `else` on an enum split. MDL008
is by contrast correct (mxbuild reports CE0079 per uncovered value plus
CE0773), which is the point: the two rules look alike and only a real build
tells them apart. A test now fails if the allowlist is widened without that
check.

Placed in the create handler rather than validateWithContext, so
`check --references` does not report each violation twice. Warnings are never
promoted — check itself passes with them.

Fixes mendixlabs#833

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
CREATE EXTERNAL ENTITIES read names, types and navigation properties out of the
contract correctly, then defaulted every capability to true regardless of what
the contract said. Mendix compares the two at build time and refuses:

    'Seasons' is marked Countable=False in the OData service, but True in the app.
    'latitude' is marked Filterable=False in the OData service, but True in the app.

Eight errors from an eight-resource import — on the one command whose whole job
is fidelity to the contract.

Insert/Update/Delete restrictions were already parsed; Count/Filter/Sort were
not, so there was nothing for the import to honour. An unannotated set still
means countable/filterable/sortable, which is OData's own default — silence is
not a restriction.

Verified on 11.12.1: a contract declaring CountRestrictions/Countable=false and
NonFilterableProperties produced two CE6630 before, none after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
CREATE ODATA CLIENT accepts UseAuthentication / HttpUsername / HttpPassword and
stores them for the runtime, but the design-time fetch was a bare client.Get.
Against a service behind `authentication basic` that is a 401 — and since the
failure is only a warning, the client is created with no cached entity types, so
the CREATE EXTERNAL ENTITIES that follows imports nothing from a script that
looks like it succeeded.

The credentials and any HEADERS now go out with the fetch.

Only literals can be used. The visitor strips a quoted literal's quotes, so
'f1api' and Module.ApiUser both arrive as bare strings; the AST now records
which was written. A constant is resolved by the runtime, and sending its *name*
as the password would be worse than sending nothing — so unresolved names are
reported instead, alongside a note that the client was left empty and that
pointing MetadataUrl at a committed contract file avoids the problem entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`execute database query … dynamic $Sql` reached the runtime as the string
literal '$Sql', so the database was asked to execute four characters:

    ERROR - ExternalDatabaseConnector: Parser Error: syntax error at or near "$"

The builder quoted anything not already starting with a quote — correct for
`dynamic 'SELECT …'`, wrong for an expression — and the AST kept no
literal-vs-expression flag, so it could not tell them apart. That blocked
runtime-built SQL, and therefore query pushdown, outright.

Verified by reading the stored BSON: DynamicQuery now holds $Sql, not '$Sql'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The hint said "Known properties here: ReadMode, InsertMode, UpdateMode,
DeleteMode, UsePaging, PageSize" long after the visitor learned Countable,
SkipSupported and TopSupported — so a user typing an accepted property was told
it was unknown.

The lists are separate by design (the visitor decides, the hint displays), but
nothing kept them in step. The AST struct is now the source: every field of
PublishedEntityDef and CreateExternalEntityStmt must be advertised or explicitly
listed as structural, so adding a property and forgetting the hint is a test
failure instead of a wrong message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`mxcli test --local` sets the after-startup microflow to its own endpoint
registrar and restores it afterwards — deliberate, because a test run wants a
known starting state. But it said only:

    After-startup set to MxTest.RegisterEndpoint (registers the endpoint; runs no tests)

so a suite that needs startup state passes under --attach and fails under
--local against an empty scratch database, with nothing in the failure pointing
at the cause. The tests were asking for state the runner had prevented.

The run now names the displaced microflow and says --attach is the way to test
against an app that has actually started up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
MDL009 errored on `when Open, Pending then` with "Mendix enumeration splits
require exactly one value per branch". Mendix does no such thing: verified on
mxbuild 11.6.6, a multi-value branch covering every value plus `(empty)` builds
with 0 errors. So `mxcli check` was rejecting valid MDL — and contradicting the
shipped write-microflows skill, which documents that very form. The rule was
found while deciding what mendixlabs#833 could safely promote to a hard exec failure.

What actually fails the build is a MISSING branch. An enum split is an
exclusive split needing one outgoing flow per condition value, and an uncovered
one is CE0079 "The 'X' condition value should be configured in properties for
an outgoing flow."

MDL056 checks the `(empty)` branch specifically. That half of CE0079 is
universal and needs no knowledge of the enumeration's members — confirmed it
fires even when the split is on a `not null` enum attribute — so it works from
the statement alone. Full value coverage is deliberately left out: it requires
resolving the split variable's type to an enum member list, which
ValidateMicroflow cannot see, and guessing would trade one false positive for
another.

A new rule ID rather than a repurposed MDL009, so anything still citing the old
number keeps meaning the old, wrong thing.

MDL008 (no `else` branch) is correct and stays — mxbuild reports CE0079 for
each uncovered value AND CE0773 on the else flow, so an `else` does not stand
in for the missing flows. The skill's CASE example used `else` and is corrected
here in the same change, since leaving it would teach MDL that fails the build.

No positive example in mdl-examples/ trips MDL056; check-mdl and
check-skill-mdl both pass. The -ok.mdl repro builds at 0 errors in mxbuild,
including the multi-value branch MDL009 used to reject.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
Re-running a `create or modify odata service` after editing a `publish entity`
block did not apply the change. Marking a member Filterable and re-executing
left the served $metadata exactly as it was; only `drop odata service` + create
picked it up. The modify branch updated the service's scalar properties and
never touched EntityTypes or EntitySets.

Supplied entities now replace the stored ones wholesale. Replacing rather than
merging is what makes the script the description of the service: a member
removed from the script is removed from the service, which merging could not
express.

The same change carries AllowedModuleRoles across a modify. That is a guard, not
a reproduction — the reported grant loss (mxcli-formula1 #26) did not reproduce
on 11.12.1, on either the fixed or the previous build — but a modify cannot
express grants, so it must not be able to drop them.

Verified on 11.12.1: the same script yields `Label as 'label'` before and
`Label as 'label' (Filterable, Sortable)` after, and the build stays at 0 errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`create module role` had no `or modify` form, so re-running a security script
failed on the first role that already existed and role creation had to live in
its own run-once file.

`create or modify module role` now updates an existing role's description
instead of failing. AddModuleRole already overwrites, so it also adopts the
caller's casing — the same path the auto-provisioned-role branch above it uses.

`createModuleRoleStatement` carries its own CREATE keyword (it is dispatched
from securityStatement, not from the shared createStatement rule), so the
optional OR MODIFY goes in that rule and has to stay distinguishable from
`create or modify module`. Both spellings are covered by a test, and the full
doctype integration gate is green — the lesson from the last grammar change is
that a `check`-only sweep proves nothing about what the visitor builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Implement @cleanup rollback, and stop --local displacing the app's after-startup microflow
`split type` produced a project mxbuild could not load at all:

    KeyNotFoundException: The given key '<guid>' was not present in the
    dictionary  at StreamingBsonUnitReader.ResolvePostponedProperties()

`mxcli check` passed and `mxcli exec` reported success. Reproduced on Mendix
11.6.6 and 11.13.0. Found while testing whether an enum-split `else` is
version-dependent.

Two gaps in the modelsdk writer, both the mendixlabs#791 shape — an object dropped at
serialization while the sequence flows pointing at it are still written:

  1. microflowObjectToGen had no *microflows.InheritanceSplit case, so the
     split hit `default: return nil` and vanished. Three flows referenced its
     $ID; that is the dangling pointer the loader trips on.
  2. caseValueToGen had no InheritanceCase case, so every branch degraded to a
     bare Microflows$NoCase and lost the entity it selects on. Its
     value-receiver normalisation omitted the type as well, so handling only
     the pointer form would still have missed half the calls.

Diagnosed with the recipe the symptom table already records for this class:
dump the microflow, collect every $ID, check each key ending in `Pointer`
resolves. Before: 27 objects, 10 pointers, 3 dangling. After: 28, 10, 0.

Field list taken from the generated type rather than the legacy serializer.
Legacy writes ErrorHandlingType on the split, but initInheritanceSplit has no
such property — Mendix does not define it there — so the codec omits it.

Verified end-to-end: the repro script now reports 0 errors on both 11.6.6 and
11.13.0, where it previously could not be loaded.

Two modelling rules were confirmed on both versions along the way and are
recorded in the repro: a type split needs an outgoing flow for every type
INCLUDING the base entity (CE0090 otherwise), and an `else` does not substitute
for the base-type case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
An attribute named `name` came out of CREATE EXTERNAL ENTITIES prefixed with the
remote type — Stg_Drivername, Circuitname — so a page written against the
published $metadata failed with "The selected attribute 'F1Live.Drivers.name'
no longer exists", and the same field carried a different name in every module
because the remote type names differ.

`name` was simply not reserved. Adjudicated on 11.12.1 by importing a contract
with a property for each name on the list and prefixing disabled: Mendix answered
CE7247 "The name 'x' is a reserved word" for id, owner, changedBy, changedDate,
createdDate, type and context — and said nothing about `name`. So this is one
wrong entry, not a scheme that needs redesigning; the other seven earn their
place and keep it.

The remaining renames are now reported at the end of an import instead of being
discovered later when a page will not build.

Note for existing projects: a re-import renames `Stg_Drivername` back to `name`,
which is the point, but anything referring to the old name has to follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
MOVE accepted seven doctypes and rejected the rest at parse time
(`no viable alternative at input 'MOVEJAVA'`). Neither CREATE JAVA ACTION nor
CREATE ODATA SERVICE takes a folder clause either, so those documents could
never leave the module root from MDL — five of the reporting project's documents
were stuck there while the other 36 sorted into folders.

Both are plain document units, so each reduces to the existing reparent
primitive: the executor sets ContainerID and calls the backend, which persists
the containment row and touches nothing else. sdk/mpr's moveUnitByID is exported
for the doctypes that have no dedicated writer method of their own.

Verified on 11.12.1: `move java action` and `move odata service` into
'Support' and 'Api/Published' created exactly three folders (two levels for the
nested path), left the document count unchanged, and the project still loads and
builds. Full doctype integration gate green — mandatory for a grammar change,
and it caught a bad first draft of the new example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The write-microflows skill taught `case Spec` + `else` with no branch for the
base entity, and described `else` as handling "objects that do not match any
listed specialization". It does not. An object-type decision needs an outgoing
flow for every listed type, and without the base entity the build fails

    [error] [CE0090] "The 'X' value should be configured for an outgoing flow."

`else` IS accepted — it serializes as Microflows$NoCase — which is what made
the guidance look right. It simply does not satisfy coverage, so it is
redundant once every type has a branch.

Matrix verified on 11.6.6 and 11.13.0:

    specializations + base           0 errors
    specializations + base + else    0 errors  (else redundant)
    specializations + else only      CE0090

The examples also omitted a return after `end split;`. Branch bodies converge
on a merge that continues to the microflow's end event, so a non-void microflow
needs one — otherwise mxcli check reports MDL003 and the build fails CE0067
"The 'Return value' property is required."

Two shipped examples had the same defect and did not build:
mdl-examples/bug-tests/365-microflow-inheritance-split.mdl and
475-inheritance-split-continuing-branch-merge.mdl — the latter's own header
claimed "mx check against the resulting MPR reports 0 errors", which had not
been true. Both now build clean on 11.6.6 and 11.13.0.

475's added base case is deliberately a TERMINATING branch. The scenario it
pins is "exactly one non-split branch continues"; an empty, falling-through
body would make two branches continue and quietly retire the regression.
Verified after the edit that the post-split activity still renders outside both
case bodies and that the describe→exec roundtrip is mxbuild-clean.

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

Reported from the Formula1 test build (FINDINGS §33): a themed app is
on-palette everywhere, and then a few things are not. The Data Grid 2 pager
caption — "1-15 of 77", the only thing telling a user where they are in the
result set — measured 1.02:1 against a dark ground and was invisible. Row-select
checkboxes stayed stock Mendix blue in a re-branded app, the loader flashed
white on every page turn, and popovers cast light-mode shadows.

One cause. _mxcli-atlas-map.scss re-points Atlas Core's custom properties, which
covers the app. The theme source shipped by the *widget modules* under
themesource/ styles some things with Sass variables and literals instead, and
Sass resolves those at compile time, before any custom property exists — so the
value is baked into theme.compiled.css and no --mxt-* can move it. The parts
that did work resolve var(--gray-darker, …) through Atlas: same pager bar, two
mechanisms, one of them reachable.

Adds _mxcli-widgets.scss, a third shared partial imported after the theme's own,
correcting each baked declaration through a token so both palettes follow.

The obvious fix does not work, and it is worth writing down why. Each module's
main.scss imports theme/web/custom-variables *before* its own `!default`
variables, so setting `$pagination-caption-color: var(--mxt-ink-muted)` there
would win and Sass would substitute the var() into every use site. But the names
collide with Atlas Core's, and Atlas Core feeds them to Sass colour functions —
atlas_core/web/_variables.scss:20 computes mix($brand-primary, #e7e7e9, 10%),
and handing mix() a var() is a compile error, so the app stops building. And the
worst offenders are not behind a variable at all: _three-state-checkbox.scss
writes #264ae5 and rgba(#264ae5, 0.4) directly.

Every selector was read out of a compiled theme.compiled.css rather than from
the SCSS sources. That distinction halved the work: the sources are full of
`var(--token, #fallback)` declarations that already resolve correctly, and of
the 46 declarations mentioning the stock blue, 24 were harmless fallbacks. The
report's own list was assembled from the sources and is correspondingly longer.

Verified in a browser, both variants, both light-first and dark-first themes:
pager caption 1.02:1 -> 6.99:1 on console dark and 6.39:1 light (the exact
rgb(154,166,180) / rgb(85,96,110) the report measured for its own fix), 6.78 /
5.93 on signal. Checked-checkbox fill and loader background resolve to
--mxt-brand and --mxt-surface in the compiled output, with mxcli's declaration
last.

A test asserts the layer reintroduces no literal colour, and the shared-partial
drift guard now covers both shared files rather than only the Atlas map.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEZmExJUvn2nWTWE9mrd4i
fix(theme): add the widget-module layer, so Data Grid 2 follows the palette
Fix three upstream check/exec gaps (mendixlabs#831, mendixlabs#832, mendixlabs#833) + two corruptions found while verifying
…stics-spike-emta6h

# Conflicts:
#	cmd/mxcli/cmd_test_run.go
#	cmd/mxcli/testrunner/runner.go
ako and others added 12 commits August 8, 2026 14:03
OData publishing, external-entity imports, dynamic SQL and folders: eleven formula1 findings
DESCRIBE printed a bare `else` for a `split type` that never had one, and a
describe→exec roundtrip accumulated another each pass.

An object-type decision always carries an `(empty)` outgoing flow — the
null-object case — which the builder emits whether or not an `else` was
written. DESCRIBE rendered that flow as an `else`. The artifact was invisible
until the InheritanceCase writer landed: before that every branch flow degraded
to a bare NoCase, so nothing distinguished the `(empty)` flow from a real case.

Fixed in the describer: drop the `else` line when its body renders empty, using
the same elseLineIdx/truncate pattern the if/else emitters already use. Exec
re-creates the flow, so the omission is lossless — verified that describe→exec
→describe is byte-stable for an empty-branch split, an all-branches-return
split, and an authored else.

The obvious fix — not emitting the branch in the builder when no `else` is
written — was implemented first and is wrong. Without that flow the build fails

    CE0089 "The '(empty)' value should be configured for an outgoing flow."

so it is load-bearing, and MDL's `else` on an inheritance split IS the `(empty)`
case. That also explains a result from the previous commit: an `else` cannot
substitute for the base entity's own case (CE0090) because `(empty)` and the
base type cover different things.

Worth recording how the wrong fix was caught: every shape was re-run through
mxbuild, not just the test suite. The unit tests passed against it — the type
splits it broke only failed at build time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
Literal credentials started working when the fetch learned to authenticate, but
a constant reference still got a 401 and an empty client. That is the shape MDL
pushes users towards — mxcli requires a constant for ServiceUrl, so a client
written the documented way has constants for its credentials too. The tool
insisted on the shape whose credentials it would not read.

The quoted spelling was the sharp edge. `'@Module.ApiUser'` is a STRING_LITERAL,
so the isLiteral flag says "literal" and the previous code sent the fifteen
characters `@Module.ApiUser` as the username — worse than a 401, because it
looks like it tried, and no unresolved-credential note fired either.

All three spellings now resolve: a literal, `@Module.Name`, and the same
reference quoted. A constant's design-time default is exactly what Studio Pro
uses for its own fetch, so reading it is not a workaround — it is the value. An
unknown constant, or one with no default, still reports itself unresolved rather
than sending something that merely looks like a credential.

Verified against a basic-auth server that 401s without credentials and 403s
without a custom header: all three spellings cache the contract, where the
quoted form previously failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`alter page … set <prop> on <widget>` reported "widget not found" when the
widget lived inside a datagrid column rendered as customContent, so the only
way to touch it was CREATE OR REPLACE PAGE — a full page rewrite.

findInWidgetChildren's pluggable branch searched the grid's own
Object.Properties[].Value.Widgets and matched columns by their derived name,
but never descended into a COLUMN's own content. Columns live at
Object.Properties[columns].Value.Objects[]; a column's widgets are one level
deeper, at Properties[content].Value.Widgets[].

Addressing is by the nested widget's OWN name. A `grid.column.widget` path was
considered and rejected: DataGrid2 columns carry no stored name in the MPR (the
existing findBsonColumn documents this), so the column segment could only ever
be a derived name — the bound attribute, or the caption — which changes the
moment someone edits the caption, leaving such a path silently stale. The
nested widget's name is real and stable, and the grammar needs no change.

A second test pins that a column still resolves by its derived name, since the
new descent runs in the same loop and could otherwise shadow it.

Fixes mendixlabs#834

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
`alter page … set Caption = '…' on <actionbutton>` failed with "widget has no
Caption property" for every action button — nested or top-level.

An ActionButton has no `Caption` document. Its caption is a
Forms$ClientTemplate stored under `CaptionTemplate` (Template → Items[] →
Translation.Text), which is the same structure setWidgetContentMut already
walked for `Content`. setWidgetCaptionMut only looked for `Caption`.

Both setters now share setClientTemplateText.

Found while fixing mendixlabs#834 and worth separating: the reporter hit it inside a
customContent column, but it reproduces on a plain top-level button too, so it
is an independent defect — and the mendixlabs#834 finder fix alone would not have made
their command work. Verified end-to-end that both the nested and the top-level
button now take a new caption, and that the resulting projects build with 0
errors on mxbuild 11.6.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
The widget layer covered .column-selectors and stopped there. Four more rules in
_datagrid-filters.scss bake the same two-layer light-mode shadow —

    box-shadow: 0 2px 20px 1px rgba(5, 15, 129, .05),
                0 2px 16px 0 rgba(33, 43, 54, .08);

— on the filter-operator popover, the dropdown filter's list in both its
standalone and contained forms, and the list inside a dropdown container. Each
already takes its background from --bg-color-secondary, so Atlas re-colours the
panel and leaves the shadow: elevation drawn for a light ground, floating over a
dark one.

Selectors read out of the shipped themesource, not the report — the fourth is
`.dropdown-container .dropdown-list`, which is nested and easy to miss.

Verified the way §33 insists on: applied the theme to a real project, ran
mxbuild, and read theme.compiled.css. The rule lands at line 30794, after the
widget module's own at 27765, so it wins the cascade.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
MOVE could place a document in a folder, but nothing could read the
placement back. SHOW STRUCTURE groups by document type at every depth and
never names a folder; DESCRIBE answers for one document at a time. So a
move could not be confirmed, and an intended layout could not be diffed
against the real one, without opening the .mpr as SQLite.

LIST FOLDERS [IN Module] renders module -> folder path -> documents:

  Mv
    (module root)  [1]
      Microflow Read_Rows
    Api  [0]
    Api/Published  [1]
      ODataService Api
    Support  [1]
      JavaAction Helper

Three properties are load-bearing for the diff use case:

- Empty folders are listed ([0]). A listing that hid them could not
  round-trip against an intended layout.
- Documents still at the module root appear under "(module root)" rather
  than by subtraction -- what is not filed yet is what you most want to
  notice.
- Ordering is stable, so a diff shows only real movement.

Documents are indexed by ContainerID across every list call the backend
offers, each best-effort: a backend that cannot answer one kind yields a
listing missing that kind rather than no listing at all.

LIST is the verb per .claude/skills/design-mdl-syntax.md; SHOW is accepted
as the legacy spelling. FOLDERS is added to the keyword rule so it remains
usable as an identifier.

Wired end to end: MDLLexer.g4 (FOLDERS), MDLCatalog.g4 (showOrList FOLDERS
(IN ...)?), ast.ShowFolders, visitor, execShow. Syntax topic "folders",
quick-reference rows, organize-project skill section, doctype example,
symptom row.

Tests: cmd_list_folders_test.go. Verified live against a real project
(both verbs, with and without IN, --json).

mxcli-formula1 issue #2
LIST FOLDERS, constant credentials for $metadata, and dark-mode filter popovers
Fix mendixlabs#834 (ALTER PAGE into a customContent column) + two defects found with it
@ako
ako merged commit 2f5b182 into mendixlabs:main Aug 8, 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