Skip to content

Sync ako/mxcli: ALTER PAGE pluggable fixes, describe round trips, integration import fixes, lint catalog - #1576

Merged
ako merged 46 commits into
mendixlabs:mainfrom
ako:main
Oct 18, 2026
Merged

ako merged 46 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator

Syncs ako/mxcli:main into upstream: 22 commits from fork PRs ako#1460–ako#1466, ako#1469–ako#1475 and ako#1478–ako#1481.

ALTER PAGE

describe → exec round trips

Integrations

check

Lint and catalog functions

Tests

claude and others added 30 commits October 17, 2026 07:06
…ixlabs#1499)

`revoke create on entity M.E from M.RoleA, M.RoleB, M.RoleC` printed
"Unchanged entity access: … — nothing to revoke", exit 0, and every rule
kept create. Same cause as mendixlabs#1501: the partial-revoke matcher only touched a
rule whose role set equalled the statement's, and create/delete go through
that matcher too. fb0cec9 already fixed it on main; its tests pinned only
the member-write right.

Adds a backend test in the reported shape (one rule per role, two with
XPath, one without), the bug-test script, and the finding. Control:
restoring the sameStringSet guard fails the test with 0 rules modified and
create kept on all three; the pre-fix binary prints the reported notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PYLYzy5hqkdw3nKZm6uuS
…"/"no"

