Sync ako/mxcli: page widget bindings, OData updatability, authorable microflow header, security and report fixes - #1144
Merged
Merged
Conversation
…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
AI Code ReviewReview SummaryThis 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 IssuesNone found. Moderate IssuesNone found. Minor Issues
What Looks Good
RecommendationApprove - 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
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 ignoredAttributeRef.EntityRefentirely, sodescribe → execover a Studio Pro page rebound the widget to nothing withcheckclean. 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.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 inVariable, a literal or expression inExpression. Measured on Workflow Commons 4.11.0: of 101 mappings, 95 bind throughVariableand 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 aNamekey thatForms$PageVariabledoes not have, so every Studio Pro-authored argument described as absent and a round trip looked lossless.mx checkreports 0 errors before and after, so the error really does appear only in Studio Pro, as reported.show_pageargument with no context object (5317ebe, fixes A page-levelactionbutton(outside any dataview) withAction: 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 adatagridleaves a plain string where a rule keyed onGetDataSource() != nilwould refuse correct code.d947aa8, refs Selectionhelper with renderStyle: 'custom' — slot children containing StaticImageViewer are not re-executable; workaround requires placeholder dynamictext #1057) —dynamicImageToGencalled 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 forForms$ImageViewereither, 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.
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.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.UserNameis 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.3c19cce) — the non-key sibling; the repro now reaches 0. Mendix does not derive attribute updatability from the annotation at all, sodefaultUpdatableis!isTopLeveland 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 → execis 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 bycheckand 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.CarryTranslationsalready pairs texts by containment path), anddescribe microflowhas a second header renderer, so unit tests passed while the command printed nothing.Security, report and ALTER PAGE
197ecb4, closes GRANT write on an entity with an AutoNumber fails the build (CE6592) ako/mxcli#524) —grant write *on an entity carrying an autonumber failed CE6592. The downgrade asked only whether an attribute was calculated; an autonumber carries noCalculatedValue, so the predicate covered exactly half the rule. The GRANT is half the fix: the finalize step re-broke a hand-corrected grant on the next write touching the module. Each test carries two controls — without the second, they pass against a blanket downgrade that would strip write rights from the whole model.reportignored lint-config.yaml (4b39ccf, closes mxcli report ignores lint-config.yaml, so a disabled rule still scores against the project ako/mxcli#525) — a rule a team had deliberately 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.reportalso carried its own inline copy of the rule list, one rule behind, so the two commands scored one project against two rule sets. This is Bundled Starlark lint rules fail silently: stale files skipped without warning, -r unknown-rule says 'No issues found', discovery is mpr-relative while init writes cwd-relative #904 in the opposite direction, and both are invisible because a score carries no provenance. The guard is structural, since both commands build their rules inside a cobraRunE.ALTER PAGE … SET Documentation(c7c93e3, closes ALTER PAGE cannot set Documentation, so documenting a page means re-running its CREATE ako/mxcli#527) — documenting an existing page meant re-running its CREATE, which for a real page means adescribe → execround trip only as complete as what MDL can spell. One missing case in the mutator; one case covers Page, Layout and Snippet.ALTER PROJECT SECURITY STRICT MODE ON|OFF(11a1c6e, closes SEC005 lints for strict mode with no way to turn it on from MDL ako/mxcli#526) — lint rule SEC005 reported strict mode disabled and its own suggestion said "not settable via MDL": a rule with no remedy.StrictModewas read everywhere and written nowhere. The AST field is a pointer so aDEMO USERStoggle cannot silently disable it, andSTRICT/MODEboth go in the parser's keyword rule —modeis a plausible attribute name, and a keyword left out of that rule silently breaks every model already using the word.Tests and docs
d5b7194) — MDL repros for the two reproducible fixes, layout and snippet coverage for SET Documentation (alterPageOperationis 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.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.f4779b7) — third and last pass onsdk/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.