Skip to content

Sync ako/mxcli: List View specialization templates, packaging fixes, and folder placement for every document type - #942

Merged
ako merged 34 commits into
mendixlabs:mainfrom
ako:main
Aug 20, 2026
Merged

Sync ako/mxcli: List View specialization templates, packaging fixes, and folder placement for every document type#942
ako merged 34 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Syncs ako/mxcli:main into mendixlabs/mxcli:main.

34 commits (20 non-merge, 14 merges), 191 files changed, +8526 −803.
Clean fast-forward — git merge-base --is-ancestor holds, no rebase or squash
divergence.

Covers PRs #197#203. Several entries below were found by triaging real
applications' findings against mxbuild 11.13 and against Studio Pro authored
projects, so they are corrections to things mxcli reported as working.

Model content that was silently lost

Each of these reported success and left a project mx check called clean. That
is the whole problem: the document stays valid, it just holds less.

  • List View specialization templates were destroyed by a round trip. A list
    view over a generalization can render a body per specialization; DESCRIBE PAGE never read the Templates array, and since DESCRIBE emits create or modify page, re-executing its output rebuilt the page without them. Measured
    on a Studio Pro authored page: 4 templates before, 0 after, mx check
    clean both times. SHOW REFERENCES also reported 0 pages, 0 widgets for an
    entity used only inside a template, so tooling deciding "unused, safe to
    delete" would have deleted a document in active use.
  • A folder was dropped on describe/replay. Most document types had no
    FOLDER clause at all — mappings, queues, scheduled events, regular
    expressions, workflows, menus, image collections, Java/JavaScript actions,
    database connections, data transformers, the AI-agent documents — so they could
    only be created at the module root, and describing one Studio Pro had filed
    away and replaying it recreated it unfiled.
  • ALTER STYLING … SET '<design property>' wrote nothing on any widget mxcli
    had created, while printing "Updated styling on widget". Two defects composing:
    a bsonnav.DSet that could not create an absent array, and an array written
    without its Mendix list marker.
  • --require-assertions was a silent no-op on the default runner. It was
    consulted in one of the two result-assembly loops, so mxcli test tests/ -p app.mpr --require-assertions — without --local — exited 0 on a suite that
    asserted nothing.

Projects mxcli produced that Mendix tooling then refused

  • mx create-module-package aborted on any module mxcli created, with
    Unable to cast object of type 'JValue' to type 'JObject' inside
    MprDocumentHasher. Fixed by writing the page keys Studio Pro writes and the
    marker its reader needs.
  • CE0463 on a hidden pluggable-widget property. A widget's editorConfig.js
    hides properties under some configurations and mxbuild evaluates that logic: a
    hidden property must hold its default or the build fails. Now only the property
    that actually fails the build is flagged, not every harmless one.

New authoring surface

  • List View specialization templates, in MDL for the first time:
    template for Module.Entity { … } inside a list view, with INSERT INTO and
    DROP TEMPLATE FOR … IN … on ALTER PAGE. Templates keep their source order
    (Mendix matches in that order, so it is authored, not derived), and inside one
    the context object is the specialization, so an attribute only it has resolves.
    Refused rather than written: an entity outside the list view's hierarchy, a
    duplicate, and a nested template.
  • A FOLDER clause on CREATE for every document type, applied on
    CREATE OR MODIFY too, and emitted by DESCRIBE.
  • MOVE accepts every top-level document type. It listed nine; the other
    twenty-two were never unimplemented, merely unlisted — the backend methods sat
    fully wired and called from nowhere.
  • The type split now uses the enum split's branch syntax, so the two
    multi-way splits are one statement with two subjects.

DESCRIBE correctness

  • A widget's datasource is read and rendered in one place. DESCRIBE emitted
    database from for datasources that were not a database-over-entity, sometimes
    with the entity slot blank, sometimes with a microflow name in it — which made
    its own output fail mxcli check --references.
  • A flow datasource's arguments now survive describe.
  • An XPath constraint is always emitted as a bracketed group.
  • A building-block datasource override rebinds by widget type, not by one
    that happens to already be present.

Commands that did not do what they said

  • SHOW ACCESS ON ENTITY did not parse, and SHOW ACCESS ON PAGE described
    the page instead of its access.

Docs

  • The symptom table records the mx check baseline trap: comparing against a
    project that was already dirty weakened a control, so a fix looked verified
    when it was not.
  • Bug reports and a proposal for the split-syntax change, and the folder clause
    documented everywhere it is looked up

claude and others added 30 commits August 20, 2026 09:56
…doctype

A document's folder is its unit's ContainerID — a column in the Unit row, not
a field in the document's contents. Every Update* on both engines rewrites
contents only, so a handler that resolved a folder, stored it on the model
object and called Update had the placement silently dropped: the statement
reported success, resolveFolder had already created the folder as a side
effect, and the document stayed where it was.

