Skip to content

Sync ako/mxcli: check-time fixes for #1100–#1104, and describe → exec fidelity - #1116

Merged
ako merged 22 commits into
mendixlabs:mainfrom
ako:main
Sep 17, 2026
Merged

ako merged 22 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Ten commits since the last sync, all bug fixes found by working reported issues against real projects and mxbuild. Every fix was measured end to end — mxcli checkexecmx check — with a control that proves the test detects the defect.

mxcli check catches things that previously only mxbuild did

Each of these had the same signature: check clean, exec reporting success, and the failure only at build time — or later.

  • A list operation nested inside another (MDL-LISTOP02, #1101). $n = COUNT(FILTER($reqs, …)) dropped the inner call entirely — list and predicate — and wrote an activity with an empty List, failing the build with CE0012 / CE0096. Measured across every spelling: head/tail/filter/sort, both operands of union, and a bare string literal. sort(filter(…), Attr) was worse than the report — mxbuild aborts with an InvalidOperationException rather than reporting an error, because the sort attribute resolves against the now-absent list's entity.
  • A LIMIT 1 retrieve then used as a list (MDL-RETRIEVE01, #1103). limit 1 compiles to Mendix's "First object" range, so the variable is an OBJECT. DESCRIBE re-emits limit 1 either way, so an object retrieve and a list retrieve are identical MDL text — CE0097 at the far end of a build was the first sign.
  • LOOP and declared variables are typed (#1100). $out = $out + $r/Status inside LOOP $r IN $reqs — an Enumeration concatenated into a String — passed check --references and failed with CE0117, while the same mistake on a parameter was already refused.
  • A .test.mdl file parses as the microflow bodies it is (#1103). The file was handed to the top-level grammar, so RETRIEVE was swallowed as an identifier and the leftover FROM … started an OQL query — the reader was told their retrieve needed a SELECT. Nine of this repo's ten test files reported errors that way, one of them 392. make check-mdl now sweeps them.

describe → exec fidelity

This is how a page is copied or edited, so a property lost on read is a property deleted from the model, with everything clean at both ends.

  • A DataGrid2 column's filter is kept beside its custom content. A column has two widgets-typed slots; the reader took the first one it met rather than keying on the resolved property key, and the writer emits properties alphabetically — so content won and the filter was dropped.
  • A check box's ReadOnlyStyle is written instead of a hardcoded constant. Not cosmetic: the value decides whether a read-only check box renders as the words "Yes"/"No" or as the disabled checkbox glyph.

Read coverage

  • The System module's enumerations are exposed, read-only (#1102). describe enumeration System.WorkflowActivityType answered "enumeration not found" while describe entity printed that very type in the same session, so a System enum's values could only be guessed at until the build rejected a guess.

Test runner

  • A failed mxcli test run says what mxbuild rejected, and leaves nothing behind (#1104). MxBuild puts identical text in Message for every failing build, so the error could not distinguish "your test does not compile" from "an unrelated document is broken" — the real errors were in hand and discarded. Cleanup now removes every generated MxTest.Test_*: the names are positional and reused per file, so keying cleanup on the suite left flows behind whenever a later run had fewer tests than an earlier one, and one leftover that does not build fails every later run of every test file.

Docs

Findings recorded for the above, plus corrected DataGrid2 column-filter guidance and CLAUDE.md pointed at the engine that exists.


Note on two commits. Delete sdk/mpr — the legacy engine is gone and Point CLAUDE.md at the engine that exists are already upstream under different SHAs (applied via #1114), so git cherry marks them as equivalent. They appear in the range but contribute no new content

claude and others added 21 commits September 16, 2026 07:12
A test block is a microflow body — that is what the runner turns it into —
and `mxcli check` and the LSP were handing the file to the top-level
grammar instead. DECLARE is not a top-level statement, so the parser
resynced; RETRIEVE is a non-reserved keyword, so it was swallowed as an
identifier; and the leftover `FROM …` started an OQL query, whose follow
set is {GROUP_BY, SELECT, HAVING}. The reported error therefore told the
author their RETRIEVE needed a SELECT — on a statement
`mxcli syntax microflow.retrieve` prints as its own example
(mendixlabs#1103).

Not a corner case: the VS Code extension binds MDL to `.mdl`, which
`.test.mdl` matches, so every test file open in the editor was a wall of
squiggles. 9 of this repository's 10 test files reported errors this way,
one of them 392; all 10 now report 0.

testrunner.CheckSource renders each block as the microflow it becomes,
padded so the body keeps its SOURCE line numbers — wrapper fragments go
on the lines the doc comment and the '/' separator occupied, so a
diagnostic's line:col is the author's with no mapping table to drift.
Everything downstream then applies unchanged: an uncompilable body, an
unusable @expect or @verify (MDL-TEST01), and the semantic rules.

`make check-mdl` sweeps test files too, which is what keeps this true;
`.fail.test.mdl` names one whose annotations are deliberately unusable,
and the two existing fixtures of that kind are renamed to match.

Control: stubbing IsTestFile back to false reproduces the reporter's
message verbatim, `line 6:76 mismatched input 'LIMIT' expecting
{GROUP_BY, SELECT, HAVING}`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
`retrieve $x from Mod.E … limit 1` is compiled to Mendix's "First object"
range, so $x is an OBJECT and not a one-element list. That is deliberate
and documented — but nothing between the author and mxbuild said so:
`check --references` passed, and DESCRIBE re-emits `limit 1`, so an
object retrieve and a list retrieve are identical MDL text. The first
sign was CE0097 "The selected 'x' variable must be of type List" at the
far end of a build, and inside a .test.mdl file not even that — the
injected test simply failed to build (mendixlabs#1103).

MDL-RETRIEVE01 tracks the variables a limit-1-no-offset retrieve binds,
in statement order so a rebinding clears them, and flags a later list use
— list operation, aggregate, loop, ADD/REMOVE. Keyed on exactly the
condition the writer uses, because a check that disagrees with the writer
it describes is worse than no check.

The confusion is structural rather than careless: the same word means the
opposite on `import from mapping`, where `first` binds the object and
`limit 1` a one-element list. The message therefore names both working
spellings instead of only refusing.

Behaviour is unchanged; only the silence is fixed. Measured on the whole
mdl-examples corpus (558 scripts, `make check-mdl`): no new failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Two halves of mendixlabs#1104, both about a failed `mxcli test` run
telling the reader nothing and then poisoning every later run.

MxBuild puts the same text in Message for every failing build, so
"build failed: The project cannot be deployed, because it contains
errors." cannot distinguish "your test does not compile" from "an
unrelated document is broken". The parsed problems were in hand and
discarded one line before they were needed: --attach and every --watch
rebuild built their error with fmt.Errorf, while the attribution that
turns a build error into the failing test's row was wired only into the
--local boot. Both now return a *docker.BuildFailedError and go through
one shared resultsForBuildFailure, so an error in a generated test
microflow becomes that test's ERROR row and one in the project names its
document. The hint no longer repeats the errors the error already
renders, and names a leftover generated flow with the DROP that removes
it.

Cleanup now removes every generated MxTest.Test_* the project holds
rather than the current suite's. The names are positional — Test_test_1,
_2, … from the test's index in its file — and every test file reuses
them, so a run with fewer tests than the last one left the surplus
behind; under --attach the MxTest module always pre-exists (the dev loop
installed it), so the whole-module drop never fired. One leftover that
does not build then failed every subsequent run of every test file.
Keying on the suite was wrong the other way too: it issued DROP for flows
a part-way injection never created, and those failures made cleanup
announce "the project has been left modified" for a project it had just
cleaned. What cleanup genuinely cannot remove is now named, with its
DROP.

Measured end to end: with a planted bad MxTest.Test_test_2, a known-good
one-test suite failed and left it in place before, and after the fix run
1 fails and cleans while run 2 passes. Reverting buildFailure() to
fmt.Errorf reproduces the reported sentence verbatim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
CLAUDE.md gains the `limit 1` idiom, because it is a cardinality change
with identical source text on both sides and LLM-generated MDL walks into
it; and the test-loop bullet now carries the three things a failed run
must do.

MDL_QUICK_REFERENCE and `mxcli syntax microflow.retrieve` spell out which
LIMIT forms bind an object and which a list, and name the opposite
meaning the same word has on `import from mapping` — the contradiction is
what makes the mistake reasonable, so saying it is worth more than the
rule alone.

The test-microflows skill gains "check a test file before you run it",
which is now possible and much faster than a run.

Three findings records: two in cmd/mxcli (the format a tool owns being
handed to another tool's parser; deriving what to clean up from what
exists rather than from what you meant to create) and one in mdl/executor
(a clause that changes a variable's cardinality needs a check keyed on
exactly the writer's condition).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
163 files, 41,674 lines. This finishes
docs/plans/2026-09-14-retire-legacy-engine.md.

Zero importers was not the same as safe to rm -rf. Two live dependencies
survived, and neither is visible to a check written against the parent package's
import path. sdk/mpr/version had six importers, two of them shipping code
(cmd/mxcli/docker/build.go and patch.go) — a subpackage is a different import
path. And cmd/mxcli/docker/update_widgets_test.go read
sdk/mpr/testdata/v1-project by filesystem path, an os.DirFS string rather than
an import.

So: before deleting a package, search for three things — its own import path,
its subpackages' paths, and its directory as a literal string (testdata,
go:embed, scripts). The last two are invisible to any importer census.

The six importers went to mdl/types, not to modelsdk/mpr/version.
sdk/mpr/version.ProjectVersion is `type ProjectVersion = types.ProjectVersion`,
an alias, so types.ProjectVersion is the same type — while modelsdk/mpr/version
declares a duplicate struct that would have been a different one. Read the
declaration, not the name. The v1 fixture moved to modelsdk/mpr/testdata/.
Everything was repointed and proven green with the package still present, which
separates "the repoint was wrong" from "the deletion was wrong".

Two measurements worth keeping. The shipped binary is identical in size before
and after, so the linker had already dropped the package: this removes source
weight, not runtime behaviour. And sdk/widgets fell to zero importers as a side
effect but is deliberately kept — modelsdk/widgets/dirty_template_test.go reads
sdk/widgets/templates/mendix-11.6 by path. Same trap, caught by grepping the
directory name rather than the import.

The import guard added in the previous slice is removed: with the package gone,
an import is a compile error, strictly stronger than a test asserting it.

Gates: build, vet (incl. -tags integration), go test ./..., check-mdl (560),
check-findings, and the full repo-wide integration suite: exit 0, no failures.
docker check and check --post-migration are byte-identical to the pre-deletion
binary.

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

A column has two Widgets-typed slots — `content` and `filter` — and the
write path fills both. The reader took the FIRST widgets-typed property it
met instead of keying on the resolved property key, and the writer emits
column properties alphabetically, so `content` won and the filter was
dropped. `describe → exec`, which is how a page is copied or edited, then
deleted the filter, with mxcli check, exec and mx check clean at both ends.

A filter-only column round-tripped by accident: with `content` empty the
filter landed in the content list and was re-emitted in the column body,
where the builder routes it back to the filter slot by widget type. That is
why the common shape looked correct and only the combination broke.

Route on the property key instead, keeping the first-wins heuristic only for
a document whose keys do not resolve, and emit both lists in the column
body. Measured on 11.6.6: the round trip now reports `Unchanged page` —
the description reproduces the stored document — where it reported
`Replaced page` and lost the filter before.

Fixes #489. Reported upstream as mendixlabs#1111, where the
describe gap read as a missing DataGrid2 capability; the capability is
there — the same column renders checkbox cells and a working filter in a
browser on 11.6.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5JMoL3NegeVcn5SwSCqFK
Deleting sdk/mpr left the always-in-context routing document pointing at files
that are gone. CLAUDE.md is where a session looks first, so a dead path there
does not just go stale — it sends the next reader somewhere that cannot be
opened, and every one of these was a routing instruction rather than prose.

Eight sites. The architecture tree listed sdk/mpr's reader/writer/parser/utils
and no modelsdk at all, which had it describing the deleted engine as the format
layer and omitting the real one. The BSON storage-name procedure said to verify
against sdk/mpr/parser_microflow.go; that now points at modelsdk/codec and
modelsdk/gen. The write-choke-point list named both engines' files — there is one
engine, so the list is now one entry and says so. The test-first checklist sent a
parser test to sdk/mpr/ and a backend mutation test to mdl/backend/mpr/, neither
of which exists. Useful Files offered sdk/mpr/parser.go and writer_widgets.go.

Two checklist items were rules about a package that no longer exists, so they are
restated as rules about the engine that does: "no sdk/mpr write imports in the
executor" becomes "no engine internals in the executor" (modelsdk/mpr,
modelsdk/codec, modelsdk/gen), with the note that a missing backend method gets
implemented rather than bypassed — which is what the last five slices kept
running into. The shared-types rule described sdk/mpr re-exporting aliases; it
now states the rule directly and cites modelsdk/mpr/version.ProjectVersion as the
cautionary case, since that one duplicates types.ProjectVersion instead of
aliasing it and the two print under the same name.

Every path the file now names was checked to exist.

Docs under docs/ are deliberately untouched: they describe what the code used to
do, and ADRs are immutable by convention.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
`ReadOnlyStyle:` parsed, passed check, and reached a writer that hardcoded
"Inherit". DESCRIBE reads and emits the property, so describe → exec on a
Studio Pro-authored page silently downgraded Control to Inherit.

Not cosmetic: the value decides how a READ-ONLY check box renders — Text
gives the words "Yes"/"No", Control gives the disabled checkbox glyph. In a
DataGrid2 cell that is the whole of "show a Boolean as a checkbox", and it
was unreachable from MDL.

pages.CheckBox carries the value, the builder canonicalises it (Inherit /
Control / Text, case-insensitive in, Mendix casing out) and refuses an
unknown member — mxbuild tolerates one and Studio Pro then cannot open the
project — and the codec writes Inherit when unset, so a script that never
mentions it produces the document it always did.

Verified on 11.6.6: the page mxcli writes from `ReadOnlyStyle: Control` is
semantically identical to the hand-patched document that was checked in a
browser (re-authoring it reports `Unchanged page`), renders 9 disabled
checkbox cells, and the column's drop-down filter still cuts 9 rows to 3.
`mx check` 0 errors; 562 MDL example scripts unchanged.

Scope is the check box, matching what DESCRIBE reads back today. The other
input widgets still write a hardcoded Inherit and are not read back either;
the retired sdk/mpr serializer (no live callers) is left alone.

Fixes #490.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5JMoL3NegeVcn5SwSCqFK
Two claims in the syntax topic and the quick reference were wrong, and both
pushed users away from a combination that works:

- "A Boolean column takes no filter at all." The drop-down filter's own
  widget XML declares its attribute types as Enum AND Boolean, and a Boolean
  column filters Yes/No — measured in a browser on 11.6.6.
- Neither said a column can hold a custom-content widget and a filter at the
  same time. `content` and `filter` are separate Widgets-typed slots with no
  dependency on `showContentAs`, so a read-only checkbox cell and a
  drop-down filter live in the same braces.

Also documents `ReadOnlyStyle:` where it matters — it is what makes a
read-only check box render as the glyph rather than the text Yes/No.

The first claim is the likely origin of mendixlabs#1111, which asked
Mendix for a new Boolean column type to do something mxcli could already do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5JMoL3NegeVcn5SwSCqFK
`$n = COUNT(FILTER($reqs, $currentObject/Status = Mod.E.Approved))` passed
`mxcli check`, execed with "Created microflow", wrote zero list properties,
and then failed the build with CE0012 "The 'List' property is required."
(mendixlabs#1101).

MDL's expression grammar makes list operations look composable. Mendix's
model is not: each is a separate ACTIVITY whose list is stored as a VARIABLE
reference, with no slot for a nested computation. buildSetStatementNode took
every list operand through extractVariableName, which reads *ast.VariableExpr
and *ast.IdentifierExpr and returns "" for anything else with no default
branch — so the inner call was discarded, list and predicate together, and
the activity written with an empty AggregateVariableName. `describe` read the
result back as `$n = count($)`.

Measured on mxbuild 11.6.6, one microflow per project:

  count(filter(...))       CE0012
  head/tail/filter(...)    CE0096, the list-operation flavour of the same
  sum(filter(...), 1)      CE0012 + CE0117
  union($l, filter(...))   the SECOND operand lost the same way
  sort(filter(...), Name)  mxbuild ABORTS with InvalidOperationException —
                           the sort attribute resolves against the absent
                           list's entity, so the document cannot be loaded
                           at all. No error code, no line.
  count('nonsense')        CE0012 — nesting is not required to lose the list

The conversion now records what it could not reduce to a variable
(ast.UnresolvedOperand) and MDL-LISTOP02 refuses it at check time, naming the
operand and printing the two-statement rewrite. Keying on the conversion's
RESULT rather than on the argument's node type is what makes it uniform:
buildSetAggregate resolves an attribute path that extractVariableName cannot,
so a node-type predicate would differ per arm and drift apart again — which
that function's own comment already warns about. The switch moved into
buildListOrAggregateStatement so all fourteen arms share one tail.

Refusing rather than materialising an implicit variable covers every spelling
from one rule, including the literal operand that no materialising would fix,
and needs no project, so plain `mxcli check` reports it.

Verified:
- the printed remedy, pasted verbatim: check clean, exec clean, mxbuild 0 errors
- the reporter's workaround and the nested form in ONE project: exactly 1 error
- 584 corpus scripts: 1 flagged, and it is the new bug test (positive control)
- guard stubbed: all 9 new tests fail, the accept-control stays green

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
`describe enumeration System.WorkflowActivityType` answered "enumeration not
found" while `describe entity System.WorkflowActivityRecord` printed
`ActivityType: Enumeration(System.WorkflowActivityType)` in the same session, so
a System enum's values could only be guessed at until the build rejected a guess
with CE1613 (mendixlabs#1102).

`modelsdk/meta.SystemEnumerations` — all 15 enumerations with their values — had
been in the tree since mendixlabs#889 with zero non-test consumers. The System module is
not stored in the .mpr (the string `WorkflowActivityType` occurs in 0 of 370
mprcontents units), so its entities, associations and Java actions each have a
Build* helper appended to a listing; the enumeration half had the data and no
wiring. Adding `BuildSystemEnumerations` and appending it in `ListEnumerations`
fixes four reported symptoms at once, since the catalog builder reads through the
same backend:

  - `describe enumeration System.X` reports the values
  - `show enumerations` lists them (15 rows)
  - `check --references` no longer rejects an attribute typed against one — a
    false positive that blocked valid scripts, the mendixlabs#1071 direction of this gap
  - `CATALOG.attributes ⋈ CATALOG.enumerations` resolves 19 of 19 on the
    expr-checker fixture, where it resolved 0

Making them visible also made them addressable by the write paths, and the
System module has no stored unit for a write to live in. `CREATE ENUMERATION
System.BrandNewThing` already reported success and wrote a unit whose
ContainerID was the synthetic module id 00000000-…-0001, present in no Unit row
— an orphan with a dangling parent, measured at 369 → 370 units. So every
enumeration write naming System is now refused before the backend is touched:
create, alter, drop, move (both ends), rename, and `DROP MODULE System`, whose
cascade would otherwise warn once per synthesized document. DESCRIBE emits `--`
comment lines for these rather than a `create or modify` statement the guard
would reject, as DESCRIBE BUILDING BLOCK does for the other read-only doctype.

The system-module skill's enumeration section had drifted into wrong casing
(`created`, `end`, `single`, `microflow`, `error`, `user`, `external`) and was
missing three enumerations including WorkflowActivityState — so a developer
copying `created` out of it hit the very CE1613 the section existed to prevent.
Regenerated from the table and pinned by a test. The write-workflows skill
documented the bug as a permanent limitation with a workaround; updated.

Tests: the resolvability guard the entity half has had all along
(TestModelerSystemEntities_HaveResolvableGeneralizations) now has its
enumeration sibling. Each half of the fix was reverted to confirm its tests fail
with the reported symptom, and the doc guard was checked against the original
drift. Controls cover stored enumerations still listing, resolving by ID, user
enumeration writes still working, and a user enumeration still describing as
re-executable MDL.

Not fixed, filed separately: `search` still does not match these (its index is
built from value captions, and the meta table carries names only — defaulting a
caption to the value name would be inventing text indistinguishable from a
developer's own), and enum-split CASE values are validated against the
enumeration for no module, user or System.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DHM1in4g85vBtHnbjSejSd
Delete sdk/mpr — the legacy engine is gone
…s#1100)

`$out = $out + $r/Status` inside `LOOP $r IN $reqs` — an Enumeration
concatenated into a String — passed `check --references`, was written by
`exec`, and failed the build with CE0117 at the Change variable activity,
while the same mistake on a parameter was refused as E004.

The report reads as "the checker is skipped inside a LOOP body". It is
not, and the control says so: `'status=' + $T/Status` on a *parameter*,
written one line inside the loop, was refused before this change. The
body was always walked; the variable scope had two holes, and both had
to close before the reported script reported anything.

  1. `buildVarEntityScope` recorded CREATE, database RETRIEVE and
     parameters but never `LoopStmt.LoopVariable`, so `$r/Status`
     resolved to no attribute and inferred KindUnknown — which every
     rule tolerates by design.
  2. `CheckAdapter` never set `Context.Scope` at all, so a
     `DECLARE $out String` was Unknown too. E004 needs BOTH operands
     typed, so closing (1) alone still reported nothing. That is also
     why `$out = $out + $Req/Status` was silent with no loop in sight;
     the report's case A hides it by putting a literal on the left.

The list sources a loop can iterate are typed with it: a database
retrieve, an association retrieve (far end resolved through the
association index, which is why parameters are seeded before the body
walk), a CREATE LIST, and the list operations that carry their input's
element type through. Two other block-scoped positions the report asked
about are covered: an ON ERROR handler body, which was not walked at
all, and a FIND/FILTER predicate, where `$currentObject` is bound to the
element type of the list under test. A bare attribute name in a
predicate still resolves to nothing — binding bare names would change
what a bare identifier means everywhere in an expression.

The DataTypeKind and ON ERROR tables now live once in adapters with
mdl/executor delegating: `validate_member_refs.go` already typed loop
variables from the list and the expression checker's walk did not, which
is the drift these two copies invite.

Measured: exec-then-type-check over 591 mdl-examples scripts gives 11
violations before and 11 after, same rules on the same lines. The sweep
earned its keep — a first cut reported Mendix's STRING
`find($Hay, $Needle)` as a non-Boolean predicate, because the visitor
still builds it as a list operation and the flow builder disambiguates
it later (ledger #63); a predicate is now checked only when the input
list's element entity is known, applying the same disambiguation.

Controls: each of the four scope sources reverted in turn fails a
distinct test with the reported symptom (empty violations).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012af91Z2nhirBJyY9MLRJqp
Refuse a list operation nested inside another (MDL-LISTOP02)
fix(exprcheck): type LOOP variables and declared variables (mendixlabs#1100)
Keep a DataGrid2 column's filter on describe, and write a check box's ReadOnlyStyle
fix(enumerations): expose the System module's enumerations, read-only
@github-actions

Copy link
Copy Markdown

AI Code Review

Review Summary

This PR removes the legacy sdk/mpr engine (already deprecated upstream) and implements eight bug fixes to make mxcli check catch issues that previously only mxbuild would detect. The changes include:

  1. Removing legacy MPR engine code (verified as already upstream via PR Delete sdk/mpr — the legacy engine is gone #1114)
  2. Fixing nested list operations dropping inner calls (MDL-LISTOP02)
  3. Fixing LIMIT 1 retrieve type confusion (MDL-RETRIEVE01)
  4. Adding variable typing in LOOP constructs
  5. Fixing .test.mdl file parsing as microflow bodies
  6. Preserving DataGrid2 column filter alongside custom content
  7. Correcting checkbox ReadOnlyStyle handling
  8. Exposing System module enumerations as readable
  9. Improving test runner error reporting and cleanup
  10. Adding comprehensive test cases for each fix

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

  • The PR description mentions 10 commits but notes 2 are already upstream - while explained, this creates slight confusion about what's actually new in this PR
  • Some test file removals (legacy engine tests) could be verified for completeness, but given the upstream verification, this is acceptable

What Looks Good

  • Thorough bug fixing: Each fix was measured end-to-end with controls (mxcli checkexecmx check)
  • Appropriate test coverage: Added MDL test scripts in mdl-examples/bug-tests/ for each issue (1100-1104) plus test runner fixes
  • Architecture alignment: Properly removes legacy engine while maintaining modelsdk as the sole engine per ADR-0004
  • Documentation updates: CLAUDE.md updated to point to current engine
  • Consistent with project goals: Directly addresses the core issue where check failed to catch build-time errors
  • Clean code removal: Eliminates 15+ legacy engine files reducing attack surface and maintenance burden

Recommendation

Approve. The PR successfully removes deprecated legacy


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

@ako
ako merged commit 5228405 into mendixlabs:main Sep 17, 2026
5 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