https://github.com/mendixlabs/mxcli/compare/main...ako:mxcli:main?expand=1 - #878
Merged
Conversation
`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
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 againstthe four Studio Pro-authored queues in Business Events 3.12.1, which agree
exactly:
Config.ParallelismExpressionis a string, and the sibling int32Parallelismis absent in all four, so it is not written. Parallelism is anexpression 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 microflowwroteQueueSettingsback as null and the binding was gone — silently. Worse,mx checkwent fromCE1613 "The selected task queue no longer exists"to0 errors, because the configuration the error was about had been deleted;
describenever showed the binding either, so the loss was invisible fromevery angle. Authoring a queued call is deliberately still unimplemented —
the
Retryshape has no Studio Pro-authored sample to diff against, andguessing 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$*Schedulevariants, and only thatvariant's fields are accepted — a
Multiplieron aDailyrepeat is refusedby
mxcli check(MDL-SCHED01) and by exec, which call the same function sothe two cannot drift. Merging the field sets is what produces a document
mxbuild accepts and Studio Pro cannot open.
modelsdk/genis wrong about two properties here, so both engines shareone 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
StartDateTimeis a BSONdatetime (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/IntervalTypeare legacy siblings ofSchedulethat Studio Prowrites and does not keep in sync (Workflow Commons stores
0/"Minute"beside a
DayScheduleof 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,
describeoutput re-parses and re-validates for everyvariant, and a re-run writes nothing — the
MXCLI_ALWAYS_WRITE=1controlchanges 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:
CATALOG.SCHEDULED_EVENTSandCATALOG.QUEUES, registered inTables().scheduleedge from each event to its microflow, added tocallerRefKinds,graphRefKindsand the QUAL004 rule — three consumers, none of which sharesthe others' list.
interval_secondsnow derives from theSchedulechild insteadof 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 iteratequeues().Also fixes a regression introduced by the queue work earlier in this series:
QUEUESbecame a lexer keyword, soselect * from CATALOG.QUEUESparsed tonothing — no error, no output — until
QUEUESwas added tocatalogTableName,the same trap
COMMUNITIEShit before.Independent fixes
show callershid widget-action references. Page/widgetactionedgeswere not indexed, so a microflow called only from a button reported no
callers.
mxcli new --output-dirwith a deep path. The project is created in astaging directory first, so a nested output path works.
so the workflow stops failing on forks.
Verification
go build ./..., fullgo test ./...,make check-mdlandmake lint-goaregreen 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.