Skip to content

Sync ako/mxcli: page widget bindings, OData updatability, authorable microflow header, security and report fixes - #1144

Merged
ako merged 27 commits into
mendixlabs:mainfrom
ako:main
Sep 20, 2026
Merged

ako merged 27 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

15 commits from the fork's main, on top of the last sync (756fa00). Grouped by theme.

Page widget bindings

Four defects in how widgets bind to data, three of them read/write mirror images where fixing one half alone would have turned silent corruption into a build error.

  • An attribute over an association (496a649, closes Input widgets cannot bind an attribute over an association (CE1613), and DESCRIBE drops the hops ako/mxcli#529) — textbox t (attribute: Assoc/Attr) built a flat path and failed CE1613; the association segment was pasted onto the context entity rather than navigated. The same syntax already worked on a DataGrid2 column, so one page could bind an associated attribute in a grid column and fail on the text box beside it. Whether Mendix permits this on a plain text box was the question that blocked the fix, and reasoning could not settle it — a Studio Pro reference does. The read half ignored AttributeRef.EntityRef entirely, so describe → exec over a Studio Pro page rebound the widget to nothing with check clean. Six input widgets now carry the hops through one shared resolver. Verified at 11.14.0: same script, same mxbuild, only the binary differing — CE1613 before, 0 errors after.
  • A flow argument naming a page parameter (0561516, refs Page parameter not wired in nanoflow argument when button is inside a dataview bound to a different parameter CE1571 #1140) — written as a text Expression, which binds nothing, so Studio Pro reports CE1571 on opening the page. Mendix has two slots: a reference to a page/snippet parameter or page variable goes in Variable, a literal or expression in Expression. Measured on Workflow Commons 4.11.0: of 101 mappings, 95 bind through Variable and the 6 Expressions are all Boolean literals — the $-prefixed form mxcli wrote occurs zero times. The read side was wrong in the mirror image and hid it: it looked for a Name key that Forms$PageVariable does not have, so every Studio Pro-authored argument described as absent and a round trip looked lossless. mx check reports 0 errors before and after, so the error really does appear only in Studio Pro, as reported.
  • A show_page argument with no context object (5317ebe, fixes A page-level actionbutton (outside any dataview) with Action: show_page Page(Param: $Var) silently drops the argument and rebinds every target-page parameter to $currentObject — CE1571 at build #1029) — a page-level button dropped its argument in silence and mxbuild answered one CE1571 per parameter. MDL-PAGEARG01 had two context states where it needed three: the root of a CREATE PAGE walk was indistinguishable from ALTER PAGE's genuinely unknown context, so the guard stood down. A full-document walk does know — nothing encloses the root, so the empty mapping is a missing one, not an inferred one. Concluding "no context" descends only through an allow-list; anything else degrades to unknown, since a datagrid leaves a plain string where a rule keyed on GetDataSource() != nil would refuse correct code.
  • Dynamic images (d947aa8, refs Selectionhelper with renderStyle: 'custom' — slot children containing StaticImageViewer are not re-executable; workaround requires placeholder dynamictext #1057) — dynamicImageToGen called a zero-argument constructor for a property whose entire content is the entity binding, so every dynamic image mxcli wrote failed CE0489. DESCRIBE had no case for Forms$ImageViewer either, so the replay deleted the widget — and the build got quieter, 4 errors to 2, because dropping the widget dropped the CE0582 it carried. A round trip that "fixes" a build error by deleting the user's work. A source the type cannot hold is now refused rather than ignored, since writing the holder without one produces a message blaming the author for something they did not do.

OData: external entity updatability

Three commits converging on one rule, each correcting the previous one against a wider contract.

  • Never mark a key Updatable (2202722) — a writable service produced one CE6630 per key part. Mendix computes a key as non-updatable whatever the contract says. Took the reported repro from 2 errors to 1.
  • Gate it on top-level (bb01f45) — clearing Updatable on every key turned the one reported error into seven of its inverse on the live TripPin contract, over derived and contained types mutated through their parent's write flow. UserName is the two-sided control inside one document: False on Person, True on Employee and Manager, which rules out inheritance and the property itself. The seven synthetic shapes probed for the first commit were all top-level, so they could not see the variable that mattered.
  • No attribute of a top-level entity is updatable (3c19cce) — the non-key sibling; the repro now reaches 0. Mendix does not derive attribute updatability from the annotation at all, so defaultUpdatable is !isTopLevel and the key-specific guard is subsumed. Measured across ten top-level contract shapes, each of which mxbuild reads as updatable and each answering False — including one that asserts by name that the attribute IS updatable. That last shape is what made this actionable rather than a guess: seven shapes agreeing was the uniform-negative pattern that usually means the probe is wrong. Two things the rule is not, each with a control: not "the entity is read-only" (Creatable follows the contract from the same document), and not the model's allow-change-locally flag.

The microflow header is now authorable

  • url / export level / disallow concurrent execution (9765c65) — the previous sync made these survive a rewrite, which left one hole preservation could not close: describe → rename → exec is mxcli's copy operation, and a copy is a new document with nothing to preserve from. DESCRIBE emitted -- URL: as an apology; it now emits the clauses. Every AST field is a pointer, so an omitted clause still preserves and clearing is explicit — otherwise the feature reintroduces the drop through the front door. Four platform rules refuse before the write rather than at build time, three of them shared by check and the executor because two copies in two currencies is how a resolver drifts. Five corrections the work itself produced, including: the translations guard it was designed around was unnecessary and was deleted (measured — canon.CarryTranslations already pairs texts by containment path), and describe microflow has a second header renderer, so unit tests passed while the command printed nothing.

Security, report and ALTER PAGE

Tests and docs

  • Bug-tests and shared-rule coverage (d5b7194) — MDL repros for the two reproducible fixes, layout and snippet coverage for SET Documentation (alterPageOperation is shared by three statements — recurring finding 26 is that a clause added for one silently reaches the others), and three new rows in the recurring-findings table for the patterns this branch's own fixes were.
  • Carried microflow properties documented (aa79d78) — the three places the checklist asks for and the previous work skipped. The bug-test says plainly that it pins only the syntax half and cannot pin the preservation, since MDL can neither set these properties nor read them back: a fixture that passes equally against broken and fixed code is not coverage, and claiming otherwise is worse than having none.
  • Current-state docs point at the engine that exists (f4779b7) — third and last pass on sdk/mpr, covering the user manual, wiki, ARCHITECTURE and the MDL spec, where a path to a deleted package is wrong rather than historical. Five things were wrong beyond a path: the library's Go examples imported the deleted package, and compiling the rewritten version found two API errors the docs had carried for a long time; the wiki's architecture page stated its own central claim in the wrong number; and the doc CLAUDE.md routes people to before extending the engine had two of its seven recipe steps pointing at things that no longer exist, with its whole verification section resting on the deleted engine as a parity baseline. Dated records are deliberately left alone.

claude and others added 27 commits September 18, 2026 11:06
…ext object

A page-level `actionbutton` (outside any dataview) with
`Action: show_page Page(Param: $Var)` dropped the argument in silence:
`mxcli check --references` said "All references valid", `exec` reported
success, and `DESCRIBE PAGE` then printed `(Item: $currentObject)` on a
page where $currentObject is unbound. mxbuild 11.13.0 rejected the result
with one CE1571 per parameter of the target page.

mxcli stores a widget show-page action with an EMPTY ParameterMappings
array and lets Mendix infer the argument from the enclosing widget's
context object -- deliberate, since an explicit mapping is CE0115 (#296).
MDL-PAGEARG01 already refused an argument that names something other than
that object, but it had two context states where it needed three:
`contextKnown` was set only on entering a data-bound widget, so the root
of a CREATE PAGE walk was indistinguishable from ALTER PAGE's genuinely
unknown context and the guard stood down. A full-document walk does know:
nothing encloses the root, so there is no context object, and the empty
mapping is not an inferred one but a missing one.

- `pageArgContext` replaces the `(contextVar, contextKnown)` pair with
  known / present / varName. known+absent is the new, provable state; the
  root of CREATE PAGE, CREATE SNIPPET and CREATE LAYOUT starts there.
- With no context object, EVERY argument form is refused -- a literal and
  an association path too, which are left alone elsewhere only because
  they cannot be compared to a context object that exists.
- Concluding "no context" descends only through an allow-list of
  context-free containers. `datagrid dg (DataSource: Mod.Entity)` leaves a
  plain string rather than a parsed DataSourceV3, so a rule keyed on
  `GetDataSource() != nil` would call a row-scoped column button
  context-free and refuse correct code (bug-tests/295-...); anything not
  on the list degrades to UNKNOWN instead.
- ALTER PAGE's INSERT/REPLACE keep their stand-down via a separate
  `validateWidgetSubtree` root.
- One refusal wording now serves both `mxcli check` and `exec`, and the
  exec path names the widget.

Verified on mxbuild 11.13.0 both ways. Fault forced back in: check
passes, exec writes, DESCRIBE shows `$currentObject`, 1 CE1571 at
`btnOpen` (2 with the snippet-root probe -- a snippet parameter supplies
no context object either, measured rather than assumed). Fixed: refused
at check and at exec, while a project carrying `$currentObject` in a
dataview, the argument by the dataview's own variable name, a listview
row button and a zero-argument page-level button builds at 0 errors.

Fixes mendixlabs#1029

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkmKyLuqRZVUFVCzVbrVYK
fix(pages): refuse a show_page widget argument where there is no context object
… Expression

A page parameter passed as an argument to a nanoflow or microflow button action
was written as a text Expression ("$BufferDefinition"). Mendix stores a flow
argument in one of two slots of Forms$MicroflowParameterMapping /
Forms$NanoflowParameterMapping: a reference to a page parameter, snippet
parameter or page variable goes in Variable as a Forms$PageVariable, and a
literal or expression goes in Expression. The Expression form binds nothing, so
Studio Pro reports CE1571 "No argument has been selected for parameter 'X' and
no default is available" on opening the page.

The report describes an asymmetry — of two arguments, the one matching the
enclosing data view's source "works" — but both were written the same way and
neither was bound. Studio Pro supplies a default for the argument that happens to
be the data view's object and reports the other, which is what "and no default is
available" says.

Measured on Workflow Commons 4.11.0, Studio Pro-authored (42 pages + 84
snippets): of 101 flow parameter mappings, 95 bind through Variable and 6 through
Expression — and all six of those are Boolean literals. The $-prefixed Expression
mxcli wrote occurs zero times. generated/metamodel agrees: both slots are
optional on the type, and SnippetParameterMapping, which can only ever hold an
object, has no Expression at all.

The read side was wrong in the mirror image and hid the write side: the three
action describers and flowSourceArgs looked for a `Name` key on that
sub-document, which Forms$PageVariable does not have. So every argument in Studio
Pro-authored content described as absent — `Action: microflow M.F` for a button
whose mappings bind two parameters — and a round trip looked lossless.

One classifier now decides, in classifyFlowArgValue, where the four sites each
had their own copy of `strings.HasPrefix(v, "$")`. $currentObject, path
expressions and undeclared names keep the Expression form: no Studio Pro
reference for the bare $currentObject was measured, and show_page already depends
on the context object being inferred rather than named (MDL-PAGEARG01).

mxbuild is not a detector here — `mx check` on the reported project reports
0 errors both before and after, so the error really does appear only in Studio
Pro, as reported.

Refs: mendixlabs#1140

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M6ewJmoi3dEjEHDmZoVLt
Third and last pass on sdk/mpr, after the skills. These are the docs that
describe the system as it is now — the user manual, the wiki, ARCHITECTURE,
the MDL spec — where a path to a deleted package is wrong rather than
historical.

  sdk/mpr/writer_widgets.go     -> mdl/backend/modelsdk/widget_write.go
  sdk/mpr/writer_pages.go       -> mdl/backend/modelsdk/page_write.go
  sdk/mpr/writer_domainmodel.go -> mdl/backend/modelsdk/domainmodel_write.go
  sdk/mpr/parser_page.go        -> mdl/backend/modelsdk/page.go
  sdk/mpr/parser_microflow.go   -> mdl/backend/modelsdk/microflow_read_actions.go
  sdk/mpr/parser.go (extractInt) -> modelsdk/mpr/parser.go

with modelsdk/{mpr,codec,canon,gen} given their own rows in the package
tables, which previously had one line for the whole engine. Every path was
checked to exist before being written down.

Five things were wrong beyond a path:

The Go examples in the library docs imported sdk/mpr to call
mpr.OpenForWriting + api.New(writer). api.Open(path) does both and owns the
connection. Compiling the rewritten example then found two errors the docs
had carried for a long time: Modules.GetModule does not exist (it is
Modules.Get) and WithDateTimeAttribute takes one argument, not two. Nine and
eight occurrences respectively, including one of each in CLAUDE.md and the
builders reference table. Fixed, and the result compiles against the real
api package.

docs-wiki/architecture/mpr-read-write.md described two storage engines side
by side and said "neither engine writes a unit whose content is equal" — the
central claim of the page, in the wrong number. Rewritten to one engine with
two doors onto the same policy (updateUnit and WriteTransaction.WriteUnit),
matching CLAUDE.md.

MODELSDK_ENGINE_ARCHITECTURE.md is the doc CLAUDE.md routes people to before
extending the engine, and two of its seven recipe steps pointed at things
that no longer exist: "find the legacy serializer (sdk/mpr/writer_*.go)" and
"add a parity test in mdl/enginecompare/" (package deleted). Its whole
"Verification truth" section rested on legacy-as-parity-baseline. Rewritten
around what is actually authoritative now: a Studio Pro-authored reference
document, with generated/metamodel as the tiebreaker when gen disagrees, and
an explicit note that a green mxbuild is not evidence — measured on 10.24.25,
0 errors with two 11.5-only keys present.

docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md is a "living document"
tracking bugs in the deleted engine. Marked superseded rather than rewritten:
it records what that engine got wrong, which is worth keeping, but nothing in
it is actionable. docs/CODE_REVIEW.md reviews a project that was still called
ModelSDKGo; marked point-in-time and pointed at ARCHITECTURE.md.

Left alone: docs/plans/, docs/13-decisions/, docs/11-proposals/,
MODELSDK_BACKEND_PIPELINE.md (dated 2026-06-02, migration-era), CHANGELOG and
SYNC_LOG — dated records where sdk/mpr is correct. Also
bug-patterns/engine-divergence.md, which already says the legacy engine was
deleted and to read "the two engines" as "any two backends".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aHj6mJwKCZD7EX7wcD6jW
… that were missed

An audit against the CLAUDE.md Documentation checklist found the skill and
the wiki done, and three items not:

**mdl-examples/bug-tests/** — required for every bug fix, and skipped.
Written on the 914-excluded-document-preserved.mdl template, which exists
for exactly this shape. It says plainly that it pins only the SYNTAX half
and cannot pin the preservation: MDL can neither set these properties nor
read them back, so a script cannot establish the precondition or assert
on the result. A fixture that passes equally against broken and fixed
code is not coverage, and claiming otherwise would be worse than having
no fixture. Its value is the narrative, the three-row measurement table
and the CE5612 rule.

**docs-site/src/language/microflows.md** — the OR REPLACE section said
nothing about what survives a rewrite, which is the question a reader
has at exactly that point. Now a table of the six carried properties,
the absent-annotation-preserves rule for @excluded / @applyentityaccess,
why no checker would warn you, and the two ways to lose them by design
(DROP + CREATE, and a describe -> rename -> exec copy).

**mxcli syntax** — split the way the repo splits this kind of thing.
create-modifiers is cross-cutting and already documented ID reuse, so
property carry-over joins it there and every document type gets it in
one place rather than 27. microflow.create gets the specific six plus
CE5612.

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

fix(pages): bind a page-parameter flow argument through Variable, not Expression
docs: point the current-state docs at the engine that exists
docs(microflows): document the properties a rewrite carries (follow-up to #498)
The `dynamicimage` sibling of mendixlabs#1057, and worse than it: two
defects, only one of which is a round trip.

Every dynamic image mxcli wrote failed the build. `dynamicImageToGen` called
`imageViewerSourceToGen()` with no arguments — a zero-parameter constructor
for a property whose entire content is the entity binding — so the stored
Forms$ImageViewerSource carried nothing but its own $ID and a null
EntityRef, and mxbuild answered

    [error] [CE0489] "Select an entity for the data source of this dynamic
                      image."  at Dynamic image 'imgPhoto'

`pages.DynamicImage` had no DataSource field to pass in the first place.

And, as for the static image, DESCRIBE had no case for Forms$ImageViewer, so
a stored one came back as `-- NOT re-executable` and the replay deleted it.
The evidence shape there is worth keeping: after the pre-fix round trip the
build got QUIETER — 4 errors to 2 — because dropping the widget dropped the
CE0582 it carried. A round trip that "fixes" a build error by deleting the
user's work.

Also hardcoded beside the source, and now wired through: DefaultImage
(always "", behind a dead `DefaultImage model.ID` that named the wrong
thing — it is a by-name reference to Images$Image), both size units (always
"Auto"), ShowAsThumbnail and OnClickEnlarge (always false). Property names
are shared with the static and the pluggable image — Width/Height, the
units, Responsive, DisplayAs, OnClickType — so one spelling means one thing
across all three.

A source Forms$ImageViewerSource cannot hold is refused rather than ignored.
It has no slot for a microflow, a nanoflow or an association, and writing
the holder without one produces CE0489 — a message saying the author forgot
the source when they did not. Nothing in validate_widget*.go constrains this
(measured), so the writer is the only place that can say it.

Measured on a blank Mendix 11.12.1 project, mxbuild 11.12.1:

    pre-fix  exec                       CE0489, 4 errors
    pre-fix  describe -> exec           widget gone, 2 errors
    fixed    exec                       3 errors (CE0582 only), no CE0489
    fixed    describe -> exec           Unchanged page
    writer reverted to a bare source    CE0489 again, 4 errors

There is no Studio Pro-authored dynamic image in a blank 11.12.1 app
(measured: one Forms$ImageViewer, and it is mxcli's own), so the widget's
shape is metamodel-derived. What is pinned to Studio Pro is the element
CE0489 asks for: DomainModels$DirectEntityRef{Entity}, 20 of 20 instances.

CE0582 is Mendix's own deprecation of both legacy image widgets; the
pluggable `image` widget is the replacement for both.

Refs: mendixlabs#1057

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016JgBheMTV6UiQstQ2nLyay
Three header clauses on CREATE MICROFLOW, plus their explicit clear forms:

  url 'order/{Order}'  /  url search parameters ($Tab)  /  drop url
  export level api | hidden
  disallow concurrent execution error message '…' | error microflow Mod.X
  allow concurrent execution

mendixlabs#1120 made these SURVIVE a rewrite. That left one hole preservation could
not close: describe -> rename -> exec is mxcli's copy operation, and a copy
is a new document with nothing to preserve from — which is why DESCRIBE
emitted `-- URL:` as an apology. It now emits the clauses, and the copy is
faithful.

An OMITTED clause still preserves what is stored, so every AST field is a
pointer: "not said" must stay distinguishable from "set to the zero value",
or the feature reintroduces mendixlabs#1120 through the front door. Clearing is
explicit. `Mark as used` keeps no clause and is still carried.

Four platform rules now refuse before the write rather than at build time.
MDL-MF01 (a {Name} must name a parameter), MDL-MF02 (a PATH parameter may
not also be a SEARCH parameter — CE5612), MDL-MF03 (disallow needs a
handler — CE4899) live in mdl/types, called by BOTH `mxcli check` and the
executor, because two copies in two currencies is how a resolver drifts.
CE0570 (deep links are unique) needs the project, so it sits at the write
path — and it exists only BECAUSE this feature makes copying possible.

Five corrections the work itself produced, each from running something:

- The translations guard this was designed around is unnecessary and was
  deleted. canon.CarryTranslations already pairs texts by containment path
  and carries the other languages: measured, restating an English
  concurrency message left its Dutch translation untouched. The guard was
  refusing a round trip that works.
- The same mechanism means ALLOW CONCURRENT EXECUTION cannot clear a stored
  message — an emptied text comes back on the next write — so it does not
  try. That also matches Studio Pro, which greys those fields rather than
  erasing them.
- describe microflow has a SECOND header renderer. The clauses went into
  renderMicroflowMDL first; unit tests passed and `describe microflow`
  printed nothing. Both now call one helper.
- The keyword-collision mitigation was insufficient as described.
  identifierOrKeyword protects only rules that use it, and the workflow
  grammar takes bare IDENTIFIER — so `Hidden` stopped parsing there the
  moment HIDDEN_KW existed. Three workflow tests caught it; fixed in the
  grammar and both visitor reads.
- ExecContext.Connected() is now nil-safe, which its own doc comment
  already claimed. Without it every version-gated command panics rather
  than skipping when called without a context.

Verified end to end on Mendix 11.6.6: authored from MDL, all three land on
disk, `mx check` 0 errors, and describe -> rename -> exec copies them with
the search parameter re-qualified to the new name. Controls: neutralising
the apply, making it clear instead of preserve, and removing the CE5612
rule each fail a different test with the reported symptom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ssUKiZ9ekBvzSNM5VCVoP
feat(microflows): author the URL, export level and concurrency from MDL
CREATE OR MODIFY EXTERNAL ENTITIES FROM let a key attribute follow the
entity set's UpdateRestrictions/Updatable, so a writable service produced
one CE6630 per key part:

  [error] [CE6630] "'DefinitionId' is marked Updatable=False in the OData
                    service, but True in the app."
          at Attribute 'MyFirstModule.Definition.DefinitionId'

Mendix computes a key as non-updatable whatever the contract says — a key
cannot be changed after the object exists. Key membership was already in
the attribute loop as keyPropSet[p.Name]; it was passed to
edmToDomainModelAttrType and never consulted for updatability.

Creatable is deliberately not cleared with it: the key is written once, at
creation, and the reported build flagged it Updatable=False only, with no
Creatable error beside it. RemoteKeyPart needs no matching change — it
carries no capability fields (Name, RemoteName, RemoteType, Type).

Measured on mxbuild 11.12.1, two copies of one project, same script:

  pre-fix  (key Updatable=true)  -> CE6630 on DefinitionId AND on Label
  post-fix (key Updatable=false) -> CE6630 on Label only

A seven-variant probe contract (inline record, <Record Type=...>,
UpdateMethod=PATCH, +NonUpdatableProperties/+DeleteRestrictions,
unannotated, external <Annotations Target=...>, Core.Permissions/ReadWrite)
had mxbuild stay silent on every key once non-updatable, so the rule holds
in all of them.

The remaining Label error is a second, separate defect: mxbuild answered
"Updatable=False in the OData service" for the non-key attribute of all
seven shapes too. It is not fixed here — no probe produced a contract
mxbuild treats as updatable, so there is no positive control for the right
rule, and CE6630 fires in both directions (mxcli-formula1 §48).

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

`grant write *` on an entity carrying an autonumber wrote ReadWrite on it
and the build failed CE6592, so the user had to narrow the grant by hand
with a REVOKE.

The downgrade asked only whether an attribute was calculated. An autonumber
carries no DomainModels$CalculatedValue — its value comes from the database
on insert rather than from a microflow on read — so it failed that test and
kept its write. Mendix forbids write on both for the same reason, so the
predicate covered exactly half the rule.

The GRANT is half the fix. ReconcileMemberAccesses runs on the executor's
finalize step after every program, so a grant corrected by hand was
re-broken by the next write touching the module; both of its sites needed
it — the existing-entry downgrade and the missing-entry add, which
otherwise inherits the rule's ReadWrite default.

The rule lives once in types.WriteRightsForbidden because the two callers
speak different currencies (sdk/domainmodel and modelsdk/gen) and neither
may import the other.

Each test carries two controls: a calculated attribute, which was already
downgraded before this change, and a plain one that must keep its write —
without the second, the tests pass against a blanket downgrade that would
strip write rights from the whole model. Verified by stubbing the predicate
back to calculated-only: all three fail with the reported ReadWrite.

Closes #524

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2aYb3zDscezm874CDTH6R
`mxcli report` never loaded lint-config.yaml, so a rule a team had
deliberately accepted and disabled still scored against them — and no
configuration could move the score. On the reporting project 61 of 86
findings were two such rules, taking a 99/100 baseline to 66/100.

Separately, report carried its own inline copy of the built-in rule list
that had fallen one rule behind (MDL-FLOW01), so the two commands scored
one project against two rule sets. Both are the same root cause: report
re-implemented lint's setup instead of sharing it.

Both now go through projectLintRules and applyLintConfig, and lint is
routed through the same two helpers so there is one path rather than a
shared one plus a copy.

This is mendixlabs#904 in the opposite direction — there a silently reduced rule set
made the score falsely high, here an unread config makes it falsely low —
and both are invisible because a score carries no provenance.

The inline copy cannot be guarded by a value test: both commands build
their rules inside a cobra RunE, so nothing a unit test can call would
notice a second list re-added beside the helper, which is how the copy
drifted in the first place. The guard is structural, with a positive
control first so it cannot pass vacuously. Verified by stubbing each half
back: the config test fails on CONV010 still enabled, the guard fails
naming the re-added line.

Closes #525

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2aYb3zDscezm874CDTH6R
ALTER PAGE could not set Documentation, so documenting an existing page
meant re-running its CREATE — the doc comment on the create statement was
the only source. For a real page that means re-emitting its whole widget
tree through a describe → exec round trip that is only as complete as what
MDL can spell, so the workaround could silently lose widgets.

The field was otherwise fully understood: pageToGen has always written it
on CREATE, gen binds it as a plain top-level string, the grammar already
parsed the statement and the executor has no allowlist. It was one missing
case in the mutator.

One case covers Page, Layout and Snippet — all three declare the property
and all three reach applyPageLevelSetMut through SetWidgetProperty("").

An empty string clears rather than being rejected: removing a doc comment
from a script has to be expressible, and the property is a bare string with
no unset value.

The unsupported-property message is updated in the same change and a test
holds it there — that list is the only guidance a reader gets, and a stale
one sends them back to the workaround this removes.

Closes #527

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2aYb3zDscezm874CDTH6R
Lint rule SEC005 reported "strict mode is disabled" and MDL had no
statement that would turn it on — its own suggestion said "not settable via
MDL". A rule with no remedy.

StrictMode was read everywhere and written nowhere: security_read.go reads
it, `show security` prints it, the Starlark rule lints it, and
ProjectSecurity.SetStrictMode existed in gen and was never called.

Writing a property gen merely offers is the trap that makes a document
Studio Pro cannot open; this is not one. StrictMode is declared by both
generated sources and mxcli already reads it back from real projects.

The AST field is a pointer, not a bool: "the statement said nothing about
it" has to be distinguishable from "the statement asked for off", or every
DEMO USERS toggle would silently disable strict mode. A test pins that.

STRICT and MODE are new lexer tokens and both go in the parser's keyword
rule — `mode` is an entirely plausible attribute name, and a keyword left
out of that rule silently breaks every model already using the word.

No level-dependent refusal: the model stores StrictMode independently of
SecurityLevel, and the lint rule already scopes its own advice to
Production. The rule's suggestion now names the statement, since a remedy
still pointing at Studio Pro leaves the finding as unhelpful as before.

Closes #526

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2aYb3zDscezm874CDTH6R
Three things the PR checklist and the review's recurring-findings table
asked for:

- mdl-examples/bug-tests/ scripts for the two MDL-reproducible fixes, so
  each regression can be validated in Studio Pro. #525 gets none: it is a
  CLI config bug with nothing to express in MDL. Named with a topic prefix
  per CLAUDE.md, since bug-tests/ already collides upstream PR numbers with
  fork issue numbers on 261-266.

- Layout and snippet coverage for SET Documentation. alterPageOperation is
  shared by ALTER PAGE, ALTER LAYOUT and ALTER SNIPPET, and all three reach
  applyPageLevelSetMut through one OpenPageForMutation — recurring finding
  26 is that a clause added for one construct silently reaches the others.
  Documentation is safe on all three on the evidence that matters: the key
  is one of the ten measured on Atlas_Core.Atlas_Default at 11.13.0, and
  snippetToGen writes it too. Not because gen declares it — gen also offers
  Layout.MainPlaceholderName and six siblings that no Atlas layout carries.

- Three new rows in the recurring-findings table, for the patterns this
  branch's own fixes were: a predicate naming one cause of a CE number, two
  commands computing one thing from two copies of a setup, and a test that
  cannot construct an argument telling you the argument does not belong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2aYb3zDscezm874CDTH6R
`textbox t (attribute: Assoc/Attr)` built a flat path and the build failed
CE1613 "The selected attribute
'Rules.RuleAction.RuleAction_BusinessRule/Name' no longer exists" — the
association segment pasted onto the context entity rather than navigated.
The same syntax already worked on a DataGrid2 column, so one page could
bind an associated attribute in a grid column and fail on the text box
beside it.

Whether Mendix permits this on a plain text box was the open question that
blocked the fix, and reasoning could not settle it — the plausible
alternative was to refuse the syntax and hint at a nested dataview. A
Studio Pro reference settles it: ako/TestApp's Rules.RuleAction_NewEdit
stores, on textBox4, Attribute "Rules.BusinessRule.Name" with an
IndirectEntityRef over Rules.RuleAction_BusinessRule.

The read half was broken in the mirror image and had to move with it.
extractAttributeRef returned the last segment of AttributeRef.Attribute and
ignored AttributeRef.EntityRef, so DESCRIBE emitted a bare `Name` for that
text box — and RuleAction has no Name, so describe → exec over a Studio Pro
page rebound the widget to nothing with `check` clean. columnAttributeFromRef
already did this correctly; sharing it is the point, since two readers of
one BSON shape is how the halves drifted. Fixing only one would have turned
silent corruption into a build error rather than a round trip.

Six input widgets carry the hops now (textbox, textarea, datepicker,
dropdown, checkbox, radiobuttons), through one resolver and one writer
helper beside the existing attributeRefWithStepsToGen.

Verified against the real project at Mendix 11.14.0. Same script, same
mxbuild, only the binary differing: pre-fix → CE1613, post-fix → 0 errors.
A describe → exec of the Studio Pro page reproduces the stored binding
byte-for-byte, element $IDs included.

Closes #529

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2aYb3zDscezm874CDTH6R
The first version of this fix cleared Updatable on EVERY key. That turned
the one reported CE6630 into seven of its inverse on the live TripPin
contract (mxbuild 11.12.2, integration suite):

  [error] [CE6630] "'TripId' is marked Updatable=True in the OData service,
                    but False in the app."
          at Attribute 'TripPinClient.Trip.TripId'

over Trip, PlanItem, Event, Flight, PublicTransportation, Employee and
Manager — every one a derived or contained type with no entity set of its
own, mutated through its parent's write flow — while the entity sets in the
same contract (Person, Airline, Airport) stayed silent at Updatable=false.

So the rule is the key of a TOP-LEVEL entity, matching the existing
non-top-level defaults a few lines above. `UserName` is the two-sided
control inside one document: expected False on Person and True on Employee
and Manager, which rules out inheritance and the property itself.

Adds TestCreateExternalEntities_DerivedTypeKeyStaysUpdatable so the pair is
a two-sided control at the unit layer — either test alone passes against a
fix that is wrong in the other direction, and catching this needed a
12-minute CI round before. Verified locally: the exact failing case,
`-run 'TestMxCheck_DoctypeScripts/10-odata-examples'`, now passes.

The seven synthetic annotation shapes probed for the first commit were all
top-level, so they could not see the variable that mattered — the mendixlabs#1118
finding had already said "do not stop at a synthetic fixture; TripPin is
the fixture to reach for". Finding and bug-test comments corrected
accordingly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJtAzF7vSzuv7gpcpLTA9A
fix(odata): never mark an external entity's key attribute Updatable
Five fixes from the ChipCoV3 test-project findings
The sibling of the key-attribute defect: every NON-key attribute followed
the entity set's UpdateRestrictions too, so a writable service was one
CE6630 per attribute —

  [error] [CE6630] "'Label' is marked Updatable=False in the OData service,
                    but True in the app."

Fixing only the key took the reported repro from 2 errors to 1. It now
reaches 0.

Mendix does not derive attribute updatability from the annotation at all:
no attribute of a top-level entity is updatable, and every attribute of a
non-top-level one is, because that one is written through its parent's
flow. So `defaultUpdatable` is `!isTopLevel`, and the key-specific guard
added in the previous PR is subsumed by it.

Measured on mxbuild 11.12.1 across ten top-level contract shapes, each one
mxbuild reads as updatable and each answering False: inline <Record>,
typed <Record Type=...>, UpdateMethod=PATCH, +NonUpdatableProperties
+DeleteRestrictions, unannotated, external <Annotations Target=...>,
Core.Permissions/ReadWrite, Core.OptimisticConcurrency (ETag),
DeepUpdateSupport/Supported=true, and NonUpdatableProperties naming ONLY
the key. The last is what made this actionable rather than a guess: that
service asserts by name that Label IS updatable, and mxbuild still says
False. Seven shapes agreeing was the uniform-negative shape that usually
means the probe is wrong, which is why it was not acted on before.

Two things the rule is not, each with its own control:

  - Not "the entity is read-only". mxbuild reads InsertRestrictions from
    the same document in the same shapes and honours it, so Creatable
    follows the contract while Updatable does not. Every test here asserts
    Creatable as its control.
  - Not the model's "allow creating and changing objects locally".
    Setting AllowCreateChangeLocally=Yes left the expectation at False.
    An external object can be changed in memory and handed to an external
    OData action — that is what the local-change flag governs, while this
    one mirrors what the endpoint accepts on a PATCH.

TripPin stays green, since every entity it flags is non-top-level; it is
the other-direction control.

mendixlabs#1118's TestCreateExternalEntities_FlattenedAttributesAreReadOnly asserted
Label was Creatable AND Updatable as its control. The Updatable half had
been assumed from the contract rather than measured, while the flattened
half it was controlling for had been. Corrected; the control still does
its job on Creatable alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJtAzF7vSzuv7gpcpLTA9A
fix(odata): a top-level external entity has no updatable attributes
@github-actions

Copy link
Copy Markdown

AI Code Review

Review Summary

This PR implements multiple fixes across page widget bindings, OData updatability, microflow header authorability, and security/report functionality. The changes are well-structured and follow the project's architectural patterns.

Critical Issues

None found.

Moderate Issues

None found.

Minor Issues

  • Some test files are lengthy (e.g., cmd_contract_key_updatable_test.go at 332 lines), but justified by complexity
  • Minor comment redundancy in cmd_pages_showpage_args.go (repeated explanation of ParameterMappings behavior)

What Looks Good

  1. Thorough bug fixes: Each defect mentioned in the PR body has corresponding test files that reproduce the issue and verify the fix
  2. Full-stack consistency: MDL changes properly flow through grammar → AST → visitor → executor → backend → LSP → DESCRIBE roundtrip
  3. Proper preservation logic: Microflow header changes correctly implement "absent preserves" vs "stated overrides" semantics
  4. Clear error handling: Security strict mode fix includes proper validation and meaningful error messages
  5. Documentation alignment: Skills documentation updated to reflect new behavior
  6. Backwards compatibility: STRICT/MODE tokens made usable as identifiers to avoid breaking existing models
  7. Test coverage: Each fix includes MDL test scripts in mdl-examples/bug-tests/ that verify Studio Pro behavior

Recommendation

Approve - The PR addresses multiple distinct issues with appropriate scoping, follows all architectural guidelines, includes comprehensive tests, and maintains backward compatibility. The changes are ready for merging.


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

@ako
ako merged commit 9c5a837 into mendixlabs:main Sep 20, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment