Skip to content

Sync ako/mxcli: pages, check/exec agreement, microflow error events, OData - #1141

Merged
ako merged 28 commits into
mendixlabs:mainfrom
ako:main
Sep 18, 2026
Merged

ako merged 28 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Fourteen commits from ako/mxcli:main that are not yet upstream. Each carries
its measurement in the commit body; the grouping below is by what was wrong,
not by file.

Everything here was validated against a real project — mxcli check, exec,
mx check on a blank Mendix 11.12.x app — and each fix has its control
recorded (revert the fix, see the reported symptom return).

Pages and widgets

  • Author a List View's search barsearch by on the data source. "Search
    attributes" had no MDL spelling at all: SearchAttributes: on the widget was
    MDL-WIDGET07 ("not recognized and will be silently dropped on write"), so the
    search bar could only be configured in Studio Pro.
  • Refuse search by where it cannot be stored. Found while reviewing the
    commit above: the clause hangs off the shared database-source rule, so it
    also parses on a gallery and a data grid, where only
    Forms$ListViewXPathSource declares Search and nothing writes it. check
    passed and the clause went nowhere.
  • Write the on-click a list view and the image widgets already model.
    MDL-WIDGET23 called this a missing writer and suggested wrapping the widget
    in a container. The warning was wrong about the cause — the model carried the
    field and the writers already serialised it; only the three builders never
    read the action off the AST.
  • A list view template must be a STRICT specialization. template for <the list view's own entity> passed check and failed the build with CE0543. The
    guard existed and was one case too generous.
  • Stop claiming a List View's Editable is dropped (MDL-WIDGET20). Every
    clause after the "but" was wrong, and the suggestion was addressed to a
    button — the tell that the branch was written for a different widget.
  • Correct the List View design-property count — six, not three.

mxcli check agreeing with exec

  • Read the same widget registry exec does. check -p --references refused
    MDL that describe page had just emitted (htmlelement, attribute,
    tagcontentcontainer "is not a widget in this project", MDL-WIDGET25, exit 1)
    while exec --no-check wrote it without complaint.
  • Resolve design properties on ALTER STYLING, and refuse a flat multi-select
    value. ALTER STYLING is the one statement whose entire job is writing design
    properties, and the one statement MDL-WIDGET11 never looked at — so it passed
    check and failed the build with CE6083.
  • Name the route a built-in widget's SET dead-ends into. The old message
    ("widget has no pluggable Object") names nothing the author wrote, cannot be
    made true by editing the script, and omits the remedy: design properties on a
    built-in widget are written by ALTER STYLING.

Microflows

  • Refuse raise error; outside an ON ERROR handler (MDL084). It passed
    check, was written by exec, and the build then failed CE0710 — Mendix's
    error event re-raises the error being handled, so one is legal only where an
    error is in scope.

