Skip to content

https://github.com/mendixlabs/mxcli/compare/main...ako:mxcli:main?expand=1 - #878

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

https://github.com/mendixlabs/mxcli/compare/main...ako:mxcli:main?expand=1#878
ako merged 44 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Seven commits since the last sync, in three groups.

Task queues and scheduled events (new MDL document types)

Neither was authorable in MDL. Task queues turned out to be hiding a data-loss
bug, and scheduled events were being read back lossily by both engines.

  • CREATE [OR MODIFY] / DROP / SHOW / DESCRIBE QUEUE. BSON pinned against
    the four Studio Pro-authored queues in Business Events 3.12.1, which agree
    exactly: Config.ParallelismExpression is a string, and the sibling int32
    Parallelism is absent in all four, so it is not written. Parallelism is an
    expression everywhere in MDL, not a number.

  • Rewriting a microflow with a queued call is now refused, naming the queues
    that would be lost. Previously create or replace microflow wrote
    QueueSettings back as null and the binding was gone — silently. Worse,
    mx check went from CE1613 "The selected task queue no longer exists" to
    0 errors, because the configuration the error was about had been deleted;
    describe never showed the binding either, so the loss was invisible from
    every angle. Authoring a queued call is deliberately still unimplemented —
    the Retry shape has no Studio Pro-authored sample to diff against, and
    guessing it would put a second unverified shape into user projects
    (guard-don't-drop, ADR-0005).

  • CREATE [OR MODIFY] / DROP / SHOW / DESCRIBE SCHEDULED EVENT. Repeat:
    names one of the eight ScheduledEvents$*Schedule variants, and only that
    variant's fields are accepted — a Multiplier on a Daily repeat is refused
    by mxcli check (MDL-SCHED01) and by exec, which call the same function so
    the two cannot drift. Merging the field sets is what produces a document
    mxbuild accepts and Studio Pro cannot open.

  • modelsdk/gen is wrong about two properties here, so both engines share
    one raw-BSON codec (mdl/scheduledevents): the integers are stored as int64
    (gen says int32 — the same mismatch as Other parsers share #583's narrow-int32 assertion: silently zeroes numeric fields when Studio Pro writes them as int64 #585) and StartDateTime is a BSON
    datetime (gen says string). The codec is pinned by re-serializing three whole
    Studio Pro-authored documents — Workflow Commons 4.11.0, OIDC SSO 4.6.0 and
    SAML 4.2.1 — element by element, in order, with matching BSON types. Those
    cover the Day and Hour variants; the other six are metamodel-derived and
    verified to load with 0 errors.

  • Interval/IntervalType are legacy siblings of Schedule that Studio Pro
    writes and does not keep in sync (Workflow Commons stores 0/"Minute"
    beside a DaySchedule of 01:00). They have no MDL syntax: derived on CREATE,
    carried through untouched on MODIFY.

Verified on Mendix 11.13 under both engines: all eight variants leave a clean
project at 0 errors, describe output re-parses and re-validates for every
variant, and a re-run writes nothing — the MXCLI_ALWAYS_WRITE=1 control
changes 9 units where the normal run changes 0.

Catalog and linter: scheduled microflows were reported as dead

A scheduled event produced no edge in the reference graph, so a microflow run
only by one was reported unused from three directions at once:

show callers of Ops.SE_Cleanup   -> (no callers found)
CATALOG.GRAPH_DEAD_ASSETS        -> Ops.SE_Cleanup
mxcli lint                       -> [QUAL004] Microflow 'SE_Cleanup' is not
                                    called from anywhere.  -> Remove if unused
  • New CATALOG.SCHEDULED_EVENTS and CATALOG.QUEUES, registered in Tables().
  • A schedule edge from each event to its microflow, added to callerRefKinds,
    graphRefKinds and the QUAL004 rule — three consumers, none of which shares
    the others' list.
  • The linter's interval_seconds now derives from the Schedule child instead
    of the stale legacy pair, so a "fires too often" rule no longer reads a
    nightly job as every 0 seconds. Rules can also branch on repeat /
    on_overlap / time_zone, and iterate queues().

Also fixes a regression introduced by the queue work earlier in this series:
QUEUES became a lexer keyword, so select * from CATALOG.QUEUES parsed to
nothing — no error, no output — until QUEUES was added to catalogTableName,
the same trap COMMUNITIES hit before.

Independent fixes

  • show callers hid widget-action references. Page/widget action edges
    were not indexed, so a microflow called only from a button reported no
    callers.
  • mxcli new --output-dir with a deep path. The project is created in a
    staging directory first, so a nested output path works.
  • Docs workflow. GitHub Pages publish is skipped where no Pages site exists,
    so the workflow stops failing on forks.

Verification

go build ./..., full go test ./..., make check-mdl and make lint-go are
green on this tree. The two load-bearing fixes have negative controls: reverting
the interval derivation fails the schedule tests, and removing the reference edge
takes QUAL004 back from 1 to 2 on the test project.

claude and others added 30 commits August 11, 2026 13:48
`describe Module.Name` reported "no describable document named ..." for
building blocks and icon collections, even though `describe building block
Module.Name` and `describe icon collection Module.Name` both worked. Two
lists in different packages had drifted apart: bare DESCRIBE resolves a type
by looking the qualified name up in the catalog `objects` view and mapping
its ObjectType through `objectTypeToDescribeKind`.

- Building blocks were built into `building_blocks_data` but never joined
  into the `objects` view union, so the lookup found no row.
- Icon collections had no catalog table at all, which left the existing
  `ICON_COLLECTION` entry in the map as dead code — nothing could emit it.

Measured against a project carrying seven real marketplace modules: 43 of
251 documents (40 building blocks, 3 icon collections) were unreachable this
way. Auto-detect coverage goes from 204/251 (81%) to 247/251 (98%), verified
end-to-end over the same documents with no new name ambiguity. The remaining
4 are separate defects: import/export mapping describe errors, and menu
documents have no DESCRIBE at all.

The denominator comes from raw MPR unit types rather than the catalog, which
indexes only describable types and so reports full coverage by construction.

`TestDescribeAutoCoversCatalogObjectTypes` guards the drift: it reads the
ObjectType literals out of the objects view's own SQL and fails when one has
neither a describe kind nor an entry in an explicit exemption list.

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

`describe import mapping FeedbackModule.IMM_PostResponse` reported "not
found" for a mapping that exists and that `show import mappings` lists.

`moduleNameFor` resolved a unit's module by reading its direct container
(`GetModule(u.ContainerID)`). Studio Pro nests documents in folders, and
folders in folders, so for a foldered document that container is a
`Projects$Folder`: GetModule returns nil, the module name resolves to "",
and every `...ByQualifiedName` lookup built on it misses.

This is the normal case in a real project rather than an edge case. In the
seven marketplace modules of the test fixture, 15 of 16 microflows, both
JSON structures and both mappings live in folders, while all 8 domain models
sit directly under their module — which is why the three domain-model
callers worked and hid the defect.

The fix walks the containment chain to the first enclosing module. Behaviour
is unchanged for direct children, so the domain-model callers are
unaffected. The parent map is now built once rather than re-scanning
ListUnits() on every call from inside a loop over every document, and the
walk is guarded against the project root, which is its own container.

This also repairs `GetJsonStructureByQualifiedName`, which backs the CREATE
OR MODIFY existence check and DROP for JSON structures — a lookup that
answers "no" turns a modify into an attempted create.

Bare-DESCRIBE coverage over the fixture's 251 documents goes from 247 to
249; the 2 remaining are menu documents, which have no DESCRIBE support at
all. The legacy sdk/mpr reader already walked the chain correctly, so the
two engines disagreed and only the default one was affected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Menu documents (Menus$MenuDocument) were the last document type in the test
fixture's seven marketplace modules with no DESCRIBE support at all — no
grammar, no handler, no reader. Atlas_Core ships two of them, Phone_Menu and
Tablet_Menu, and a menu widget points at one.

A menu document is not the menu inside a navigation profile, though the two
are built from the same Menus$MenuItem elements. That shared shape is what
this reuses rather than duplicates:

- Items are modelled as types.NavMenuItem, not a parallel item type.
- The modelsdk backend converts them with navMenuItemFromGen, which already
  handles the three icon variants and the client-action dispatch; the legacy
  reader reuses parseNavMenuItem the same way. The only structural difference
  is the Menus$MenuItemCollection wrapper.
- Output is rendered by printMenuMDL, the renderer DESCRIBE NAVIGATION uses,
  so menu items read identically in both places.

Menus are read-only: Mendix offers no way to author one outside Studio Pro,
so there is no CREATE/ALTER/DROP and no round-trip. The header says so, as
DESCRIBE BUILDING BLOCK does. printMenuMDL's icon note now takes the
construct it should name, because "not reproducible by CREATE NAVIGATION"
would be wrong for a document CREATE NAVIGATION cannot author; navigation
still passes that exact string, so its documented output is unchanged.

Menus are also indexed in the catalog objects view, so bare DESCRIBE
Module.Name resolves them. That takes the fixture's bare-DESCRIBE coverage
to 251/251 documents, from 204 at the start of this branch.

SHOW MENUS is deliberately not added: it would need a new MENUS keyword,
and a new keyword shadows any identifier of that name (#94). Listing is
already available through the catalog and SHOW STRUCTURE.

The fixture's menus are flat, so nesting, page/microflow targets and the
non-round-trippable icon note are covered by a constructed menu in the
executor tests rather than left unexercised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Menus were shipped read-only an hour ago on the claim that Mendix offers no
way to author a menu document outside Studio Pro. That was wrong — Studio Pro
creates them via Add > Menu. The gap was mxcli's missing writer, not a
platform limitation, so this adds the writer and corrects the claim wherever
it was written down.

Syntax reuses the existing navMenuItemDef grammar rule, the same one CREATE
NAVIGATION's MENU block uses, so a menu item is written identically wherever
it appears and DESCRIBE output feeds straight back into CREATE:

  create or modify menu MyModule.Main_Menu (
    menu item 'Home' page MyModule.Home_Web icon Atlas_Core.Atlas.home;
    menu 'Admin' (
      menu item 'Accounts' page Administration.Account_Overview;
    );
  );

DESCRIBE MENU now emits that statement rather than an informational block, so
describe -> exec -> describe is a fixed point (verified byte-identical).

The writer goes through gen + codec rather than hand-built BSON, and that is
load-bearing: Studio Pro's menu documents carry typed-array marker 3 on both
the item collection and each item's sub-items, which is the codec's default.
The navigation writers build menu items by hand with marker 1. Whether that
is a latent navigation bug or a genuine difference is unverified — this
fixture stores no navigation menu items — so navigation is left untouched
rather than changed on a guess.

OR MODIFY replaces the item list wholesale, like CREATE NAVIGATION, while
preserving the document's ID, container and export level so menu widgets
pointing at it keep working.

Authoring is modelsdk-only; the legacy engine refuses rather than writing a
differently-shaped document.

Verified against mxbuild 11.10.0 on an 11.6.6 project: create, modify and
drop each leave the app at 0 errors, with a clean-fixture baseline as the
control. An earlier run pointed a menu item at a page with a required
parameter and mxbuild reported CE1571 at "Menu item" — proof the check
actually inspects what was written rather than passing vacuously. That
gotcha is documented in the example and syntax help.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Answering "did the docs get done": examples, syntax help and the quick
reference did; the skill, the docs-site page and the standalone `mxcli
describe` command did not. The last of those was a functional gap, not a
documentation one.

`mxcli describe` is a separate Cobra command with its own type list and its
own $Type/ObjectType maps, independent of the executor's DESCRIBE. Menus were
in neither, so `mxcli describe menu Atlas_Core.Phone_Menu` failed with
"Unknown type: menu" and the bare `mxcli describe Atlas_Core.Phone_Menu`
failed to resolve, even though both worked from the REPL and from exec. Both
paths now work.

The syntax topic moves from `menu` to `navigation.menu-document`. At the top
level it resolved but never appeared in any listing, because the top-level
index is a curated list; under `navigation` it shows up next to the profile
menu it is most often confused with.

The skill section goes in manage-navigation.md, which already covers profile
menus, and leads with how to tell the two apart — `show navigation menu` for
the menu inside a profile, `describe menu` for a standalone document. It
carries the CE1571 parameterised-page trap and the icon round-trip limit,
both of which cost a debugging cycle to find.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
§7 recorded module security as invisible to a describe-based comparison and
ranked it the largest remaining risk to the marketplace differ. Re-measuring
shows that was wrong, and wrong in a way worth writing down.

Three of the four parts of module security were already emitted: entity
access rules by DESCRIBE ENTITY, page access by DESCRIBE PAGE, and microflow
access by DESCRIBE MICROFLOW. Only the module's role list was missing —
it lives in the module's own Security$ModuleSecurity unit and belongs to no
document, so no document describe could ever reach it. DESCRIBE MODULE now
emits it.

The original error came from grepping describe output for "role|access|
allowed", a pattern that cannot match the line it was looking for:

  grant view on page Administration.Account_Overview to Administration.Administrator;

Nothing in that statement contains any of those words. The search returned
nothing and the absence was read as a missing feature rather than a bad
pattern. §7 now records that, since the same mistake is easy to repeat.

Roles are sorted before emission: the reader's order is not guaranteed, and
an unsorted list would show up as a phantom change in exactly the comparison
this exists to support. A backend that cannot read module security degrades
to the old output rather than failing the describe.

With this and the two fixes before it, all of §7's Phase 1 prerequisites are
closed except the page/template conflation, which does not mislead the
differ. Phase 1 is unblocked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
CI has been red since afc857a on `make test-integration`, a target I never
ran locally — `make test`, `check-mdl` and `lint` were all green, which is
why this shipped.

TestMxCheck_DoctypeScripts executes every doctype-tests/*.mdl in a **fresh
blank project** and validates the result with `mx check`. When menus became
authorable the example changed from describe-only to CREATE statements, and
it referenced documents a blank project does not have — Administration.* is a
marketplace module, and MyFirstModule.MyFirstLogic exists only in the
vendored fixture I had been testing against by hand.

The example now creates its own module, pages and microflow, matching how
every other doctype script is written, and points its menu items at those.

It also runs under both engines, and menu authoring is modelsdk-only by
design: the legacy backend refuses it rather than writing item lists with
typed-array marker 1 where Studio Pro stores 3. That refusal cannot pass the
gate, so the script is registered in engineScriptSkip for legacy with the
reason — the same mechanism the SOAP and chart-template splits use.

Verified by running the failing test rather than inferring: the menu script
now passes on modelsdk with 0 errors from mx check, and skips on legacy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`make lint` rewrites these on every run, so they show up as uncommitted
changes in every working tree and had to be reverted out of each commit in
this branch to keep the diffs to one thing each. Committing the formatting
once stops that recurring.

Mechanical only. The visible half is alignment and import ordering; the rest
is gofmt splitting semicolon-packed one-liners in the enginecompare tests
onto separate lines, e.g.

  lp := copyProject(t); if _,err:=Run(Legacy,lp,s);err!=nil{...}

becomes the same statements one per line. No statement is added, removed or
reordered. Verified with `gofmt -l` (clean afterwards), `go build ./...`,
`go vet`, and the full unit suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The part of `marketplace diff` that carries the design risk: capture what a
module looks like, and decide whether two captures differ.

Comparison is on DESCRIBE output rather than BSON, per the proposal's §3
measurement — an *untouched* module differs from its own published package in
~15,000 BSON paths, because the installed copy carries whole subtrees the
package does not. DESCRIBE discards exactly those artefacts.

Elements are enumerated from the catalog `objects` view and described through
executor.DescribeKindFor, newly exported so this resolves types through the
same table bare DESCRIBE uses. Three copies of that mapping already exist and
each has drifted once; a fourth was not worth the convenience.

The honesty rule is the load-bearing behaviour and has its own tests: an
element that cannot be described on either side is Unknown, never Unchanged,
and a report containing one is not Clean. "We could not tell" is deliberately
a different answer from "nothing changed" — collapsing them would clear a
module for a destructive upgrade on the strength of a gap in coverage.

Verified against the vendored 7-module marketplace fixture, not just
synthetic snapshots:

- Administration snapshots 21 of 21 elements, all describable.
- Two snapshots of the same module compare clean. This is the control the
  design rests on: if DESCRIBE output varied run to run, every result would
  be noise. Two separate copies are used, so a path dependency would fail it.
- A real edit (`alter entity Administration.Account add attribute ...`) is
  reported as exactly one modified element, ENTITY Account, and nothing else.
  Both halves matter — missing it is unsafe, and false positives make the
  report ignorable.

The backend factory is injected rather than chosen here: the engine is a
global CLI concern, and comparing both sides with the same engine is what
makes the result meaningful.

Slices 2 (scratch project from a downloaded .mpk) and 3 (the CLI command)
follow; the marketplace API is reachable from this environment, verified
against DataWidgets 116540.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Resolves the version a module was installed from, creates a project at the
consuming project's Mendix version, and imports the published .mpk into it —
the baseline slice 1 compares against.

Three things were found by running it rather than reasoning about it, and each
changes the code:

**A blank Mendix project is not empty.** The create-project template already
ships Administration, Atlas_Core and friends, so `mx module-import` refuses
with "Module 'Administration' already exist in the app" (exit 47) — for
precisely the module the field report cares about. The template's copy is now
removed first, through mxcli's own DROP MODULE rather than a file edit,
because that also unpicks the references the template set up ("Removed
Administration.User from 1 user role(s)"); leaving those dangling would hand
module-import an inconsistent model.

**ResolveMxForVersion silently falls back to any cached mxbuild.** Asking for
11.6.6 on a machine holding 11.12.1 returns 11.12.1 without complaint, and
create-project stamps the project with whatever binary ran it. The reference
would be built Mendix versions away from the project under comparison, and
every platform migration between them would read as a user edit — false
findings indistinguishable from real ones. The stamped version is now read
back from the created project and a mismatch is refused, not warned about,
with the setup command in the message.

**mx create-project dies with PathTooLongException under a deep path.** It
extracts its template with .NET path handling; t.TempDir() nested in a long
module path breaks it where /tmp/xxxx works. Documented on the parameter, and
the tests use a short dir.

Also recorded: a .mpk's package.xml carries no version, so `mx module-import`
stamps the module's *internal* version, not the marketplace release —
importing Administration 4.3.2 records 2.0.1. That stamp never reaches
DESCRIBE output so the comparison is unaffected, but it means the reference
project's recorded version is not evidence of which package was imported.

Verified end to end against real marketplace content (Administration 4.3.2,
content 23513, at Mendix 11.12.1): 21 elements captured from the package, two
independently built references compare clean — so the baseline is
reproducible and a diff against it carries no noise — and a single added
attribute is reported as exactly `ENTITY Account` modified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
/v1/content/{id}/versions returns 10 items when given no paging
parameters and caps `limit` at 20, but Client.Versions asked once and
trusted the answer. Every content item therefore appeared to have exactly
ten versions, and looking up an older one reported it as not published --
Data Widgets has 131 published releases and mxcli could see 10.

Walk pages until one comes back short. `marketplace versions`,
`download` and `install` all resolve older versions correctly now.

The cap here is not the /v1/content cap (100), so the existing pageSize
constant could not be reused; the test's mock clamps `limit` the way the
server does, and fails a client that makes a single request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Reports which elements of an installed marketplace module have been
edited locally -- the question Studio Pro's Marketplace update never asks
before replacing the module and discarding those edits.

    mxcli marketplace diff 23513 -p app.mpr [--to 4.5.0] [--json]

--to adds what an upgrade would touch and which of those collide with
local edits. --json is the CI form; read both locallyModified and
verified, because an element that could not be described is reported
unknown, never unchanged.

Identifying the module: each installed module records AppStoreGuid, and
that GUID is the marketplace *version* UUID, so the module and the exact
release it came from both fall out of the project with no network call.
Matching on the version *number* instead looked equivalent and is not --
a blank project has Atlas_Web_Content at 4.1.0 and Administration's
content has also published a 4.1.0, so a number match selects two modules
and cannot tell them apart. The first real run of the command refused on
exactly that, which is how it was found.

Measured against real marketplace content, not a fixture: Administration
4.3.2 in a blank 11.12.1 app reports 21 of 21 elements verified
unchanged; one added attribute reports exactly ENTITY Account; --to 4.5.0
reports five elements touched by the author with one conflict, and the
control for that (--to 4.3.2, the version already installed) reports
nothing touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
listUnitsByType matched on a type prefix, and Mendix storage names nest:
`Forms$Page` is a prefix of `Forms$PageTemplate`. ListPages therefore
returned both, and `show modules` reported Atlas_Web_Content as having 46
pages when it has none. The modelsdk backend had bolted templates onto
its own ListPages deliberately, to match, so both engines agreed on the
wrong answer.

The miscount was the visible half. A page template also describes with an
empty body -- its widgets hang off LayoutCall and the page describe path
reads FormCall -- so every template compared on nothing but its name,
folder and CSS class. `marketplace diff` was reporting 46 elements
unchanged without having read them, which is the false negative its
honesty rule exists to prevent, hiding behind a bug filed as cosmetic.

Both engines now match the storage type exactly. Templates keep their own
catalog type (CATALOG.PAGE_TEMPLATES, PAGE_TEMPLATE in the objects view)
rather than being dropped from the index, because 46 documents nothing
can enumerate is worse than 46 filed wrong. Having no DESCRIBE handler,
they now report as unknown -- the truthful version of what the differ was
already doing.

No DESCRIBE PAGE TEMPLATE handler yet; that is a separate feature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`mx module-import` silently converts a v2 project to v1. Measured on a
blank Mendix 11.12.1 app, one import turned a 69,632-byte .mpr plus 341
.mxunit files into a single 14,295,040-byte SQLite blob with no
mprcontents/ and no _Transaction table. Reproduced independently on
11.13.0 by the mxcli-formula1 investigation (FINDINGS §53), so it is not
version-specific.

`marketplace install` shelled straight out to it against the user's own
project. The v2 layout is what makes the model diffable and mergeable per
document: it is what `mxcli diff-local` reads, and what makes an
idempotent re-run observable as "no files changed" (ADR-0008). The
conversion is one-way -- `mx convert` targets Mendix versions, not
storage formats -- so the loss is permanent and lands on a real
repository.

Refuse rather than warn, with --allow-format-change for a project that is
not kept in git; the opt-in path then states plainly that the project is
now v1.

The reference-project import in `marketplace diff` is deliberately left
unguarded: it writes a throwaway in a temp directory, and a v1 reference
still diffs exactly against a v2 project because DESCRIBE output does not
depend on how the model is stored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`mx module-import` refuses a theme module outright ("Importing theme
module is not supported", exit 112), which took Atlas_Core,
Atlas_Web_Content and Conversational UI off the table for `marketplace
diff`. The refusal is gated on a single BSON boolean on the module
document inside the package (Projects$ModuleImpl -> IsThemeModule);
clearing it and importing the otherwise identical package, theme files
included, imports and checks cleanly -- reported in mxcli-formula1
FINDINGS §53 and reproduced here on 11.12.1.

`diff` now clears the flag on its own copy of the package before
importing. That is sound only because the copy is imported into a
throwaway reference project, described once and deleted, so the flag's
real meaning never comes into play; it would not be sound in a path
writing to a user's project.

Atlas_Web_Content 4.1.0 in a blank 11.12.1 app now reports 43 of 89
elements verified and the other 46 -- its page templates, which have no
DESCRIBE handler -- as unknown rather than as unchanged. That is the
first end-to-end demonstration of the honesty rule on real content.

One trap worth the comment it now carries: the package's v1 project.mpr
must be unpacked into a directory of its own. The MPR format test is
adjacency -- an .mpr beside an mprcontents/ directory is read as v2 -- so
extracting it into the work directory that already holds the scratch
project made every unit resolve against the wrong model, and the flag
flip silently reached nothing.

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

`mxcli new` pointed `mx create-project` straight at the output directory.
MxToolset refuses any full destination path over 259 characters — its own
Windows-compatibility limit, not the filesystem's — and aborts extraction PART
WAY THROUGH when it hits that, so a deep output directory was left holding a few
hundred files and no .mpr: output that looks like a project until you open it.

Bisecting the output-directory length on mxbuild 11.13.0 pins the rule exactly:

  77 characters -> project created, .mpr present
  78 characters -> PathTooLongException, 259 files left behind, no .mpr

The blank template's longest relative path is 181 characters there, and
77 + 1 + 181 = 259. The reporter measured 182 on 11.12.0 and saw 258 files, so
the constant drifts a character per release.

Rather than validate a path we can avoid, create the project in a short staging
directory and move it into place. Two things follow:

  * A deep destination WORKS — the limit is MxToolset's, and POSIX allows 4096.
    Relocation is safe because a fresh project embeds no absolute paths: grepping
    a blank 11.13 project for its own directory matches zero files.
  * The destination is never partially populated. Nothing is written there until
    creation has already succeeded, so a failure leaves the user's directory
    exactly as it was.

The template's longest path is measured from the staged tree rather than
hardcoded, and used for a warning — not a refusal — when the finished project
exceeds 259: it works here, but Studio Pro on Windows may not open it, and
refusing would block a machine where it is fine. The warning carries both
numbers and the budget, so nobody has to derive the limit themselves the way the
reporter did.

Verified against mxbuild 11.13.0 at the length that used to fail:

  build     exit  result
  pre-fix   1     PathTooLongException, 259 orphaned files, 0 .mpr
  fixed     0     project created, warning emitted, `mx check` 0 errors

Cross-device staging falls back to os.CopyFS, which os.Rename cannot do.

Fixes mendixlabs#825
Every push to main on this fork left a red X on "Deploy Documentation":

    Failed to create deployment (status: 404) ... Ensure GitHub Pages has
    been enabled: https://github.com/ako/mxcli/settings/pages

A fork inherits the workflow but not the upstream Pages configuration, so
actions/deploy-pages 404s on something unrelated to the change being
pushed. The API confirms both halves for this repo: fork=true and
has_pages=false. It has failed on all three runs since the fork was
active, while Build/Test/Lint is 23/23 green — a permanently red workflow
is how a real failure gets missed.

The upload and deploy steps are now gated on the repository not being a
fork, with an opt-out for a fork that does want to publish its own copy
(enable Pages, then set the repository variable DEPLOY_DOCS=true).

The book is still BUILT everywhere — on forks and on pull requests, as
before — so a docs change is still proven to compile wherever it is
pushed. Only publishing is skipped, and a skipped job does not fail the
run.

Nothing changes upstream: mendixlabs/mxcli is not a fork, so the condition
is true there exactly as it is today. This is worth sending upstream on its
own merits, since any fork of the repo currently hits the same 404.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUToAkUx54bNkNjsBpufRH
Open question #5 asked whether GUID carries the database mapping. It was
inference: §4 showed Studio Pro's update renumbers every $ID and
preserves every GUID, which made GUID the only candidate, but nothing had
been measured against a real database.

Measured now on Mendix 11.12.1 against a live PostgreSQL. The runtime
writes its own identity map, and it holds the model's GUIDs verbatim:
mendixsystem$entity.id for Administration.Account is
b16e49ea-91df-4caa-aed8-6ba4c4e133c5, which is the entity's stored GUID
bytes with the .NET field order undone. mendixsystem$attribute.id matches
each attribute's GUID the same way.

Changing ONLY the GUID -- same entity name, same table name, same
attributes -- makes the runtime treat it as a different entity and
destroy its rows. An unchanged reboot is the control and preserves them.

So Studio Pro's update preserves exactly the identity the database keys
on: $ID renumbering is irrelevant to data safety, and a GUID-preserving
replace is data-safe at the level of element identity. That is the gate
on phase 2, and it is now a measurement rather than an argument.

What it does not establish is recorded with it: an element the new
version deletes still loses its column, the DDL text was not read (the
runtime logs counts, not statements, at INFO), and associations were not
exercised.

Adds the invariant to CLAUDE.md, because it binds every write path: a
codec that mints a fresh GUID for an existing element would silently drop
a table's worth of production data on the next deploy, and no mx check or
build would catch it.

Method and results: docs/11-proposals/data/marketplace-upgrade/GUID_IDENTITY.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Two gaps were reported together in mxcli-formula1 FINDINGS §53. Only one
of them was real.

**The version gap was real.** `show features` listed nothing for agents
or MCP, because sdk/versions/mendix-11.yaml had no agent_documents area,
so checkFeature() had nothing to consult and a pre-11.9 project got no
error at all. This is the one version gate with no downstream safety net:
Agent Editor documents are stored as custom blobs and mxbuild contains no
agent-editor strings whatsoever, so an ungated project builds green and
fails only when Studio Pro tries to open the document. Adds the four
doctypes at 11.9.0 and gates all four create handlers.

**The provider claim was not.** The report said CREATE MODEL can author
only MxCloudGenAI because the writer assigns it unconditionally. The
assignment is `if m.Provider == ""` -- a default, not an override.
Measured: `Provider: OpenAI` parses, writes and round-trips through
`describe model`, and so does `Provider: TotallyMadeUp`.

So the real defect there is that nothing validates the value, and no
allowlist is added: the enum lives in a Studio Pro extension, not in
generated/metamodel and not in mxbuild, so a guessed list would reject
values Mendix accepts. The skills now say plainly that Provider is
free-form, that nothing checks it, and to take the value from a document
Studio Pro created.

Also fixes a latent nil-deref the new gates exposed: checkFeature
dereferenced ProjectVersion() without a nil check, so any gated handler
panicked under a backend that cannot report a version. It now skips,
matching what it already does when the project is not connected or the
registry will not load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…ow callers` hiding them

`show callers` and `show references` missed anything reached only from a page
action button, reporting "(no callers found)" — a false negative that reads as
"safe to delete".

Two independent defects behind one symptom, and fixing either alone closes half
the issue.

1. The page reference was never recorded. scanWidgetOwnRefs collects a widget's
   Entity, Microflow and Nanoflow out of raw BSON but not `Form`, which is where
   a PAGE reference lives — "Form" being Mendix's original word for a page, the
   same rename behind ShowFormAction/CloseFormAction. So widgets_data had no page
   column and the refs projection had no page row. `create object … then open
   page` is one action carrying TWO references, and only the entity survived.

2. The microflow reference was recorded and then hidden by the query.
   execShowCallers filtered RefKind = 'call', the kind a microflow CALL ACTIVITY
   produces; a button's microflow is 'action', and that row was sitting in the
   refs table all along.

widgets_data gains a PageRef column, the projection a PAGE/show_page row, and
`show callers` an explicit set of invocation kinds — call, action, show_page,
calculate, and the three navigation kinds.

`show callers` stays narrower than `show references to`: datasource, parameter,
return, retrieve, create, change, delete, associate, generalize and layout are
uses of a TYPE or a LAYOUT, not invocations, and folding them in would make the
two commands synonyms. Both the included and the excluded set are pinned by test.

Verified on a project reproducing the reporter's two scenarios:

  before: (no callers found) for both the button-called microflow and the
          button-opened page
  after:  the page is a caller of its button's microflow; the overview page is a
          caller of the page its "create object … then open page" button opens;
          transitive finds the overview at depth 2

The microflow→microflow control still resolves, and `show callers of <entity>`
is still empty while `show references to <entity>` shows the datasource use.

Fixes mendixlabs#773
The load-bearing safety mechanism of a module update. §8 measured that
the runtime keys entities and attributes on the model's GUID, so a
replace that does not carry every existing GUID onto its replacement
destroys that module's data on the next deploy -- silently, with a valid
model and a green build.

CaptureIdentities walks a module's units and records every element that
carries a GUID, keyed by its path of names (Account, Account/FullName)
rather than by name alone: two entities each having a Name attribute is
ordinary, and a name-keyed map would transplant one entity's identity
onto another's column. The path is also what survives an update, since a
replace renumbers every $ID and keeps every name.

The walk is type-agnostic -- any node carrying both Name and GUID is
recorded, wherever it sits -- because the set of GUID-carrying types is
not enumerable from the metamodel. It resolves module membership by
walking the containment chain, not by reading one level, which is the
defect that made foldered documents invisible to DESCRIBE (mendixlabs#759).

Corroboration: the fixture's Administration yields 9 identities (2
entities, 6 attributes, 1 association), the same count §4 measured Studio
Pro transplanting on a different project at a different Mendix version.
The test asserts Account's captured GUID is byte-identical to the
mendixsystem$entity.id read from the live database in §8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The other half of the pair. ApplyIdentities writes recorded GUIDs back
onto a module's elements, matched by the path capture recorded them
under, so that after a module's documents are replaced every element that
existed before still carries the identity the database keys on (§8).

Three behaviours are deliberate and tested:

- An element with no recorded identity keeps its freshly minted GUID. It
  is new in the target version, and inheriting an old one would make two
  elements the same entity as far as the runtime is concerned.
- A recorded path that no longer exists is reported as missing rather
  than swallowed. That is an element the new version removed, and its
  column is about to disappear -- a data decision the caller has to make,
  not something this function should absorb.
- Only units that actually changed are written, so a module whose
  identities already match is not rewritten (ADR-0008).

The round-trip test scrambles every GUID before restoring them. Without
that step it would pass whether or not the code did anything, since the
identities already matched.

Slice 3 is the unit copy itself: reading a module's units out of a
package and writing them into the target with mxcli's own writer, which
is what keeps the project in MPR v2 (option A).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
ci(docs): only publish Pages where a Pages site exists
fix: deep --output-dir for mxcli new (mendixlabs#825), and reference indexing for page action buttons (mendixlabs#773)
The import step, done with mxcli's own writer rather than
`mx module-import`. That command rewrites an MPR v2 project as v1 --
measured, and refused outright by `marketplace install` -- and it also
refuses theme modules. Copying the units directly avoids both and keeps
the destination in whatever format it already uses, since the writer
handles v1 and v2 alike.

Units are copied verbatim, unit IDs included. That is sound because the
destination's copy of the module is removed first, so the IDs are free,
and because no element $ID pointer crosses a unit boundary (§4) -- so a
unit is either copied whole or not at all, never rewritten, which is what
ADR-0008 requires. Only the module unit is re-parented, onto the
destination's project unit under the "Modules" containment every module
uses.

Verified where it counts. A round trip through mxcli's own reader proves
only that mxcli agrees with itself; the failure to rule out is a model
that reads back fine and that mxbuild rejects. Measured on 11.12.1 with
two blank projects: 28 units copied, and `mx check` reports "The app
contains: 0 errors." That run is now an integration test.

Transplant deliberately does not preserve identities -- the copied module
carries the package's GUIDs, and the caller pairs it with
CaptureIdentities before and ApplyIdentities after. Keeping the two
separate is what lets the identity step be tested against a scrambled
source, where a no-op would otherwise look like success.

Still open before this is a command: project-level references (a user
role's grant of a module role) are unpicked by DROP MODULE and not
restored by the copy, and the conflict case -- what to do when diff says
the user edited an element the update replaces -- is undecided.

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

A microflow call bound to a task queue in Studio Pro lost that binding on
`create or replace microflow` — silently. Worse, `mx check` went from

  [CE1613] "The selected task queue 'X' no longer exists."

to 0 errors, because mxcli had deleted the configuration the error was about.
`describe microflow` never showed the binding either, so the loss was invisible
from every angle: the write reported success, the describe looked complete, and
the build looked healthier than before.

Two defects behind one report, fixed here:

1. MDL had no queue surface at all, so a script could not restate a binding even
   in principle. CREATE [OR MODIFY] / DROP / SHOW / DESCRIBE QUEUE now exist,
   wired through grammar → AST → visitor → executor → backend on both engines.

   The BSON is pinned against the four Studio Pro-authored queues in Mendix
   Business Events 3.12.1, which agree exactly: Config is a
   Queues$BasicQueueConfig whose ParallelismExpression is a STRING, with the
   sibling int32 `Parallelism` absent in all four. Parallelism is therefore an
   expression everywhere in MDL, not a number.

2. Rewriting a microflow whose stored calls are queued is now refused, naming
   the queues that would be lost. Binding a call to a queue is deliberately
   still unimplemented — the Retry shape has no Studio Pro-authored sample to
   diff against, and guessing it would put a second unverified shape into user
   projects. Refusing is the only option that does not lose data (ADR-0005).

QUEUE/QUEUES are added to the `keyword` rule, so `queue` remains usable as an
ordinary attribute name.

Verified on Mendix 11.13: the example script leaves a clean project at 0 errors
under both engines, and describe → exec round-trips. The guard's test fails (the
rewrite runs, the binding goes) when its call site is disabled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUToAkUx54bNkNjsBpufRH
…at it stores

Scheduled events were read-only: ScheduledEventBackend had List/Get and nothing
else, and there was no MDL surface at all. A project's cron had to be added in
Studio Pro even though everything around it could be scripted.

CREATE [OR MODIFY] / DROP / SHOW / DESCRIBE SCHEDULED EVENT now exist, wired
through grammar → AST → visitor → executor → backend on both engines.

The repeat rule is stored as one of eight ScheduledEvents$*Schedule types that
differ in WHICH fields they carry, not just in their values. MDL mirrors that:
`Repeat: Daily` names the variant and then only that variant's fields are
accepted — a Multiplier on a Daily repeat is refused, by `mxcli check`
(MDL-SCHED01) and by exec, which call the same function so they cannot drift.
Merging the field sets is what produces a document mxbuild accepts and Studio
Pro cannot open.

The read was lossy in both engines and is fixed here too. The legacy parser
never looked at Schedule at all, and the modelsdk reader went through
modelsdk/gen, whose generated types disagree with what Studio Pro writes on two
properties: every integer is stored as int64 where gen declares int32, and
StartDateTime is a BSON datetime where gen declares a string. Both engines now
share one codec (mdl/scheduledevents), pinned by re-serializing three whole
Studio Pro-authored documents — from Workflow Commons 4.11.0, OIDC SSO 4.6.0 and
SAML 4.2.1 — element by element, in order, with matching BSON types.

Those four references cover the Daily and Hourly variants; the other six are
derived from the metamodel and verified to load with 0 errors from `mx check`.

Interval/IntervalType are legacy siblings of Schedule that Studio Pro writes and
does not keep in sync (one shipped module stores 0/"Minute" beside a daily
schedule of 01:00). MDL has no syntax for them: a new event gets the pair
matching its repeat — an empty IntervalType is not valid, the enumeration has no
such member — and OR MODIFY carries the stored pair through untouched.

Verified on Mendix 11.13 under both engines: all eight variants leave a clean
project at 0 errors, DESCRIBE output re-parses and re-validates for every
variant, and a re-run writes nothing (the MXCLI_ALWAYS_WRITE=1 control changes 9
units where the normal run changes 0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUToAkUx54bNkNjsBpufRH
The second thing an update has to carry, alongside the GUIDs.

A user role's grant of a module role lives in the *project's* security
document, not in the module, so removing the module takes it away and
putting the module back does not return it. Measured on a blank 11.12.1
app: dropping Administration left Administrator holding 2 module roles
instead of 3, and User 3 instead of 4. Nothing complains -- the app
builds, and users quietly lose access.

CaptureRoleGrants records which of one module's roles each user role
grants; RestoreRoleGrants re-grants them afterwards. Only that module's
roles are recorded, because an update touches one module and restoring a
grant it never removed would be a write nobody asked for.

A recorded role the new version no longer defines is reported as dropped
rather than skipped. Someone had that access and now cannot, which is a
permission change the operator has to see rather than something to hide
behind a successful-looking update.

Restoring goes through MDL (ALTER USER ROLE ... ADD MODULE ROLES) so it
uses the same validated write path a user would, instead of a second
hand-rolled security writer.

The test asserts the loss before asserting the repair -- grants gone
after the drop, still gone after the transplant, restored only by the
restore. Without those two intermediate checks it would pass against code
that did nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
claude and others added 14 commits August 12, 2026 05:16
… help surface

The two features were implemented but only half-discoverable. Filling the gaps
the CLAUDE.md checklist calls for:

- `mxcli describe` did not know either type. Both are now in the type list, the
  dispatch, the error message, and the live-reader auto-detect map — so
  `mxcli describe -p app.mpr Ops.NightlyCleanup` resolves without naming a type,
  as it already does for every other document.
- `show structure` counted scheduled events but not queues, so a queue was
  invisible at every depth. Added to the depth-1 counts, the compact summary and
  the depth-2/3 listings, and to `project-tree` (which feeds the VS Code tree).
- New user-facing skill `.claude/skills/mendix/scheduled-events-and-queues.md`,
  indexed in the skills README and its LLM loading guide, so `mxcli init` ships
  it into user projects. It leads with the trap: pick the Repeat first, then use
  only that repeat's fields.
- CLAUDE.md: both features added to the implementation status, and the skill to
  the read-this-first list.
- VS Code extension: tree icons, terminal-link type inference, the describe
  fallback type list, and the context-menu viewItem pattern.

Already in place from the feature commits: `mxcli syntax queue` /
`mxcli syntax scheduled-event`, MDL_QUICK_REFERENCE, the docs-site page, the
symptom-table rows and the MDL examples.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUToAkUx54bNkNjsBpufRH
The command. It refuses when the module has local edits, --save-edits
writes them out as re-executable MDL first, and --force proceeds --
so a destructive update becomes park, replace, replay.

Measured end to end on real packages, Administration 4.3.2 → 4.5.0 in a
blank 11.12.1 app with one locally added attribute:

  28 units copied, 9 element identities preserved, 2 role grants restored
  Removed in 4.5.0 (1): Account/MyLocalEdit

Account's GUID after the update is ea496eb1df91aa4caed86ba4c4e133c5 --
byte-identical to the mendixsystem$entity.id read from the live database
in §8, so the table survives. The role grants are back at 3 and 4, the
counts before the drop. Replaying the parked file restores MyLocalEdit
and its access rules.

Running it for real found a defect no fixture would have. The reference
project is built with `mx module-import`, which stamps a module with the
author's INTERNAL version rather than the marketplace release: after
updating to 4.5.0 the project recorded AppStoreVersion 2.0.1 and an
unrelated GUID. That is not cosmetic -- diff and update identify a module
by its AppStoreGuid, so the next update could not have found it at all.
StampMarketplaceVersion fixes it, and a second update now correctly
reports "already at 4.5.0; nothing to do".

Also measured: the update leaves 11 CE0463 widget errors, which
`mx update-widgets` clears to 0. That is the known resync after any
headless module install (mxcli-formula1 FINDINGS §53), not a fault here,
so the command says so rather than leaving the user to diagnose it.

Not done: this does not roll back. A failure partway leaves the module
removed, and the command says to work on a copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Testing slice 5 against DataWidgets 3.5.0 -> 3.11.3 found a gap that
Administration could not expose, because Administration ships no widgets.

The update moved only the model. A widget module's .mpk carries its
widget binaries under widgets/, and copying units out of a reference
project never touches them -- so the project reported 3.11.3, its pages
referenced 3.11.3's widget definitions, and all ten binaries on disk were
still 3.5.0 (Datagrid.mpk project=216193 vs package=166933). No build
error names that.

InstallPackageWidgets takes them from the package rather than from the
reference project, whose widgets/ also holds the blank template's widgets
-- copying those would overwrite widgets the update has nothing to do
with. Zero-size directory entries are skipped: the 3.11.3 package has ten
widgets/ entries and nine real files. Re-run: 9 of 9 match 3.11.3.

Also corrects the post-update guidance. `mx update-widgets` clears the
CE0463 resync as before, but DataWidgets 3.11.3 then leaves 29 x CE6083
-- its widgets want design properties an older Atlas does not define.
`mx rename-design-properties` renames 0 and changes nothing, so this is a
cross-module dependency rather than a resync, and the command now says so
instead of implying two commands will clear everything.

Administration 4.3.2 -> 4.5.0 still reaches 0 errors. DataWidgets does
not, and that is a property of the content rather than of the update.

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

Neither document type was in the catalog, and the consequence was worse than a
missing table. A scheduled event produced no edge in the reference graph, so a
microflow run only by one was reported as unused from three directions at once:

  show callers of Ops.SE_Cleanup    -> (no callers found)
  CATALOG.GRAPH_DEAD_ASSETS         -> Ops.SE_Cleanup
  mxcli lint                        -> [QUAL004] Microflow 'SE_Cleanup' is not
                                       called from anywhere.
                                       -> Remove if unused

on a microflow that runs nightly. The table is an absence somebody notices; the
missing edge is a wrong answer three tools state confidently, one of which
recommends deleting the file.

Changes:

- CATALOG.SCHEDULED_EVENTS (microflow, repeat, a readable schedule phrase,
  interval, enabled, timezone, on-overlap) and CATALOG.QUEUES (parallelism as a
  string — Mendix stores an expression — and cluster-wide), both registered in
  Tables() so SHOW CATALOG TABLES lists them.
- A `schedule` edge from each event to its microflow, added to callerRefKinds,
  graphRefKinds and the QUAL004 rule — three consumers, none sharing the others'
  list.
- The linter's interval_seconds is now derived from the Schedule child instead of
  the stored Interval/IntervalType pair. Studio Pro writes that pair and does not
  keep it in sync (Workflow Commons ships 0/"Minute" beside a daily schedule), so
  a "fires too often" rule read a nightly job as every 0 seconds. Rules can now
  also branch on repeat / on_overlap / time_zone, and iterate queues().

Also fixes a regression from the queue feature: QUEUES became a lexer keyword, so
`select * from CATALOG.QUEUES` parsed to nothing — no error, no output — until
QUEUES was added to catalogTableName, the same trap COMMUNITIES hit before.

Both fixes have a control: reverting the interval derivation fails the schedule
tests, and removing the ref edge takes QUAL004 back from 1 to 2 on the test
project.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUToAkUx54bNkNjsBpufRH
feat(queues): author task queues in MDL, and refuse the rewrite that dropped them
The CE6083 design-property errors left over from DataWidgets 3.5.0 ->
3.11.3 were diagnosed in the previous commit as a cross-module dependency
on a newer Atlas. That was wrong, and the correction is the point of this
one.

DataWidgets ships its OWN themesource/datawidgets/web/design-properties.json.
The properties CE6083 named -- Grid spacing, Hover, Pagination on Gallery
-- are declared there, not by Atlas, and the project still held the 3.5.0
copy. Updating Atlas would have changed nothing. `mx rename-design-properties`
renaming 0 was the tell: it renames properties between Atlas generations,
so nothing to rename means the declaration was absent rather than moved.

This was the third instance of one gap, presenting as three problems: the
model was copied, then widgets were added because the binaries were
stale, and themesource was still missing. Enumerating the directories
that seem to matter is what produced that sequence. InstallPackageFiles
copies everything the package contains, excluding only project.mpr and
package.xml -- manifest rather than payload -- with a guard against paths
escaping the project.

Measured: DataWidgets 3.5.0 -> 3.11.3 replaces 49 bundled files, and
after `mx update-widgets` the app contains 0 errors. Administration
4.3.2 -> 4.5.0 remains at 0. Both subjects now check clean.

The post-update guidance drops the claim that a check may surface
dependency errors, since that came from the wrong diagnosis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Installing a NEW module was refused on any MPR v2 project, because the
only path was `mx module-import` and that rewrites v2 as v1. Since a
vanilla `mxcli new` app is v2 and every module is a new install there,
the headless install path was effectively closed for exactly the case it
matters in -- the guard was right and the capability was missing.

Slice 3 already built the missing piece. Installing is the update path
minus the drop and minus identity capture: build a reference project from
the package, copy the module's units in with mxcli's own writer, stamp
the marketplace version, install the bundled files. The format is
preserved because the writer handles v1 and v2 alike, and theme modules
work because the reference builder clears the flag module-import refuses
on.

Measured on a vanilla 11.12.1 app: CommunityCommons 11.5.1 installs as
128 units and 126 bundled files, mprcontents/ grows from 369 to 497
.mxunit files -- so the project is still v2 -- and after
`mx update-widgets` the app contains 0 errors.

--allow-format-change still selects the legacy module-import path for
anyone who wants exact mx semantics, and the v2 guard still covers it.

This is what unblocks installing the Agents Kit 2 modules headlessly,
including Conversational UI, which mxcli-formula1 FINDINGS §53 recorded
as impossible by any headless path because module-import rejects theme
modules outright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The marketplace skill stopped at "install a new module; for updates, use
Studio Pro", which is two commands out of date. It now covers discover →
download → install → diff → update, with the measurements behind each
step and the limits stated up front (no dependency resolution, no
rollback, local edits not preserved).

It also corrects a hint that was actively destructive. After a headless
module install the project's widget definitions are stale and a check
reports CE0463, and both marketplace commands told the user to fix that
with bare `mx update-widgets`. That command does perform the resync, and
rewrites an MPR v2 project as v1 while doing it — measured on 11.12.1,
200 .mxunit files to 0 and a 69,632-byte index to 14 MB. The same
collapse mxcli refuses to perform through `mx module-import` was being
recommended in prose.

`mxcli docker check` is the v2-safe route: it runs the same step under a
storage-format snapshot, so the check sees the resynced model and the
project keeps its format (verified: 200 units and 69,632 bytes before
and after). That also means the resync is not persisted, which the docs
now say rather than implying it sticks.
Running the documented install workflow end-to-end on a fresh 11.12.1 app
failed on the first command. The marketplace publishes new releases
against the newest Studio Pro patch within days of it shipping, and
`install` with no --version resolves to the latest — so on any project
not on the very newest patch, the default is routinely the one version
that cannot be installed. All six agent-editor stack modules had a latest
release requiring 11.12.2, published five days earlier.

The refusal already happened, three layers down: after the download and
after building a reference project, as `mx module-import` exit 117, under
the command's full flag list. Every version the API returns already
carries minSupportedMendixVersion; nothing consulted it. Now install and
update check it first and name the version to use instead. SilenceErrors
goes with SilenceUsage because main() already prints what Execute
returns, so silencing only usage left every refusal printed twice.

The rest of the run is recorded in the skill: 8 modules and 2 widget
packages into a vanilla app, error count 0 → 15 → 0 → 18 → 1 → 22 → 1,
staying MPR v2 throughout (1,869 .mxunit files), ending with a working
`create agent`. Dependencies are not resolved and include widget content,
and the count rises before it falls — both now stated with the measured
sequence rather than in the abstract.

Two corrections. The .mxunit counts published yesterday were shard
directories, not files: the update-widgets collapse is 370 files to 0,
not 200. And CE6087 has no headless fix — `mx rename-design-properties`
does real work (149 properties across 41 documents) and collapses v2 like
its siblings, but unlike the widget resync its result must persist, so
the snapshot-and-restore trick does not transfer. Documented as open
rather than papered over.
`mx update-widgets` and `mx rename-design-properties` each fix
something only Mendix can fix — CE0463 and CE6087, the normal aftermath
of a headless module install — and each rewrites an MPR v2 project into
the single-file v1 format while doing it. Measured on 11.12.1:
update-widgets took 369 .mxunit files to 0; rename-design-properties
took 1,865 to 0 and a 249,856-byte index to 39,895,040 bytes, having
renamed 149 design properties across 41 documents.

The snapshot-and-restore that already protects update-widgets (mendixlabs#808)
cannot be reused. That one is allowed to throw the tool's output away
once the check has run; these renames have to persist, so restoring the
snapshot would restore the un-renamed model with it. So harvest instead:
let the tool convert the project, read every unit back out of the
converted file, restore the v2 storage, and write the changed units into
it through mxcli's own writer — which is also where canon.Reconcile
preserves identity fields and elides the units the tool rewrote without
changing anything (ADR-0008), so a second run writes nothing. Copying
whole units is safe for the same reason a module transplant is: no
binary $ID crosses a unit boundary.

Exposed as `mxcli fix widgets` and `mxcli fix design-properties`. Both
print the storage count before and after, because a collapse shows up
there as a zero and a success message without its own evidence is how
this shipped the first time. Every failure path after the tool has run
restores the format first, so a failed command leaves the project as it
was rather than as a half-converted v1 file.

Measured end to end on the vanilla 11.12.1 agent-stack app: a plain
`mx check` reported 203 errors (202 CE0463 + 1 CE6087) and 0 after the
two commands, with the project still v2 at 1,868 units — reproduced from
a restored pre-fix snapshot, so the fix is shown to be the cause. Under
MXCLI_ALWAYS_WRITE every one of the 1,868 units round-trips and the
model still checks clean. Each new test was verified to fail with the
reported symptom when the fix is stubbed out.

Also corrects an off-by-one in yesterday's counts: mprcontents/ holds an
`mprname` file beside the units, so a raw file count is one higher than
the unit count. unitCount now counts only .mxunit, with a test tying it
to what the reader reports.
Four conflicting files, all from two branches adding document types to the
same registries: this branch added menus, page templates and icon
collections; main added task queues and scheduled events. Each conflict is
additive, so both sides are kept — the CLAUDE.md status list, the
`describe` CLI's type list and storage-name map, the catalog's table
definitions and objects view, and Catalog.Tables().

One conflict the textual merge could not see. main's two new document
types are emitted by the catalog objects view but had no entry in
`objectTypeToDescribeKind`, which is the exact state BUILDING_BLOCK and
ICON_COLLECTION were in when 43 of 251 documents in a marketplace project
could not be reached by bare `DESCRIBE Module.Name`. That guard is a test
this branch introduced, so it fired for the first time on the merge and
main had no way to know about it.

Listing them as deliberately not-auto-describable would have silenced it
without fixing anything, so they are wired up instead: two DescribeStmt
kinds, two dispatch cases synthesizing the statement types queues and
scheduled events already use, and their labels. `DESCRIBE Module.Name`
now resolves a queue or a scheduled event without naming the type.

The generated ANTLR parser is regenerated by `make`, so the merged
grammar builds. make build, make test and make lint all green.
@ako
ako merged commit 4b15694 into mendixlabs:main Aug 12, 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