Reported against JSON structures (mendixlabs#932); it was true of every doctype with a
FOLDER clause. Verified against raw Unit-table containment counts rather than
mxcli's own read paths, because the same symptom was once a read illusion
(mendixlabs#722 / Bug 12b): before the fix a modify-with-folder left "Documents in
Modules" and "Documents in Folders" unchanged, while an explicit MOVE of a
supported doctype — the positive control — shifted them by one.

Fixed as a class. One type-agnostic MoveDocument(unitID, containerID) on the
backend rather than a Move<Doctype> per document type: placement is the same
row update whatever the document is, and a per-doctype method means the next
document type inherits the bug simply by not having one written for it.
FindDocumentUnit resolves a name through the unit table for the same reason.

Two things the row update has to do for itself, because a move changes no byte
of the document and is therefore invisible to content-based no-op elision
(ADR-0008): count in WriteStats, so ReportMutation stops calling a real move
"Unchanged", and skip when the unit is already in that container, so
re-running an in-sync script leaves the .mpr byte-identical. Removing either
fails TestMoveDocumentPersistsAndIsIdempotent.

An omitted FOLDER must leave placement alone — resolveFolder("") answers with
the module root, so passing it through would unfile every foldered document on
the next CREATE OR MODIFY, which is worse than the no-op being fixed.
resolveRequestedFolder returns "" for "not asked" and applyDocumentFolder
ignores "", so no call site has to remember the distinction. Doctypes rewritten
as delete+create (snippets, rest clients) had the inverse bug already live —
they really do re-apply the container — and now carry the stored one forward.

mx check: 1078 errors before and after on the probe project (all pre-existing
CE6083 design-property errors), so no new build errors.

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

MOVE listed nine doctypes. The other twenty-two were never unimplemented,
merely unlisted — a top-level document's move is one containment row whatever
the document is — so `MOVE IMPORT MAPPING …` was a parse error while
MoveImportMapping sat fully wired on the interface, both engines and the mock,
called from nowhere. Reported in mendixlabs#932 for mappings and JSON structures.

The doctype list is now a grammar RULE rather than an inline alternation, and
that is the substantive part. MOVE FOLDER is told from a document move by the
ABSENCE of a doctype, and that discriminator was a hand-written negation of
every doctype keyword (mxcli-formula1 #32 flagged it after adding two). With
twenty-two more it would not have stayed in step; as a sub-rule it is one check
that cannot go stale.

The document is resolved through the unit table rather than a per-kind list, so
one lookup covers every doctype including ones added later. Deliberately no
doctype-to-$Type table to validate against: two such tables already exist here
and disagree with each other and with a real project (unit_types.go says
JavaActions$JavaScriptAction, while the writer inserts and Studio Pro stores
JavaScriptActions$JavaScriptAction). The kind is derived from the stored $Type
instead, and a mismatch is refused only when the derived kind is itself a
doctype MOVE can spell — so a derivation that cannot be vouched for defers to
the name lookup instead of blocking a legitimate move. Output names the kind
found, not the kind the statement asked for, since the two can differ.

LIST FOLDERS had the same shape of defect and it made moves unverifiable: its
hand-maintained kind list still hid layouts, menus, JavaScript actions and
building blocks after mendixlabs#892 added five kinds to it. It now fills its gaps from
the same type-agnostic walk — measured 78 to 259 documents listed on a
7-module project, with nothing removed.

Docs, syntax help and the organize-project skill carried a doctype list that
was stale before this change, omitting three types MOVE already accepted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
Import and export mappings had none, which is half of mendixlabs#932 — and so did queues,
scheduled events, regular expressions, workflows, menus, image collections,
Java and JavaScript actions, database connections, data transformers and the
four AI-agent documents. All of them could only ever be created at the module
root, and DESCRIBE emitted no folder either, so describing one that Studio Pro
had filed away and replaying it recreated it unfiled.

The clause goes in one place — straight after the qualified name — rather than
matching each rule's local style. Pages/snippets (Folder: 'path' property) and
microflows (folder 'path' before BEGIN) already disagree with each other; a
third convention per doctype would make the syntax unlearnable.

Two rules already contained a direct STRING_LITERAL, and one of them was a
trap. createWorkflowStatement read DISPLAY / DESCRIPTION / DUE DATE by counting
AllSTRING_LITERAL() in order, so a folder path — now the rule's first string —
silently became the display name and shifted every later clause by one. Fixed
by labelling the tokens in the grammar and reading them by label, not by
placing the new clause last: positional counting is wrong for any combination
of optional clauses, not just this one. Reverting the visitor to positional
makes TestWorkflowHeaderClausesAreReadByLabel report the folder path as the
display name. Same treatment for the data transformer's source path.

The executor side is uniform: containerForDocument encodes the precedence
(named folder, else current placement, else module root) once, so no handler
decides it again — and "no clause" keeps meaning "leave placement alone".

Verified by replaying a DESCRIBE into a module where the document does not
exist, which is the only way to see whether placement survived; replaying over
the original reports Unchanged either way. mx check: 1078 errors before and
after on the probe project, all pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
Audit of the doc surfaces the PR checklist names turned up gaps the code
changes had left behind.

`mxcli syntax` was the biggest one: sixteen per-doctype topics showed a CREATE
form that now accepts a folder clause and did not mention it. Each Syntax field
gained the clause where it actually goes, and the cross-cutting rules — which
statement shapes take it as a property vs a keyword, what an omitted clause
means, that CREATE OR MODIFY moves — live in one new `document-folder` topic,
following the precedent set by `create-modifiers`: documented where the
question comes up rather than repeated across all 27. `folder` is aliased to
it, since that is the word someone reaches for.

TestExamplesParse earned its keep here, rejecting two examples in that topic
that did not parse.

Also: seven docs-site reference pages (synopsis + a FOLDER parameter entry
linking to MOVE), the two quick references, and the three skills that describe
authoring these documents.

No new lexer tokens, so LSP completions regenerate unchanged from MDLLexer.g4
at build time, and no new LSP capability, so the VS Code extension is
unaffected. MOVE has no Cobra subcommand — it is MDL only — so there is no CLI
help to update beyond the syntax registry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
`mxcli test tests/ -p app.mpr --require-assertions` — no --local, the command
in the report — accepted the flag and exited 0 on a suite where every test
asserted nothing. `--help` listed it, it parsed, and the summary even printed
"1 test(s) asserted nothing beyond \"did not throw\"", so the run read as
having noticed and chosen to pass.

The flag was consulted in exactly one of the two result-assembly loops.
`vacuousResult` had a single caller, in the endpoint runner (`--local`,
`--attach`); the after-startup runner (Docker default, and
`--local --legacy-runner`) assembles its results in
`ParseLogResults(logReader, suite)`, whose signature had no parameter the flag
could arrive through — it could not honour it even in principle.

Give the pre-run verdicts — the ones settled by the parsed test case rather
than by the run — one function, `preRunResult`, and route both loops through
it. `TestPreRunVerdictsHaveOneCallSite` pins that it is their only caller, so a
third verdict or a third runner cannot reintroduce the asymmetry. Same lesson
as the `newResult` constructor: a decision reachable from one path and not the
other is the defect.

Implemented rather than refused. `rejectVerifyOnLegacyRunner` refuses @verify
on this runner because it genuinely cannot evaluate it — its tests run during
boot, so there is no point at which to query the app. `AssertionCount()` is a
static property of the parsed test case, known before any runner starts, so
refusing here would have denied a capability the runner has.

Controls: with the flag re-severed on the log path the new test fails with the
reported symptom ("status = PASS, want ERROR"), and with the endpoint loop
restored to calling the two helpers directly the call-site guard fails naming
both sites. The pre-existing `TestRequireAssertionsMakesVacuousTestsErrors` is
why this survived — it calls `vacuousResult` directly, proving the helper works
and saying nothing about whether any runner calls it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
…PAGE describing the page

`SHOW ACCESS ON ENTITY Mod.Entity` was a parse error while the MICROFLOW
and PAGE spellings parsed, though the CLAUDE.md that `mxcli init` writes
into every project documents all three:

  $ mxcli -p app.mpr -c "SHOW ACCESS ON ENTITY eShop.CatalogBrand"
  Parse error: line 1:60 extraneous input '.' expecting the start of a statement

There was no `ACCESS ON ENTITY` grammar alternative, and ENTITY is in the
`keyword` rule, so `showOrList ACCESS ON qualifiedName` matched the word
ENTITY as the whole name; the statement ended there and the real name was
extraneous input. The command itself always existed — the bare
`SHOW ACCESS ON Mod.Entity` is the same AST node and the same handler, so
only the explicit spelling was unreachable.

Found by the test sweep for that fix, and fixed with it: `SHOW ACCESS ON
PAGE` parsed but answered the wrong question. `ExitShowStatement` is a
long if/else chain, and the singular `SHOW PAGE` → DESCRIBE PAGE alias
branch matches on `ctx.PAGE()` alone ~200 lines earlier, so the command
printed the page's entire widget tree instead of its allowed module
roles. That is why the issue reports the PAGE variant as working. The
ENTITY branch would have swallowed the new alternative the same way, so
both are now guarded with `ctx.ACCESS() == nil`.

Controls, each reverted separately against the new tests:

  - no grammar alternative  → the original extraneous-input parse error
  - no ENTITY visitor guard → ObjectType = ENTITY (the definition)
  - no PAGE visitor guard   → *ast.DescribeStmt (the widget tree)

Verified end-to-end on a real 10.24 project: entity access rules and page
allowed roles are reported, while `show entity` and `show page` still
describe. `TestEveryShowAlternativeProducesAStatement` cannot catch this
class — a statement is produced, just the wrong one — so the new tests
assert the ObjectType rather than mere presence.

Fixes mendixlabs#925
… syntax

Investigation of mendixlabs#913 ("MDL decision (split) syntax
inconsistent"), reproduced against mxbuild 11.13.0.

The reporter is right that the two multi-way splits are inconsistent, but
the investigation found a defect the issue does not name and that matters
more than the spelling: `else` on a `split type` is the `(empty)` flow — it
runs when the object is null, not when no `case` matched. Proven with
mxbuild: `case Dog` + `else` still fails CE0090 demanding flows for `Cat`
and the base `Animal`; covering every type while keeping the `else` gives 0
errors, so `else` contributes nothing to type coverage.

Also documented: DESCRIBE indents the two splits inconsistently and neither
matches if/else (enum split puts the branch body at the same column as
`when`; the type split puts `case` flush with `split type`), and no test
pins either, which is why it shipped.

The issue's proposed remedy — a Java-shaped `switch` — is rejected in the
proposal: ADR-0003 commits MDL to being SQL-shaped, and MDL already has a
second `case … when … then … end` in caseExpression, so `switch` would make
three spellings rather than one. The proposal instead moves the type split
onto the enum split's `when … then` / `when (empty) then`, which satisfies
the reporter's consistency complaint, names the empty branch honestly, and
keeps the old spelling parsing.

Adds a fix-issue symptom row for the `else` trap, which persists regardless
of whether the proposal is adopted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
A type split now takes `when <Entity> then` branches and a `when (empty) then`
empty branch, so the two multi-way splits are one statement with two subjects:

  case $Status                      split type $Animal
    when Open, Pending then           when Zoo.Dog then
      ...                               ...
    when (empty) then                 when (empty) then
      ...                               ...
  end case;                         end split;

Fixes three defects from mendixlabs#913.

`else` was the worst of them. It reads as a default branch and is not one: it
maps to Mendix's `(empty)` outgoing flow, taken when the object is null.
Measured on mxbuild 11.13.0, a split with one `case` and an `else` still fails
CE0090 demanding a flow for every other subtype AND for the base entity; cover
every type while keeping the `else` and it is 0 errors. The branch is now
spelled for what it does, and MDL065's message states the semantics and the
CE0090 consequence rather than reading as a rename.

`case` also meant two things — subject introducer in `case $x when V then` and
in caseExpression, branch introducer here. It now introduces a subject
everywhere.

Branch bodies used to render at the same column as their branch keyword, so a
nested `if`'s `else` landed exactly where a reader expects a case branch — in
output where `else` on a `case` is an MDL008 error. Both splits are fixed, in
both emitters: cmd_diff_mdl.go had the identical bug and was found by grepping
for the pattern rather than by the report.

The legacy `case`/`else` spelling still parses, indefinitely — scripts in the
wild use it and both spellings build the identical flow. MDL065 warns. It stays
a warning because exec's pre-flight gate halts on errors and would otherwise
refuse every existing script; TestMDL065_DoesNotBlockExecution pins that.

Verified end to end: two projects built from the two spellings describe
identically and both pass mx check with 0 errors; the three pre-existing
fixtures using the legacy spelling still build clean and now warn once each;
DESCRIBE output re-executes with no warnings. The indentation test was run
against reverted emitters and fails with the reported symptom before passing
after. The three DESCRIBE tests pinning the old spelling failed on the change
and were updated deliberately, not loosened.

Also documents the type split in docs-site and `mxcli syntax`, where it was
absent entirely, and corrects the two proposals whose examples contradicted
shipped behaviour.

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

`mx create-module-package` aborted on any module mxcli created:

    Exception occurred: Unable to cast object of type
    'Newtonsoft.Json.Linq.JValue' to type 'Newtonsoft.Json.Linq.JObject'
       at MprProperty.Init → MprUnit.get_Contents → MprDocumentHasher.Write

while `mx check` reported 0 errors. Reported against an Accordion; measured to
have nothing to do with pluggable widgets — `create module M; create page M.P {
DYNAMICTEXT }` reproduces it, and so does a binary built from origin/main.

`Forms$Page.AllowedModuleRoles` was written with typed-array marker 3. The
marker tells Mendix's reader what the entries are, and the entries are
qualified-name strings, so a non-empty list under marker 3 is a JValue where
the reader wants a JObject. An empty list never crashed — which is why it took
a page carrying a role, and `create module` auto-grants one. The legacy engine
has always written 1; only the modelsdk (default) engine had this.

Measured on mxbuild 11.13 at security Off, Prototype and Production: marker 1
is 0 errors and exports; marker 3 is 0 errors and crashes. Studio Pro writes 1
on all 16 pages of a blank app, empty lists included. The comment on
NewByNameRefListV3 asserting that marker 1 raises CE0557 does not reproduce at
any of the three levels, and is corrected rather than left to mislead again.

Two keys mxcli omitted are added at the same time, both found by handing an
mxcli-authored page to `mx update-widgets` and diffing the unit it wrote back:

  - Forms$Appearance always carries a DesignProperties list, emitted as the
    empty marker [3]. The codec omits an empty, never-appended PartList, so
    mxcli's widgets had no such key — which is also why ALTER STYLING's design
    property was a silent no-op (next commit).
  - CustomWidgets$CustomWidget always carries LabelTemplate, null when unset.

The repair is not retroactive: idempotent writes leave an unchanged page alone,
so an existing project keeps the old marker until each page is rewritten.

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

`ALTER STYLING ON PAGE p WIDGET w SET '<design property>' = '<value>'` printed
"Updated styling on widget" and wrote nothing, on any widget mxcli had created.
The same statement works on a Studio Pro-authored widget, and the inline
`DesignProperties: [...]` form works everywhere.

Two defects composing into a silent no-op. `bsonnav.DSet` takes a bson.D by
value, so it can only overwrite an existing key — it returns false and does
nothing when the key is absent, and DSetArray ignored that result. And the key
was absent: until the previous commit, mxcli's Forms$Appearance carried no
DesignProperties array at all.

Fixing the append alone would have been worse than the bug. A Mendix array's
first entry is its typed-array marker, and DSetArray only preserved a marker it
could see. Measured on mxbuild 11.13, a DesignProperties array written bare
makes the project fail to LOAD ("Type OptionDesignPropertyValue does not
contain a constructor with a parameter of type Appearance") and
create-module-package die with "Unknown export error"; a doubled marker throws
StorageLoadException. So:

  - DSetArray returns bool and REFUSES a markerless non-empty write rather than
    producing a document Mendix cannot open.
  - DSetArrayIn is the form that can create the property: it takes the parent,
    so the grown child can be written back, and the caller supplies the marker.
  - The design-property mutators go through it with marker 3 (measured on every
    page of a blank 11.13 app) and return an error instead of reporting success.

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

A pluggable widget's editorConfig.js hides properties under some configurations
of the same widget, and mxbuild evaluates that logic: a hidden property must
hold its DEFAULT value, or it is CE0463 "the definition of this widget has
changed" — which fails `mx check` and makes create-module-package refuse the
module.

On the reported Accordion, mxcli's MDL-WIDGET10 fired four times, on four
properties the script had left at their defaults, and said nothing about the
two that break the build. Measured on mxbuild 11.13, one property per run:

    collapsible: false + InitialCollapsedState: 'expanded'  → CE0463
    collapsible: false + InitiallyCollapsed: 'false'        → CE0463
    collapsible: false + expandBehavior: multipleExpanded    → CE0463
    collapsible: false + animate: false                      → CE0463
    collapsible: TRUE  + each of those values                → 0 errors
    collapsible: false + every value at its default          → 0 errors

Three gaps, all in the editorConfig extractor:

  - Nested hides were skipped by design (mendixlabs#574 Phase 1). hideNestedPropertiesIn
    and the 5-argument hidePropertyIn are now lifted as rules carrying the
    object list they belong to, and evaluated per item.
  - A nested condition can read the ITEM instead of the widget — the Accordion
    hides initiallyCollapsed unless the GROUP's own initialCollapsedState is
    "dynamic". Conditions carry a scope, and each is looked up in its own
    values; reading an item condition against the widget finds an absent key
    and silently reports nothing.
  - A comparison guard reached through a `||` connector or a ternary's ELSE
    branch was recorded with the wrong polarity (eq where the code means ne).
    This also flipped several pre-existing top-level rules that were inverted,
    e.g. Combobox attributeBoolean.

Two smaller ones fall out. `X && Y || hide(…)` soundly implies "hide when Y
falsy" whatever X is, so the Accordion's compound guard is liftable; `X && Y &&
hide(…)` is not, and stays unlifted. And the shipped compilation is
`forEach((function(n,o){…}))` — a double paren the callback-parameter regex read
as the parameter name, scoping every nested condition to the widget.

The diagnostic now splits on the VALUE: a hidden property set to its default is
merely redundant (warning, as before), a non-default one is an error naming the
default. mxcli's verdict matches mxbuild on all nine measured configurations.

Widget defs are cached per project, so WidgetDefGeneratorVersion is bumped to
force a refresh; note the refresh happens during exec, so the run that
regenerates them still validates against the old set.

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

Three rows appended to the fix-issue symptom table — the hidden-property CE0463,
the silent ALTER STYLING design property, and the create-module-package hasher
crash — each with the measurement that settled it and the trap to avoid next
time: minimise before believing the reporter's attribution, check a comment's
premise before honouring it, and never fix an append without fixing the marker.

The bug-test script executes cleanly and exports cleanly; the two failing widget
shapes are kept as comments so the trap stays documented without breaking the
project-less `make check-mdl` gate, which cannot see a project's widget defs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
fix(placement): FOLDER and MOVE work for every document type (mendixlabs#932)
fix(show): make SHOW ACCESS ON ENTITY parse, and stop SHOW ACCESS ON PAGE describing the page
feat(mdl): unify the type split onto the enum split's branch syntax (mendixlabs#913)
fix: three defects behind mendixlabs#931 — hidden widget properties, uncreatable arrays, and the marker that crashed the exporter
fix(test): --require-assertions was a silent no-op on the default runner
…ixlabs#941)

DESCRIBE PAGE emitted MDL that would not re-execute for any non-database
datasource. Three faces, one cause.

A chart series bound to a microflow came back as
`DataSource: database from Module.TheMicroflow`, and re-executing it reported
"entity not found". A pluggable list whose source carried no entity came back
as `DataSource: database from ` with an empty slot, which the parser reads as
an entity called "from". And a gallery bound over an association lost its
datasource entirely — silently — while a listview bound the same way kept it.

The datasource switch had been copied per widget family: five copies on the
read side and six on the write side, drifted apart. The pluggable read copy,
which every Gallery, DataGrid2 and chart goes through, knew microflow and
nanoflow sources but not associations, listen targets or context sources, so
it dropped them; its CustomWidgetXPathSource case returned an empty database
source where its own near-twin guarded against exactly that; and the
object-list item emitter had no type switch at all.

So the fix is one reader and one renderer rather than three patches. A
datasource means the same thing wherever it sits, and the copies were the
defect — patching only the pluggable switch would have left five to drift
again. The $Type set comes from generated/metamodel's DataSource interface;
sorting is read from whichever shape is stored (SortBar.SortItems or
Sort.Paths) so the reader stays container-agnostic.

Unknown type and known-type-with-empty-payload are kept apart. The first is
mxcli's gap and is now reported as a comment naming the stored $Type — never
dropped, never guessed into a `database from` that cannot re-execute. The
second is the model's own incompleteness and still yields nothing, which an
existing test deliberately pinned; collapsing the two broke it.

Verified by describing every page in a 21-page project before and after: the
four intended changes and nothing else. That diff also surfaced two WHERE and
SORT clauses the combobox emitter had been dropping, now round-tripped. 10 of
21 pages still fail describe→check, identically before and after — unquoted
XPath containing '[', duplicate generated widget names, context errors — all
separate round-trip gaps. mx check: 950 errors before and after, all
pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
…ixlabs#941's control

Copying a project for an `mx check` baseline with `cp rp.mpr mprcontents/` is
not a copy of the project. The theme lives in `theme/`, so the copy reports 924
CE6083 "Design property is not supported by your theme" plus CE0535 for the
missing widgets/ — a baseline of ~950 errors where the real project has 0.

Before and after still compare equal, so the control looks like it passed. It
had almost no power: a genuine new error would be one line in a thousand, and
every theme-dependent check never ran. Re-run against a complete copy, the
project is 0 errors before and 0 after — which is a control that can actually
detect something.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
Restoring the WHERE clause in mendixlabs#941 collided with two older gaps in the same six
lines, and the result was a regression: stock Administration.Account_New
described to MDL that parsed before the change and did not parse after it. Every
project from the Mendix template carries that page.

The constraint was emitted raw, so a stored XPath carrying an inner predicate —
System.grantableRoles[reversed()]/… — ended the MDL expression parse at the '['.
The grammar accepts either a bare expression or a bracketed xpathConstraint;
only the second is safe for arbitrary XPath. So the emitter now always uses the
bracketed production instead of trying to decide: the bare form reads better but
parses only for simple comparisons, and whether a given XPath also happens to be
a valid MDL expression is not something the emitter can know.

Splitting goes through visitor.SplitXPathPredicateGroups — the quote- and
nesting-aware helper written for mendixlabs#772 — rather than a second copy. The old code
took the outer brackets off by testing the first and last byte, which turns
[a][b] into the mangled a][b: precisely the bug that helper exists to prevent.
The guard was also strings.Trim(s, ""), whose empty cutset trims nothing, so a
whitespace-only constraint emitted a bare `where`.

Verified as a set rather than a count, which is what the original claim got
wrong: describing every page in the project and parsing the result gives 8 pass
on the parent, 7 with the PR as submitted, 8 with this fix, and joining the
pass-sets names Account_New as the only page that regressed and shows none left.
Control: revert the bracketing and it stops parsing again.

Reported in mxcli-formula1 §57.1 and §57.3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
A microflow used as a widget datasource needs an argument for every parameter,
exactly as a call action does — mendixlabs#835 fixed the write side. DESCRIBE never read
them back, so `microflow Mod.DS(Race: $Race)` described as `microflow Mod.DS`
and replaying that description failed the build with CE1571 "No argument has
been selected for parameter", the very error mendixlabs#835 was about. `check
--references` calls the argument-less form valid, so nothing caught it before
mxbuild. Plain dataviews lose them too, not just pluggable widgets.

Older than mendixlabs#941 — the previous code printed the whole line wrong, so the loss
hid behind a larger bug. Fixed here because the renderer is now one function,
which is the right place to do it once rather than six times.

The shape came from dumping a real unit rather than from the writers, which
disagree in spelling: bindings live in <Flow>Settings.ParameterMappings, each
{Parameter, Expression} behind typed-array marker 3, with Parameter qualified by
the flow ("Mod.DS_Filtered.Term") while MDL names the parameter alone. The
prefix is stripped by matching the flow's own qualified name rather than by
cutting at the last dot, so a document storing the name bare does not lose its
first segment. A parameterless flow still renders without parentheses.

Verified by replaying a description: CE1571 goes 4 to 0 on a two-widget page,
and the remaining two errors are the probe page's own pre-existing CE0117, one
per copy. The whole-project page sweep is unchanged at 8 passing with no
regressions against the parent. Control: drop the reader and the arguments
vanish from the output again.

Reported in mxcli-formula1 §57.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
An entity used only inside a List View specialization template reported
`0 pages, 0 widgets` from SHOW REFERENCES. Anything using reference counts to
decide "unused, safe to delete" would delete a document in active use.

`Templates` was not a recognised widget-container key. extractWidgetsRecursive
knew Widgets, Rows/Columns, FooterWidgets, TabPages, pluggable
Object.Properties[].Value.Widgets and NavigationList Items[].Widgets — not
Templates — so nothing inside a template reached the widgets table, and the refs
projection is built from that table. Measured on a list view holding two
templates, two data containers and two buttons: 1 row indexed, 5 expected.

The same omission had a second, opposite effect the report did not find.
Templates was missing from widgetChildKeys too, so the list view's own ref scan
descended into its templates, collected each one's Entity, and returned the
lexicographically smallest candidate — a specialization sorting before the list
view's real datasource silently replaced it. Measured: a list view over
MyModule.BaseItem with a template for MyModule.AAA_First recorded its datasource
entity as AAA_First. The wrong entity, decided by alphabetical accident.

Walk each template as a container in its own right, so the row it produces
carries the specialization it renders and its children are indexed like any
other nested widgets; and add the key to widgetChildKeys so the parent stops
absorbing them. Both halves are needed: recursion alone leaves the displacement
in place, the key alone hides the contents without indexing them. The two are
sides of one contract, and the two omissions partly masked each other — a stray
ref still leaked through — which is why this read as intermittent.

Items stays asymmetric on purpose: a nav item's own Form/Entity belongs to the
NavigationList because items are not emitted as rows, and the walk already stops
at the item's Widgets.

Controls: with the recursion removed the first test fails naming all four
missing widgets; with the key removed the second fails with EntityRef =
MyModule.AAA_First. Each half fails independently on its own symptom.

No MDL repro script: MDL has no surface for authoring a specialization template
(which is the reporter's separate point), so the unit tests are the repro.

Scoped to the catalog walkers. DESCRIBE PAGE still omits templates, and SEARCH
inherits that because the source table is built from describe output — those
need an entity field on the model type and an MDL surface first.

Refs mendixlabs#940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
… by one already present

CI caught this: `use building block Atlas_Web_Content.List_Cards (datasource: …)`
began failing with "the block has no datasource widget to rebind", on both
engines, in 15c-fragment-bindings-examples.mdl. Bisected to e50ddac — the mendixlabs#941
unification — and reproduced against its parent, which passes.

The rebind expands a block by re-rendering it to MDL through the describe
emitter and re-parsing that text, then looked for a widget already carrying a
DataSource property. A reusable block is a template: its datasource is unbound.
That used to render as the malformed `DataSource: database from ,` — precisely
the output mendixlabs#941 fixed — and the re-parse turned it into a DataSource property,
so the rebind found its target. Emitting nothing for an empty datasource, which
is correct, left it with nothing to match.

So the target is matched by widget type — what can carry a datasource, not what
does. The sibling ActionOverride branch already did exactly this, and its
comment says why: Atlas blocks "ship placeholder buttons with no action". Same
fact about the same blocks; this branch had not learned it.

The wider lesson, now in fix-issue.md: a consumer that re-parses another
component's rendered output silently depends on that output's bugs, so fixing a
renderer can break code that never reads the model. No emitter unit test can see
it — only the integration suite did.

mx check on the rebound page: 0 errors. Control: restore the property-based
predicate and the stock-block rebind fails again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
A List View over a generalization can render a different body per
specialization. MDL had no surface for authoring one, and — worse — no read path
either: DESCRIBE PAGE dropped the templates silently, and because DESCRIBE emits
`create or modify page`, re-executing its output rebuilt the page without them.

Measured on ako/TestApp's Pages.Vehicle_Overview (4 templates, Studio Pro
authored) with the previous binary: describe -> exec left 0 templates. `mx check`
reported 0 errors before and after. The document stays valid, it just holds less,
which is why nothing downstream flagged it.

Syntax — a template is identified by the entity it renders, not by a name:

    listview vehicleListView (DataSource: database from Pages.Vehicle) {
      dynamictext defaultVehicle (...)          -- default body

      template for Pages.Bus   { dynamictext busLabel (...) }
      template for Pages.Truck { dynamictext truckLabel (...) }
    }

Four things worth knowing, each of which bites if missed:

1. modelsdk/gen binds the entity under the SDK name "Specialization"; every
   Studio Pro document stores "Entity". generated/metamodel agrees with the
   documents. Patched as a STORAGE-NAME OVERRIDE on both the encode key
   (initListViewTemplate) and the decode key (InitFromRaw).
2. Order is authored, not derived. TestApp's is Bus, Truck, Car, SUV — neither
   alphabetical nor domain-model order — so a DESCRIBE that sorted them would
   not round-trip to the same document.
3. Inside a template the context object is the specialization, so an attribute
   only that specialization has resolves there. Both the builder and DESCRIBE
   thread that context.
4. FOR is in the parser's `keyword` rule, so the new widgetV3 alternative comes
   FIRST or it collides with Gallery's pre-existing `template <name>` content
   slot. That is a different construct and keeps working; a Gallery slot named
   `for` now needs quoting, the same escape hatch reserved names already use.

Refused rather than written, each naming the problem: an entity that is not the
list view's entity or a specialization of it (it could never match), a second
template for the same entity, and a nested template.

Verified end to end on a real project, both engines: authoring the example
writes three templates with the right entities and `mx check` reports 0 errors;
describe -> exec -> describe on TestApp's Studio Pro authored page is byte
identical with all four templates and their order intact. SHOW REFERENCES TO a
specialization now finds the page, and SEARCH finds content that lives only
inside a template — the catalog's source table is built from DESCRIBE output, so
that half followed from the read path.

Controls: with DESCRIBE's Templates read removed the describe test fails naming
all four missing blocks; with the legacy writer's Entity key removed the writer
test reports keys [$ID $Type Widgets]; with the gen override reverted the
storage-name test reports the property bound as "Specialization".

Refs mendixlabs#940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
ako and others added 4 commits August 20, 2026 23:25
fix(describe): read and render a widget datasource in one place (mendixlabs#941)
Adding a template reuses INSERT INTO with the same `template for` block CREATE
PAGE uses, so a template has one spelling everywhere. Removing one needs its own
form, because a template has no name to put in a widget ref:

    alter page Pages.Vehicle_Overview {
      insert into vehicleListView {
        template for Pages.Motorcycle { dynamictext mcLabel (...) }
      };
      drop template for Pages.SUV in vehicleListView
    };

Naming the list view in the drop is required, not optional: one page can hold
two list views with a template for the same entity.

INSERT is not free despite reusing the existing syntax. INSERT INTO appends to a
container's Widgets, and a list view's Widgets is its DEFAULT BODY — a
Forms$ListViewTemplate is not a widget, so routing one through InsertWidget
appends a non-widget to the widget list and produces a page Studio Pro cannot
open. Templates therefore take a dedicated mutator path, exactly as DataGrid2
columns already do for the same reason. The control makes the failure concrete:
with the routing disabled the list view's default body gains two widgets.

Two new PageMutator methods (InsertListViewTemplates, DropListViewTemplate),
implemented in pagemutator over raw BSON, stubbed in the mock, and refused with a
reason by the MCP backend. Both check the target is a list view, so a gallery's
`template <name>` — a named content slot, a different construct — is not
reachable through them.

Most template edits need none of this. Widgets inside a template are ordinary
named widgets, so `set Content = '…' on busLabel` and `insert after busLabel { … }`
already work and land in the right template. Replacing a whole template is a drop
plus an insert in one block, since operations apply in order.

Refused, each naming the problem: insert before/after a template (they are not
siblings of the body), templates mixed with widgets in one insert (they go to
different arrays), a template for an entity outside the list view's hierarchy, a
duplicate, and dropping a template that is not there — that last one names the
templates that ARE there, because dropping nothing and reporting success is how a
typo in a specialization name becomes a silent no-op.

Verified on ako/TestApp's Studio Pro authored page: inserting Motorcycle and
dropping SUV leaves Bus, Truck, Car, Motorcycle in order with `mx check` at 0
errors, and all five refusals were exercised against the real project.

Controls: with the insert routing disabled the templates land in the default
body; with the drop's not-found check removed a bogus drop reports success; with
the append targeting Widgets the default body grows instead of the Templates
array.

Refs mendixlabs#940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
fix(catalog): index List View specialization template contents
@ako
ako merged commit b763353 into mendixlabs:main Aug 20, 2026
6 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