`alter page … set (showFooter: true, clearable: false) on cbColor` stored
PrimitiveValue "yes" / "no" (mendixlabs#1454). Studio Pro and the
widget template store a Boolean as "true" / "false", and the client reads
anything but "true" as false, so a bare `true` passed mx check and turned
the property off.

setPluggableWidgetPropertyMut now spells a bool from the property's
declared kind: Boolean -> "true"/"false"; an Enumeration that declares
yes/no keys keeps taking a boolean as those keys; any other enumeration
refuses a boolean and names its keys.

Control: with the fix stashed, TestSetPluggableBooleanWritesTrueFalse_1454
fails with `PrimitiveValue = "yes", want "true"`. On a blank 11.13.0 app the
unfixed and fixed builds differ in exactly the two reported values, and
both report 0 errors from mx check, which is why only the stored bytes
show the defect.

Refs #1327

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019httGxDh8UXt1Zoyh3Gh52
… single user`

Every single- and multi-user task was written with
AutoAssignSingleTargetUser = false (a constant in userTaskToGen and the MCP
constructor), the reader never read it, describe never printed it, and MDL
had no spelling, so `create or modify workflow` turned off a box ticked in
Studio Pro. The runtime then did not assign a task whose targeting yielded
one user, and completing it from a microflow failed with "it is not
assigned to you". check and mx check were silent.

- `auto assign single user` on user task and multi user task, at most once
- written from the model, read back on all three task types, described when set
- create or modify / replace activity that does not restate a stored `true`
  is refused, like `await all users` (omission must be able to mean off, so
  a carry would make it impossible to turn off)
- default stays off: Model SDK default false, and Studio Pro 11.14.0
  (ako/TestApp) stores false on every new user task

Fixes #1445 (mendixlabs#1547)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019MqgMzUGeMdUt9ZBdNkoUr
widgetSlotKeywordOverrides / mdlContainerForWidgetSlot decide that a Data
grid 2's filtersPlaceholder is spelled `controlbar` and a Gallery's
`filter`. The page mutator is about to resolve the same keyword for
`<widget>.<keyword>` targets, so the table moves to mdl/types beside
ItemPropertyAliases, for the same reason: two paths reading one table
cannot drift. No behaviour change; the executor names are kept as thin
aliases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019httGxDh8UXt1Zoyh3Gh52
`alter page … set (DataSource: microflow Shop.DS_Order_Open) on galOrders`
passed check, printed "Altered page" and left the page unit byte-identical
(mendixlabs#1548); the same statement on a list view worked. A
CustomWidget keeps its source in the datasource property's Value, not in a
top-level DataSource key, and SetWidgetDataSource's DSet onto that missing
key failed with its result ignored.

For a pluggable widget the source is now written into that property, via
the engine's pluggable serializer (the one CREATE and INSERT use). Microflow
and nanoflow sources are written; other kinds are refused with a pointer to
REPLACE, and check reports the refusal because its dry run runs the setter.
The plain-widget path now checks DSet's result, so no widget can report
success without a slot to write into.

Control: with the fix stashed, the four new tests fail (source unchanged, no
error). On a blank 11.13.0 app the unfixed build leaves both widgets on
`database from V.Item`; the fixed build shows `microflow V.DS_Items` for
both, and mx check reports 0 errors either way.

Refs #1446

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019httGxDh8UXt1Zoyh3Gh52
….controlbar

Once a Data grid 2's last control-bar button was dropped, describe printed
no `controlbar` and every route back was refused (mendixlabs#957):

  insert into controlBar1 { … }  -> widget "controlBar1" not found …
  insert into dataGrid21 { … }   -> cannot INSERT INTO "dataGrid21"
                                    (CustomWidgets$CustomWidget): it is not
                                    a simple container

The bar is the grid's filtersPlaceholder widgets-property: a slot with no
Name. The drop never deleted it (Widgets [2] stays in the BSON); describe
just omits an empty slot.

A pluggable widget's own widgets-typed properties are now addressed like a
data view's footer: `<widget>.<keyword>`, using the keyword describe prints
(types.WidgetSlotKeyword) or the property key. INSERT INTO appends, REPLACE
swaps the content, DROP empties it, BEFORE/AFTER are refused, and a member
that also names a column is refused as ambiguous. ResolveAlterTarget accepts
the address too. `insert into <grid>` on the grid itself now lists the slot
addresses.

Control: with the fix reverted the new tests fail with the reported
refusals; the unfixed binary answers `insert into dataGrid21.controlbar`
with "column \"controlbar\" … not found". Fixed, on a blank 11.13.0 app
(stock Administration.Account_Overview, both buttons dropped) the button
comes back inside `controlbar { … }` and mx check reports 0 errors.

Refs #1278

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019httGxDh8UXt1Zoyh3Gh52
set / insert before|after / replace / drop on a widget inside a legacy
Forms$Table cell reported `widget "X" not found` (mendixlabs#1453):
none of the three child walkers (the finder, the data-source walk, the
widget-scope collector) descended into Cells[].Widgets[].

All three now do. The descent is keyed on the Cells list rather than a
$Type, because a Mendix 11 cell is stored as Forms$DbTableCell, which gen
does not model; Forms$Table is the only page type with a Cells list.

Control: with the walk reverted, the new tests fail with `widget
"btnInCell" not found` and, for the scope collector alone, "btnInCell
missing from the widget scope".

Not verified against a Studio Pro-authored table: none exists in
testdata, MDL cannot create one, and mx convert refuses Forms$TableCell
(widget_list_markers.go). For the same reason there is no bug-test script.
A wrong guess at the key fails safe as the same not-found. `insert into`
an empty cell still has no address; a cell stores no name.

Refs #1326

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019httGxDh8UXt1Zoyh3Gh52
fix: ALTER SET writes a pluggable Boolean as "true"/"false", not "yes"/"no"
fix: ALTER PAGE SET DataSource writes a Gallery's / Data grid 2's source
fix: ALTER PAGE can address a pluggable widget's slot, e.g. dataGrid1.controlbar
fix: ALTER PAGE reaches widgets inside legacy table cells
…n error handler (#1465)

The error-handler walk stopped at the first merge with more than one way
in, which is the merge closing an `if` in the handler body, so everything
after `end if;` was dropped and drop + create rerouted the handler into
the main flow. Stop instead at the first merge with a way in from outside
the handler. Loops in a handler printed only their header; emit the body.

Fixes #1456 (mendixlabs#1559).


Claude-Session: https://claude.ai/code/session_01JgXjVr9Tw5KRof2jj4JyAv

Co-authored-by: Claude <noreply@anthropic.com>
test: pin revoke create from several single-role rules (mendixlabs#1499)
fix: user tasks always written with auto-assign off; add `auto assign single user`
… events in folders

An event's container is a folder whenever it is not at the module root,
which is how Marketplace modules ship theirs. The iterator mapped the
direct container to a module, so module_name was "", qualified_name became
".Name", and IsExcluded("") let excluded modules through. Walk the folder
chain to the module, as the catalog's hierarchy does.

The test helper closed the catalog before returning it, so the module
lookup failed silently in every existing test; it now closes on cleanup.

Fixes #1452 (mendixlabs#1555)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FiK7bYZEqF75x9DeX6Eyrm
buildPermissions read entities, microflows, pages and OData services but
never nanoflows, so a stored GRANT EXECUTE ON NANOFLOW was invisible to
every lint rule. Emit element_type NANOFLOW / access_type EXECUTE rows
from the nanoflow's AllowedModuleRoles, and document the value in the
write-lint-rules skill.

Fixes #1434 (mendixlabs#1536)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FiK7bYZEqF75x9DeX6Eyrm
A statement after `return` in the same block, or after a `while true` that
has no `break`, passed check, exec wrote it, and mx check refused the flow.
New rule MDL-FLOW05 (error) refuses the first dead statement of each block
after return, raise error, break, continue, join, an if/else that ends on
both sides, or a `while true` with no break of its own. A `merge` makes the
path live again. Runs for microflows and nanoflows; stands down for an
@excluded document like the other #893 gap rules.

Measured on mxbuild 11.13.0, one microflow per shape (mx check -j for the
per-document attribution):
  log after return in an if         CE0709
  log after break / continue        CE0709
  log after raise error / return in a handler   CE0105 + CE0709
  log after join                    CE0105 + CE0709
  log after if/else returning both  CE0104 + CE0105
  log after while true (no break)   CE0104 + CE0105
  return after that loop            CE0104
  return after loop whose only break is in a nested loop   CE0104
  nanoflow, log after return in an if   CE0709
Controls, 0 errors: nothing after the loop, break then return, an if
without else that returns, merge after a return reached by a join.
Splits are not treated as ending the path (an all-returning enum split is
CE0104 but with CE0079, whose fix is the empty outcome).

Control: with the rule unwired, the test reports 0 findings on all nine
dead shapes. The cheatsheet listed CE0104 for code after a return in an if;
that shape is CE0709.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJcUHPvAr7SDBcAoGHYcPQ
The catalog had no table for MessageDefinitions$MessageDefinitionCollection
or its Mendix 11.15 replacement MessageDefinition2, so the objects view
behind documents() never held one and a naming rule on them could not
fire. Index both (message_definition_collections, message_definitions),
union them into objects as MESSAGE_DEFINITION_COLLECTION and
MESSAGE_DEFINITION, map them for bare DESCRIBE, and bump the catalog
schema to 26 so cached catalogs rebuild.

Fixes #1433 (mendixlabs#1535)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FiK7bYZEqF75x9DeX6Eyrm
…abs#1498)

`create nanoflow … export level api` parsed, passed check and exec
reported "Created nanoflow", but the document was stored with ExportLevel
"Hidden" and describe printed no clause; the same clause on a microflow was
stored as API. Nanoflows share the microflowOptions rule, so the clause
parsed, but ExitCreateNanoflowStatement read only FOLDER and EXPOSED AS.

CreateNanoflowStmt and sdk Nanoflow now carry ExportLevel (absent = carry
the stored level); nanoflowToGen writes an authored one and
carryStoredNanoflowHeader stands down for it; the reader fills it; both
describe renderers print `export level api`; the create-or-modify header
diff treats an absent clause as the stored level.

Measured on 11.13.0: create -> API; create or modify without the clause
keeps API; with `hidden` -> Hidden; describe -> exec into a fresh project
-> API; mx check 0 errors on both. genMf.VersionInfos records no
introduction for MicroflowBase.exportLevel, so it is declared at every
supported version.

Control: with the non-test changes reverted the backend test stores
"Hidden" (the reported symptom), and the exec and describe tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJcUHPvAr7SDBcAoGHYcPQ
…tion

The backend already read UserRole.Description, but the lint struct dropped
it, so a rule "every user role is documented" could not be written; the
catalog had only role_mappings, with no row per user role. Add
`description` to user_roles() and a CATALOG.USER_ROLES table (Name,
Description, IsAnonymous, ModuleRoleCount), schema 27. DESCRIBE/CREATE
USER ROLE already carry Description on main (#707).

Fixes #1442 (mendixlabs#1544)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FiK7bYZEqF75x9DeX6Eyrm
parse_xpath fed the constraint to the microflow-expression parser, which
has no entity-rooted path: `Mod.A/Mod.E/Name = 'x'` came back as just
qname Mod.A. exprcheck gains an XPath mode, used only by parse_xpath, in
which such a path is an attr_path with an empty variable and a
per-segment `predicates` list; expression mode still reports it (E014).

Rule files ran under Starlark's legacy options, which forbid recursion,
so any tree walker -- including the write-lint-rules skill's count_not
example -- failed with "function called recursively". Recursion is on
(the interpreter still stops at 100,000 frames). The example itself also
matched `not` as a call instead of a unary NOT and used `sum`, which
Starlark lacks; it is rewritten and a test now runs it from the skill.

matches() has no captures, so find_all(s, pattern) returns them, shaped
like Python's re.findall.

Fixes #1439 (mendixlabs#1541)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FiK7bYZEqF75x9DeX6Eyrm
…dule

fix: scheduled_events() empty module_name and excludeModules leak for events in folders
fix: permissions() has no rows for nanoflow EXECUTE grants
fix: user_roles() and the catalog do not expose a user role's description
Resolve the catalog schema conflict with #1442's user_roles (27): this
change becomes 28, since a cache built at 27 has no message-definition
tables and would never rebuild for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FiK7bYZEqF75x9DeX6Eyrm
Resolve the write-lint-rules skill's generated line index (#1442 added a
user_role row); regenerated with `make skill-index`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FiK7bYZEqF75x9DeX6Eyrm
ako and others added 16 commits October 17, 2026 22:26
fix: check refuses a statement that can never run (mendixlabs#1491)
fix: a nanoflow's `export level api` is written, not dropped (mendixlabs#1498)
`describe module … with all` printed each entity's access rules with the
entity, ahead of the module's associations. A grant naming an association
the entity owns was then refused on replay — "entity ModA.EntA has no
member(s) EntA_EntB; grant only names members of the entity or of an
entity it inherits from" — after `check` had passed the same script
(mendixlabs#1409, #1315).

describe module now prints entities, then associations, then every grant.
check's grant pass resolved members against the whole script and skipped
script-declared entities, so it could not see the ordering exec enforces;
it now reports a grant naming an association created further down, with
the reorder hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx8dgxQm9qE8ZsMxwRweXj
… mapping

ImportMappingCall.ContentType was always written as "Json", so re-executing
a microflow whose `import from mapping` uses a mapping built on a web
service operation failed mx check with "The selected import mapping cannot
handle content type JSON." until the activity was opened in Studio Pro
(mendixlabs#1232, #1290).

The builder derives the content type from the mapping's source (XML schema
or web service -> Xml, otherwise Json), the reader carries a stored one so
a rewrite keeps it, and REST call mapping result handling uses the same
field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx8dgxQm9qE8ZsMxwRweXj
Copying Atlas_Core.Atlas_Default with `describe layout` -> `create layout`
dropped its Forms$SidebarToggleButton, which describe printed as a
NOT re-executable comment; Atlas's slide-in navigation on phones and
tablets depends on it (mendixlabs#1220, #1285).

`sidebartoggle name (Caption, Tooltip, ButtonStyle, Icon, RenderMode)`
shares an action button's face and has no action. The written widget has
the key set of Atlas's own; modelsdk/gen bound its caption under the SDK
name `Caption`, so the storage key is overridden to `CaptionTemplate` and
struck off the key-audit ledger.

The toggle only works on a region that can be toggled: a real mx check of
the copy reported CE0628 because a scroll container region's ToggleMode
was never described or written. Regions now round-trip `ToggleMode`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx8dgxQm9qE8ZsMxwRweXj
fix: documents() never returns message definitions
fix: parse_xpath drops association paths; lint rules cannot recurse
…r fails CE6524

The reverse traversal of a Reference was always stored as a list
(MaxOccurs -1). A Reference owned by both ends is one-to-one, so from the TO
side it is a single object too; storing -1 made mx check report CE6524 "The
occurrence of '...' has changed" on CREATE, CREATE OR MODIFY and ALTER ...
ADD MEMBER.

Measured on 11.12.1: the repro gives 2x CE6524 before, 0 errors after; the
forward definition in the same script is the control (clean on both).

Fixes #1311 (mendixlabs#1386).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnpNHdpFqu1sPL7yNAQgTh
…s Studio Pro does

`create external entities from M.Client into M` named a top-level entity
after its OData entity set (`ModA.Parts`); Studio Pro names it after the
entity type (`ModA.Part`), so scripts written against that name failed with
"entity does not exist".

Re-import resolves an existing entity by service + entity set before name,
so a module imported by an older mxcli is updated in place and keeps its
entity-set name instead of growing a type-named twin mapping the same set.
The navigation-association and primitive-collection passes use the same
resolver.

Measured on 11.12.1: base reproduces "external entity not found:
Issue1432.Product"; fixed creates Product/Category, mx check 0 errors; a
base-imported project re-imported by the fix reports 0 created, 2 updated.

Fixes #1319 (mendixlabs#1432).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnpNHdpFqu1sPL7yNAQgTh
…CE6618

createNavigationAssociations made one association per navigation property,
so both ends of a bidirectional (Partner) relationship became separate
associations and mxbuild rejected each with CE6618 "Navigation '...' is not
available in the OData service".

Measured on 11.12.1, Partner is the line, not multiplicity: unpartnered
collections build clean. A partnered one-to-many pair is now ONE Reference
from the single-valued end naming both navigation properties
(BothDirections) - 0 errors. Partnered many-to-many and one-to-one pairs
build in no form (one association, two, or Owner Both), so they are skipped
and reported. A re-import names associations an earlier import made the old
way as runnable `drop association` lines; drop + re-import builds clean.

Repro: base 6x CE6618, fixed 0 errors.

Fixes #1289 (mendixlabs#1231) asks 2 and the root cause;
ask 1 (DROP ASSOCIATION) already existed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnpNHdpFqu1sPL7yNAQgTh
describe layout -> create layout dropped two things from Atlas layouts:

- Phone_/Tablet_ layouts: their Forms$Header (printed NOT re-executable),
  with the HeaderLeft / HeaderRight placeholders pages bind to (CE1613) and,
  on Phone_Sidebar / Tablet_Sidebar, the sidebar toggle. With region
  ToggleMode now copied, those two failed mx check with CE0611 "A sidebar
  toggle button is required if a region can toggle".
- Native layouts: the header placeholder, ShowBottomBar and the sidebar,
  dropped with no note; the content was written with 4 of the 8 keys
  Studio Pro stores.

`layoutheader name { region left { … } region right { … } }` writes the
header. A native layout takes `showbottombar: true`, `region header
{ placeholder X }` and `region sidebar { … }`.

Measured on a new 11.13.0 app: all 22 Atlas_Core layouts copied, with
pages bound to the copied header and sidebar placeholders, build with 0
errors; main's copy of NativePhone_SideMenu fails CE1613 twice. The CE0535
earlier blamed on the copy is the PedApp fixture's own, not a copy defect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx8dgxQm9qE8ZsMxwRweXj
…p6ini-1311

fix: a one-to-one association member in a message definition no longer fails CE6524
…p6ini

fix: create external entities names entities after the entity type, as Studio Pro does
…p6ini-1289

fix: importing partnered OData navigation properties no longer fails CE6618
…-thcbcr

fix: describe round-trip gaps — grant order, import content type, Atlas layout copies
@ako
ako merged commit cbc51de into mendixlabs:main Oct 18, 2026
15 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