Integration

  • Type Edm.TimeOfDay, and refuse an external action Mendix cannot call
    (CE7252 cannot be resolved from mxcli — no command to refresh OData action fingerprints #1089). CE7252 survived DROP + CREATE OR REPLACE MICROFLOW and was reported
    as a stale parameter fingerprint needing a REFRESH command. No fingerprint is
    stored — Mendix re-derives the alignment from the cached contract on every
    build — so the recreation really does rewrite everything, and its failure
    means the action is not callable at all.

Catalog and docs

claude and others added 28 commits September 18, 2026 05:16
`mxcli check -p --references` refused MDL that `describe page` had just
emitted, while `exec --no-check` wrote it without complaint. On a blank
Mendix 11.12.2 project with no .mxcli/widgets:

    check --references -> `htmlelement` is not a widget in this project        [MDL-WIDGET25]
                          `attribute` is not a widget in this project          [MDL-WIDGET25]
                          `tagcontentcontainer` is not a widget in this project [MDL-WIDGET25]
                          exit 1, so exec refused to run the script
    exec --no-check    -> info: updated widget definitions for Repro1135.mpr
                          Created page MyFirstModule.HtmlDemo

check is meant to be the strict gate and exec the thing that runs; here it
was inverted, and the script it blocked was one describe had just written.

The two read different registries. pageBuilder.initPluggableEngine calls
RefreshStaleWidgetDefinitions before LoadUserDefinitions, so exec generates
.mxcli/widgets/*.def.json from the project's installed .mpk on its way past.
LoadWidgetRegistry -- check, lint and the LSP -- called only
LoadUserDefinitions, so a project that had never run `mxcli widget init`
knew the nine embedded definitions and nothing else. The
`pluggablewidget '<id>'` branch of validateWidgetKind has packageInstalledFor
as its escape hatch; the generic-MDL-name branch has none.

LoadWidgetRegistry now refreshes stale definitions the same way, best-effort
-- a project whose definitions cannot be written gets the registry it got
before, which beats failing a check over a cache.

Control (mdl/executor/validate_widget_kind_uninitialized_test.go): before
the fix the test reports the three names above verbatim; a typo
(`htmlelemnt`) in the same project is still MDL-WIDGET25 after it, and now
suggests `htmlelement`, which it could not while the candidate list was the
nine embedded widgets.

The bug self-heals -- the first exec writes the definitions and every check
after it passes -- which is why it reads as flaky. Reproduce with
`rm -rf .mxcli/widgets` or it is invisible.

Refs: mendixlabs#1135

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
`alter page … set '<design property>' = off on <native listview>` failed
with

    property "Remove empty text" not found (widget has no pluggable Object)

"pluggable Object" is not something the author wrote, cannot be made true by
editing the script, and is not the whole truth: design properties on a
built-in widget are written by ALTER STYLING, which the message never
mentioned. It now says so, naming the widget and the property, so the reader
has the statement rather than a bug report.

A Forms$Appearance and no pluggable Object is exactly a built-in widget,
which is when the advice applies. A pluggable widget keeps the error naming
its own declared keys -- redirecting a mistyped pluggable key to ALTER
STYLING would point at a command that cannot write it either (control:
TestSetWidgetProperty_PluggableWidgetKeepsItsOwnError).

Measured on a blank 11.12.2 project, and worth recording because it inverts
the report: the three names in it are NOT ListView design properties in the
Atlas shipped with Mendix 11.
themesource/atlas_core/web/design-properties.json declares Style, Hover
style and Row size, and nothing else. ALTER STYLING writes the reported key
happily and mxbuild then refuses the project:

    alter styling … set 'Remove empty text' = on;  -> mx check: 1 error,
      [CE6083] "Design property Remove empty text is not supported by your
      theme." at List view 'lvThings'
    alter styling … set 'Row size' = 'Small';      -> mx check: 0 errors

So refusing the write was right and only the reason was wrong, which is why
this is the message and not write support.

Left open deliberately, and noted in the bug test: MDL-WIDGET11 ("design
property not defined for this widget type") covers CREATE PAGE / CREATE
SNIPPET / ALTER PAGE INSERT|REPLACE and not ALTER STYLING, whose widget type
lives only in the stored document -- so an unsupported key there is silent
until mxbuild says CE6083. Resolving it needs a third $Type-to-registry
mapping and belongs in its own change rather than bolted on here.

Refs: mendixlabs#1135

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
The bug test and the finding both said the ALTER STYLING design-property
gap was "tracked separately" without saying where, which is how a note goes
stale. It is #509.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
…cannot call (mendixlabs#1089)

CE7252 "The parameters for remote action '<x>' have changed" survived DROP +
CREATE OR REPLACE MICROFLOW and CREATE OR MODIFY EXTERNAL ENTITIES, and was
reported as a stale parameter fingerprint needing a REFRESH ODATA CLIENT
command. No fingerprint is stored — Mendix re-derives the alignment from the
consumed service's cached contract on every build — so recreating the microflow
really does rewrite everything the call holds, and its failure means the rewrite
was equally wrong, or the action is not callable at all.

Both turned out to be true, on different types. Measured on mxbuild 11.12.0, one
action per EDM shape written by mxcli into a real app and checked on its own:

  String Guid Boolean Byte SByte Int16 Int32 Int64 Decimal    0 errors
  Double Single Date DateTime DateTimeOffset                  0 errors
  an EnumType, an EntityType, Collection(EntityType)          0 errors
  Edm.TimeOfDay                                               CE7252 / CE7269
  Edm.Duration Stream Binary Geography*                       CE7255 (+CE7253)
  a ComplexType, a TypeDefinition, Collection(Edm.*)          CE7255 (+CE7253)

Edm.TimeOfDay is the only type that failed without Mendix also calling it
unsupported, which is what identifies it as mxcli's gap rather than the
platform's: it was unmapped, so the call was written with no ParameterType or no
VariableDataType, and neither field is reachable from MDL — both are derived from
the contract, never typed by the developer. It is now DateTime.

Everything Mendix answers with CE7255 is beyond any write. Those statements
executed silently and left a project that could not build, which is the dead end
in the report; they are now refused, naming CE7255 and saying outright that no
MDL clears it. Edm.Binary loses its mapping for the same reason — it mapped
cleanly to DataTypes$BinaryType and Mendix rejects the action regardless, so a
kind there only routed it past the refusal.

A third case sat between the two: an entity-typed PARAMETER whose external
entity was never imported. That one is fixable and the remedy is one statement,
which mxcli already named for a RETURN type and not for a parameter — the
parameter side being the one that reports as CE7252.

The rule is applied by `mxcli check --references` and by the writer, from one
function, as CheckLayoutPlaceholderNames already is. Only the checker would have
missed the reported workflow: `mxcli exec` does not run the project-resolved
reference pass, so the statement would still have executed.

An enum-typed parameter builds at 0 errors with no type written at all, so
"mxcli could not name a Mendix type for it" is not on its own grounds to refuse;
the accepts-what-builds test carries that control.

Controls: reverting the TimeOfDay mapping takes the probe app 0 -> 2 errors,
CE7252 on the parameter and CE7269 on the return, the reported symptom verbatim;
stubbing the type check restores the silent write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TH4k6nnB86KpiuWU9x68Ba
An earlier commit on this branch said Atlas declares "Style, Hover style and
Row size, and nothing else" for a List View. That came from reading the
`ListView` key of themesource/atlas_core/web/design-properties.json
directly, and it misses the `Widget` group, which applies to every widget.
Measured on 11.12.2:

    $ mxcli -p App.mpr -c "show design properties for listview"
    From: Widget (inherited)   Spacing, Align self, Hide on
    From: ListView             Style, Hover style, Row size

Six. The conclusion the number was cited for is unaffected — none of the six
is 'Remove empty text', so CE6083 still says mxcli was right to refuse the
write and wrong only in the reason it gave.

But the number is load-bearing for the follow-up in #509: someone
building the MDL-WIDGET11 pass for ALTER STYLING from the wrong three would
reject Spacing, Align self and Hide on as unknown keys.
ThemeRegistry.GetPropertiesForWidget already prepends the inherited group,
so the existing validator is correct as written and only the prose was
wrong. Both notes now say to count with `show design properties`, never from
the JSON key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
MDL-WIDGET20 warned that `Editable` on a list view or a grid column is
"silently dropped on write and the widget stays enabled". Every clause after
the "but" was wrong, and the suggestion is addressed to a button --- the
tell that the branch was written for a different widget.

Measured on 11.12.2: exec writes it, `describe page` reads back
`Editable: true`, and mxbuild reports 0 errors.

Two different Mendix properties were conflated. editableWidgetTypes is the
set of Pages types carrying Editability / ConditionalEditabilitySettings,
which is correct for the bug the rule was written for (mendixlabs#928, `editable:` on
a button). Pages$ListView and Pages$GridColumn carry neither: they have a
plain `Editable bool`, a different property meaning "make the inputs INSIDE
me editable". Parsing generated/metamodel for that shape returns exactly
those two --- GridColumn was not in the report and would have been missed by
fixing only what was reported.

So: a second set, not more entries in the first. The bracket form
`Editable: [expr]` (lowered to EditableIf) still warns on both, because
neither type has ConditionalEditabilitySettings and that form genuinely is
dropped. Silencing both would re-create the worse half of mendixlabs#928, where the
shape the docs recommend vanishes without a word.

TestEditableWidgetTypesMatchMetamodel could not have caught this: it
enumerates types carrying Editability, and these carry none.
TestPlainEditableBoolTypesMatchMetamodel is its sibling for the other
property, so a new one fails a test instead of becoming a false positive.

Controls, all passing: the mendixlabs#928 button case still fires; the bracket form on
a list view still fires; input widgets stay clean.

Why it was worth fixing rather than tolerating --- buildListViewV3's own
comment records that a list view without `Editable` renders every input as
`<div class="form-control-static">`, with entity access ReadWrite and
`mx check` at 0 errors. The warning steered authors away from the one
property that fixes a symptom the code itself calls hard to diagnose.

Fixes #510

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
`template for <the list view's own entity>` passed check, was written by
exec, and mxbuild then refused the project:

    [CE0543] "The entity of the list view template is 'MyFirstModule.Vehicle'
             and this is not a specialization of the entity of the list view."

The guard existed and was one case too generous. Both call sites gated on
entityIsOrDescendsFrom(spec, listEntity), which returns true on its first
loop iteration when spec == listEntity. Mendix requires a strict
specialization: the list view's own body already renders an object no
template matches, so a template for the base entity is a second,
unreachable default.

The belief was encoded three times and measured zero times. The guard's own
message offered the case Mendix refuses --- "X is not <listEntity> or a
specialization of it" --- and TestBuildListViewTemplateOnTheListEntityItself
asserted it was "the base case Mendix permits", justified by what
entityIsOrDescendsFrom returns rather than by any run. That test is now
inverted, and carries the measurement.

Measured on a blank 11.12.2 project, list view over MyFirstModule.Vehicle:

    template for       mxcli exec    mx check
    -----------------  ------------  --------
    Car (specialized)  writes        0 errors    <- control
    Thing (unrelated)  refused       ---         <- guard was not missing
    Vehicle (its own)  WRITES        CE0543      <- the bug

All three rows matter: the first is the control against a guard that
refuses everything, the second shows the guard was already there.

entityIsOrDescendsFrom is left alone --- its other callers resolve
association direction, where the reflexive case is correct. The rule lives
in one new method that both CREATE PAGE and ALTER PAGE INSERT/REPLACE call,
since the message and the rule were already duplicated across them and that
is how two copies drift. The syntax help said the same wrong thing and is
corrected.

Not fixed here, and noted in #514: the guard runs at exec, not at
check, so all three rows still report `Check passed!`. A false green, but a
safe one --- nothing is written when it refuses.

Fixes #514

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
fix(odata): type Edm.TimeOfDay, and refuse an external action Mendix cannot call (mendixlabs#1089)
…ries

The catalog's SourceType/TargetType, ElementType and AccessType values were
ALL-CAPS string literals repeated across ~100 emit sites. They are a public
vocabulary — Starlark lint rules filter on them through `reference.source_type`,
`permission.element_type` and `permission.access_type` — but there was nothing
for a consumer, or for documentation, to be checked against.

Name them, and publish the four lists: RefSourceObjectTypes, RefTargetObjectTypes,
PermissionElementTypes, PermissionAccessTypes. The source and target lists differ
on purpose: a LAYOUT or a WIDGET is only ever pointed at, a SCHEDULED_EVENT or a
PROJECT_SETTINGS only ever points, and one list for both would put values in front
of rule authors that their filter can never match.

Mechanical: every literal becomes the constant of the same value, including the
'WIDGET' baked into insertWidgetRefs' SQL, which becomes a bound parameter.
No behaviour changes. This mirrors SourceObjectTypes in builder_source.go, which
exists for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M6ewJmoi3dEjEHDmZoVLt
The bundled write-lint-rules skill is the only documentation of the Starlark
rule API, and six of its example rows named values the API has never returned.
Both failure modes are silent: a rule built from them compiles, runs, matches
nothing and reports a clean pass.

  action_type    listed Mendix's BSON *storage* names — CreateChangeAction,
                 CommitAction, ShowFormAction — where the catalog labels an
                 action with its SDK name (CreateObjectAction, ShowPageAction,
                 ClosePageAction, …), derived from the parsed Go type.
  source_type    documented lower-case ("microflow", "page") against an
  target_type    upper-case vocabulary.
  element_type   the same lower-casing, unreported.
  access_type
  data_type      documented "string"/"integer"/"datetime"; emitted String/
                 Integer/DateTime. The most-used filter in a lint rule.
  ref_kind       documented no values at all.

The last three were not in the report. They are the same defect in the same
tables, found by checking the siblings rather than only the rows that were named.

Correct all six against the functions that produce them, and add a callout above
the tables: what the case convention is, why action_type is never a storage name,
and the two sqlite3 probes that answer the question against a real project.

mdl/catalog/lint_rule_doc_vocabulary_test.go pins each documented value to its
producer, so the tables cannot drift again. action_type needs no list — the label
IS the Go type name, so the test reads the isMicroflowAction marker methods out
of sdk/microflows/microflows_actions.go with go/ast; the others check against the
vocabularies named in the previous commit. Each check fails rather than passes
when its table row goes missing, and each scan carries a vacuity control.

This is the second time this vocabulary has bitten: CONV010's allowlist held the
storage names and was fixed a month ago, pinned by lint_rule_vocabulary_test.go.
It recurred because the documentation the rule author copied from was never
corrected.

Refs: mendixlabs#1027

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

 multi-select value

Two ways a design property reached mxbuild instead of `mxcli check`.

ALTER STYLING (#509) was the one statement whose entire job is
writing design properties, and the one statement MDL-WIDGET11 never looked
at --- ValidateDesignPropertiesForStatement switches on CreatePageStmtV3,
CreateSnippetStmtV3 and AlterPageStmt, and *ast.AlterStylingStmt is not
among them. `set 'Remove empty text' = on` passed check, wrote, and failed
the build with CE6083.

The obvious fix was the wrong one. Mapping the stored $Type to a
theme-registry key would be a THIRD consumer of one concept --- there is
already mdlKeywordToDesignPropsKey, and an unused bsonTypeToDesignPropsKey
with zero non-test callers, hence never validated --- and duplicate
resolvers drifting apart is what this area keeps producing. So the pass
asks the question that IS answerable without opening the document: does any
widget type in the theme declare this key? A key declared nowhere cannot be
right here either, which is the reported case exactly. A key declared for
another widget type is accepted: under-reporting, never over-reporting, the
only safe direction for a check that cannot see what it is judging.

The declared set is built from every group, not one. Three of a List View's
six properties come from the inherited `Widget` group, so a single-type
lookup would report `Align self` as unknown.

Multi-select (#511) turned out not to be a missing capability. The
reference document was already in the project: grep found three Studio
Pro-authored Atlas pages carrying `Hide on`, and decoding one gives

    Forms$DesignPropertyValue  Key: "Hide on"
      Value: Forms$CompoundDesignPropertyValue
        Properties: [ Forms$DesignPropertyValue{ Key: "Phone",
                        Value: Forms$ToggleDesignPropertyValue } ]

which is structurally `Spacing`, which MDL already writes. Measured:
`DesignProperties: ['Hide on': ['Phone': on, 'Tablet': on]]` writes,
round-trips through DESCRIBE and builds at 0 errors. Only the FLAT spelling
was broken --- 'Phone' is a declared option, so it serialized as a plain
Option and mxbuild refused it with CE6084.

So the fix is a refusal naming the spelling that works, not a feature.
`multiSelect` is now parsed (it was read nowhere), the inline path refuses a
flat value, `show design properties` marks such a property instead of
listing it identically to a single-select one, and both paths warn at check
time. ALTER STYLING genuinely cannot express a compound --- a
StylingAssignment carries one flat value and the grammar has no nesting ---
so there it refuses and points at the inline form.

Controls: `Align self` (same declared control type, not multi-select) still
writes a plain option; the compound spelling still writes; a property the
theme says nothing about is still written as asked; an empty or absent
registry reports nothing at all.

Both rule-ID guards --- TestWidgetRuleIDsAreNotReused and
TestRuleIDHasOneOwner --- fail when a rule is raised from a second file.
MDL-WIDGET11/12 are registered as shared rather than renumbered: someone
suppressing MDL-WIDGET11 means both sites.

Fixes #509, #511

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

`raise error;` on a microflow's main flow passed `mxcli check`, was written
by `exec`, and the build then failed with

  [error] [CE0710] "The main flow cannot join an error flow or end in an
                    error event." at Sequence flow

Mendix's error event re-raises the error being handled, so one is legal only
where an error is in scope — inside an `on error { … }` handler. Studio Pro
will not draw the connection from the normal flow to an error event; mxcli
could, and did.

The reported diagnosis (a trailing End event appended because RaiseErrorStmt
never set the builder's "ends with return" flag) is not the cause:
`isTerminalStmt` has treated it as a terminator all along, and the graph mxcli
builds is exactly the one the report asks for — start event, error event, one
sequence flow, no trailing End event, no outgoing flow from the error event.
That shape is now pinned by a test. No wiring makes a main-flow error event
legal, so the fix is a refusal rather than a builder change.

MDL084 walks the main flow — branches, splits and loop bodies included — and
skips `ErrorHandlingClause.Body` subtrees, which is why it cannot reuse
walkBody (that descends into handlers on purpose). Rules go through the same
flow builder and are covered by the same predicate.

Posture follows the mendixlabs#893 CE-gap rules: error severity, so exec's pre-flight
refuses with nothing written, and off execEnforcedMicroflowRules, so
`--no-check` still reproduces the build failure.

Measured on mxbuild 11.13.0, two copies of a blank 11.13.0 app from a 0-error
baseline:

  main-flow raise error (the 4 reported shapes)  ->  4x CE0710
  the same statement inside `on error { … }`     ->  0 errors

Control: with the two call sites stubbed out, the new tests fail with the
reported symptom ("main-flow `raise error;` was accepted").

Repro `mdl-examples/bug-tests/microflow-1030-raise-error-main-flow.fail.mdl`,
control `…-main-flow.mdl`.

Fixes mendixlabs#1030

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSAPMZexnVRhe4UYjsz2P5
`mxcli syntax` advertises, in its own help:

    mxcli syntax workflow user-task targeting     # Drill down to targeting

Handed over as ONE string — a quoted copy-paste, a tool wrapper, `sh -c` —
that answered `Unknown topic: workflow user-task targeting`, spaces and all.
The command joined its arguments on "." and never split them, so the path it
built was the string it was given. The REPL's `help` had resolved multi-word
topics since it was written: one question, two answers, and the CLI held the
weaker copy.

The spaces in the reported message are the whole diagnosis — the command
prints the path it built, and the documented invocation cannot produce one
with spaces in it. Every other line of the report follows: `syntax workflow`
works (one word), `--json` works (a flag is not part of the topic), the three
multi-word forms fail. Reproduced verbatim on the reported v0.20.0 binary and
on main.

The mendixlabs#955 segment-match fallback could not save it either. It passed the
DOTTED path to `BySegmentMatch`, and no segment contains a ".", so it was
silently dead for every multi-word query; it now runs on the last word.

One resolver, `syntax.Lookup`, shared by both surfaces: arguments are split on
whitespace and on ".", then adjacent words are greedily hyphen-joined against
the registry. `workflow user-task targeting`, `"workflow user-task targeting"`,
`workflow.user-task.targeting` and `workflow user task targeting` are now one
query. `resolveHelpPath` is deleted rather than kept in step.

The guard is not the three cases from the report but
`TestEveryRegisteredPathIsReachableBySpelling`: all 164 registered paths,
tried dotted, as separate arguments, and as one string. The registry prints
dotted paths and then tells the reader to drill down with words, so a spelling
that does not resolve is the command contradicting its own output. Control:
stub the whitespace split and it fails with the reported path, spaces and all.

Not widened: the grammar's `helpStatement: IDENTIFIER (identifierOrKeyword)*`
still takes neither a hyphen nor a dot, so `help workflow.user-task` remains a
parse error in the REPL while `mxcli syntax` takes all three.

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

MDL-WIDGET23 reported `Action:` on a listview / staticimage / dynamicimage
as "Mendix does model one, but mxcli has no writer for it", with a
workaround: wrap the clickable part in a container.

The warning was wrong about the cause, and the gap was one line per widget.
The model already carried the field --- pages.ListView.ClickAction,
pages.StaticImage/DynamicImage.OnClickAction --- and the writers already
serialised it: widget_write.go calls clientActionToGen(x.ClickAction) for
the list view, widget_write_legacy_gaps.go does the same for both images.
Only the builders never called w.GetAction(), so the field was always nil.

Measured on a blank 11.12.2 project:

    exec      -> Created page MyFirstModule.OnClick
    mx check  -> The app contains: 0 errors.
    describe  -> listview lvClick (DataSource: …, Action: microflow …)
    re-exec   -> Unchanged page

That last line is the evidence that matters: the describe output rebuilds
the stored document exactly, not merely to something that re-checks clean.

The describe half is included because without it the fix is not a fix. The
write half landed on its own first and the action vanished on the next
describe -> exec: valid BSON, clean build, construct silently gone. Adding
the emitter then printed `Editable: true` twice on every list view, since
the shared property formatter already prints it from the same field --- the
parse side's own comment predicted that, and it only surfaced by
round-tripping a page other than the one under test.

Two metamodel-sync tests asserted the gap still existed, and are inverted
rather than deleted so the metamodel half keeps its guard:
TestClickCapableTypesCarryClickActionInMetamodel still requires those three
types to carry a click action in generated/metamodel, and now requires them
to be absent from clickCapableInMendix.

clickCapableInMendix is empty but kept: the category is real and will recur.
A widget Mendix gives a click action and mxcli does not write earns a
different sentence from one Mendix models no click action on at all, because
the remedy differs. The control for that --- a dataview still reporting
MDL-WIDGET23 --- is unchanged and passing.

Part of #512; search attributes and optimize-for remain open there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
A List View's "Search attributes" had no MDL spelling: `SearchAttributes:`
on the widget was MDL-WIDGET07, "not recognized and will be silently dropped
on write", so the search bar could only be configured in Studio Pro.

    listview lv (
      DataSource: database from Mod.Vehicle
                    where [Brand != '']
                    sort by Brand asc
                    search by Brand, Model
    ) { … }

The syntax mirrors `sort by` --- same position on the database source, same
comma list --- and takes no direction, because a search attribute is only a
name. Reusing the shape is the point: one example teaches both. List view
only, since Forms$ListViewXPathSource is the source type that declares
Search and the grid sources do not.

Pinning the stored shape needed a detour. No marketplace module ships a
populated Forms$ListViewSearch --- all 18 in a blank 11.12.2 app are empty
--- so there is no direct reference. SearchRefs is a list of
DomainModels$AttributeRef, and a Studio Pro-authored Forms$GridSortItem
carries the identical element, so the sibling supplied the reference
(System.Language.Description in Atlas).

That also decided the implementation: reuse attributeRefToGen, the helper
`sort by` already uses. The new list therefore inherits exactly what a sort
bar has always written, including two deviations from Studio Pro that are
consequently not regressions --- typed-array marker 3 where Studio Pro
writes 2, and no `EntityRef: null` key. Both markers are visible side by
side when dumping one project (Studio Pro's 2, mxcli's 3), and compare
CLAUDE.md's note on Forms$Page.AllowedModuleRoles. Changing the shared
helper would alter every existing sort bar, so it is recorded rather than
fixed here.

Full stack, per the checklist: lexer token (and the keyword rule, or
TestKeywordRuleCoverage fails), parser rule, AST field, visitor, semantic
model, builder, writer, and both halves of DESCRIBE.

Measured on 11.12.2:

    exec      -> Replaced page MyFirstModule.SearchBy
    describe  -> … sort by Brand asc search by Brand, Model
    re-exec   -> Unchanged page
    mx check  -> The app contains: 0 errors.

`Unchanged page` is the evidence that matters: the emitted MDL rebuilds the
stored document exactly, not merely something that re-checks clean.

Not fixed, and measured before concluding it was not a new gap: neither
`sort by` nor `search by` is reference-checked, so a nonexistent attribute
passes `check --references` and reaches mxbuild as CE1613. Both clauses
behave identically.

"Optimize for" (ForceFullObjects) is NOT included --- see #512 for
why the caption-to-value mapping could not be established here.

Part of #512

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
Found reviewing this branch, in the commit that added `search by`.

The clause hangs off the SHARED database-source grammar rule, so it parses on
a gallery and a data grid too --- and only Forms$ListViewXPathSource declares
Search, so listViewSourceToGen is the only writer that emits it. On a
gallery, measured on 11.12.2:

    check     -> Check passed!
    exec      -> Created page MyFirstModule.GridSearch
    describe  -> gallery gl (DataSource: database from …, DesktopColumns: 2, …)

The clause is gone. A silent drop, shipped by the commit that was fixing
silent drops --- the round trip that proved the feature on a list view said
nothing about the other widgets the rule serves.

Now refused at build, naming the widget that can store it. An error rather
than a warning: unlike an unrecognised property key, which a newer widget
package might legitimately define, this is decided by Mendix's metamodel and
cannot become valid later.

Controls: a list view keeps it, and a widget with no clause is never
refused --- a guard that rejected either would be worse than the drop it
replaces.

Also from the review: astDesignPropToValue had no production caller left
after the multi-select work, surviving only because four tests called it.
Removed, and those tests now exercise astDesignPropToValueChecked, which is
what production calls --- a wrapper kept alive by its own tests proves
nothing about shipped behaviour.

Three patterns added to the review skill's Recurring Findings table: a
clause on a shared grammar rule written by only one of its constructs; a
coverage test that asserts a gap still exists and so fails on the correct
fix; a describe emitter duplicating a field the shared formatter already
prints.

Part of #512

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
fix: check and exec must read the same widget registry (mendixlabs#1135)
fix(skills): document the lint-rule values the catalog actually emits
…dixlabs#1025)

`mxcli syntax workflow` lists its sub-topics as dotted paths —
`workflow.user-task` — and the REPL's HELP reads the same registry. Copying a
printed path into HELP was a parse error: a hyphen is its own token
(HYPHENATED_ID) and a dot is DOT, and `helpStatement: IDENTIFIER
(identifierOrKeyword)*` named neither. So the CLI took three spellings and the
REPL took one, which is the same split the parent commit closed in the
resolver.

`help workflow user-task targeting`, `help workflow.user-task.targeting` and
`help domain-model.entity` now parse and reach the same page as the words.

The rule shape is load-bearing, not tidy. helpStatement is the grammar's
catch-all — a statement that is just an IDENTIFIER and some words — so
whatever it can swallow, it swallows from the statement that should have had
it. With a leading `DOT?` in the loop, `Sec.ApiUser` became a complete
statement of its own, and `create module role Sec.ApiUser` parsed, with no
parse error to show for it, as CREATE MODULE (named "role") followed by a help
topic: two statements, wrong types, six unrelated security tests red.
Requiring a topic word before any dot leaves `.ApiUser` unconsumable, so the
CREATE MODULE ROLE alternative wins as it did before.

Bisected by shape rather than by reading the ATN: the unused rule alone was
clean, the hyphen alone was clean, the optional leading DOT was the whole of
it. The test that matters is therefore not for the new spelling but for what
the rule must still refuse to swallow —
TestHelpRuleDoesNotSwallowATrailingQualifiedName pins both statements the
regression broke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PuBqbZSg6vuQKQU9RiDn86
fix(microflow): refuse `raise error;` outside an ON ERROR handler (MDL084)
A Selection helper with `renderStyle: 'custom'` has three mandatory child
slots, and Studio Pro fills each with a Forms$StaticImageViewer. DESCRIBE
PAGE could not spell that widget, so it emitted

    -- Forms$StaticImageViewer (imgAll)  -- NOT re-executable: mxcli cannot
    author this widget, so re-running this script would drop it

and replaying the description emptied all three slots — CE0642 "Property
'All selected' is required." x3. The reporter's workaround was an empty
`dynamictext` per slot: structurally valid, icons gone.

Two independent halves were missing, and the second is the one that hides.
`Forms$StaticImageViewer` had no case in the describe parser or emitter,
even though `staticimage` has long been a keyword the executor dispatches
and both writers serialise. And MDL had no spelling for WHICH image it
shows: staticImageToGen wrote Image as the empty string under a comment
saying so, while pages.StaticImage carried a dead ImageID (model.ID) that
nothing set and that named the wrong thing — Forms$StaticImageViewer.Image
is a by-name reference to Images$Image, so the three-part qualified name
Module.Collection.Image is what is stored.

Emitting the keyword alone would have traded a visible note for a silent
drop, which is the trap #512 and mxcli-formula1 FINDINGS §142 each
cost a round on this same widget. For the same reason the size units and
Responsive are wired through: the writer hardcoded both units to "Auto" and
the builder hardcoded Responsive to true, harmless while nothing described
the widget and a silent normalisation the moment one did.

Also emits a pluggable widget's body when a populated child slot is its only
non-default content — that branch was gated on explicit properties, object
lists and actions, so the Selection helper's slots had nowhere to go.

Measured on a blank Mendix 11.12.1 project, mxbuild 11.12.1:

    pre-fix binary, describe -> exec   CE0642 x3, the reported failure
    fixed,          describe -> exec   Unchanged page
    writer reverted to Image: ""       CE0436 "No image selected."

CE0582 ("not supported in React client") is reported for a static image
anywhere on 11.12.1, these slots included. That is Mendix's own deprecation
advice, not an mxcli defect; a new page should use the pluggable `image`
widget, which takes the same `Image:`.

Refs: mendixlabs#1057

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016JgBheMTV6UiQstQ2nLyay
fix(pages): round-trip a static image, reference and all
fix(syntax): a topic resolves however it is spelled (mendixlabs#1025)
@github-actions

Copy link
Copy Markdown

AI Code Review

Review Summary

This PR contains fourteen commits fixing various issues in pages/widgets, microflows, external actions, lint rules, and documentation. The changes are well-scoped, each addressing a specific problem with validation against real Mendix projects. The PR follows the project's contribution workflow and includes thorough testing.

What Looks Good

  • Bug fixes are properly validated: Each fix includes control records (reverting the fix reproduces the symptom) and was tested against real Mendix 11.12.x projects using mxcli check, exec, and mx check.
  • Test coverage is extensive:
    • Adds 12+ new bug-test files in mdl-examples/bug-tests/ covering specific issues (search-by, raise-error, external actions, etc.)
    • Adds executor tests for new validation logic (external actions, microflow raise error, styling validation)
    • Adds catalog tests for lint rule vocabulary documentation
    • Adds syntax tests for multi-word topic handling
    • Includes both positive and negative test cases
  • Changes follow full-stack consistency:
    • For the search-by fix: grammar (MDLLexer.g4 added BY token), visitor (visitor_page_v3.go), executor (widget writers), and backend (widget_write.go)
    • For external actions: new external_action_types.go with corresponding tests
    • For lint rules: documentation fixes in write-lint-rules/SKILL.md with validation tests
  • Documentation is updated:
    • Skills files updated to reflect fixes (write-microflows, write-lint-rules, create-page)
    • CHANGELOG updated
    • MDL quick reference and syntax documentation updated
  • Atomic commits: PR body groups fixes by issue type (pages/widgets, check/exec agreement, etc.), indicating each commit addresses one specific problem.
  • No syntax violations: The search by clause fix uses standard MDL patterns (parenthesized list after verb) and doesn't introduce new CRUD verbs. The added BY token is used consistently for prepositions (like in "search by" and likely "order by").

Minor Issues

  • Token addition: The MDLLexer.g4 adds a BY token. While this appears necessary for the search-by fix and is consistent with existing usage (e.g., in ORDER BY), it would be good to verify it doesn't conflict with other grammar rules. However, given the validation against real projects and the fact that BY is a natural English preposition, this is low risk.
  • Test file organization: Some new test files (like cmd_syntax_test.go) could benefit from brief comments explaining their purpose, but this is minor.

Recommendation

Approve the PR. The changes are well-tested, properly scoped, address real bugs with verification, and maintain the project's quality standards. The fixes improve consistency between mxcli check and exec, resolve microflow validation gaps, and correct widget handling issues. No critical concerns were identified.


Automated review via OpenRouter (Nemotron Super 120B) — workflow source

@ako
ako merged commit cfc4edf into mendixlabs:main Sep 18, 2026
8 of 9 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