Sync ako/mxcli: REST request bodies, mapping fidelity, idempotent writes, and platform fixes - #938
Merged
Conversation
A "Call REST service" activity configured to store its response in a file
document had no MDL form at all, and both readers fell through to String.
DESCRIBE reported `returns String`, and the describe -> exec round trip then
wrote that back. Measured on mxbuild 11.6.6:
stored before: ResultHandlingType = FileDocument
VariableType = ObjectType(MyModule.MyFile)
stored after: ResultHandlingType = String
VariableType = StringType
mx check: unchanged, only the project's pre-existing baseline error
So the activity was silently retyped with a valid model and a green build --
nothing downstream could notice. On the legacy engine the output variable
went too, because the unread result handling took the `$var =` fallback with
it.
The two readers failed differently, and each looked fine from inside itself:
the legacy parseResultHandling had no FileDocument case and returned nil,
while modelsdk's restResultHandlingFromRaw read VariableType.Entity and then
discarded it, keeping only a literal match on System.HttpResponse so that
everything else -- FileDocument AND any other object type -- became String.
It now reads Mendix's own ResultHandlingType discriminator, falling back to
the VariableType because that property is omitempty.
Syntax is `returns Module.Entity`, added as the LAST alternative of
restCallReturnsClause so the keyword forms keep winning; a test pins that.
Mendix rejects the BASE System.FileDocument as a return type (CE0362), so
the entity is always a specialization -- which is possible because CE1540
lists FileDocument among the four System entities that may be specialized.
MDL064 reports the base type and an unqualified name before the write.
The other half of the report needed no change. `returns response`
round-trips losslessly, and the suggested "HttpResponse specialization"
cannot exist: CE1540 permits only User, FileDocument, Image and Paging, so
`response` already names the only type such a result can have. Verified, and
pinned by a round-trip test so a future change here has to stay honest.
The silent String fallbacks in the describer are removed. A result handling
mxcli cannot reconstruct now renders as text the parser REJECTS, naming what
was unsupported: a describe that fails is recoverable, one that quietly means
something else is not (ADR-0005). A test asserts the refusal does not parse.
Verified: round trip on BOTH engines (gateEngines) -- the check that finds
this class, since either reader alone looks consistent; the authored activity
builds at baseline under mx check; 338 shipped examples scanned with 0 new
hits; 19 microflows described on both engines with no spurious refusal; unit
tests confirmed to fail with the visitor branch and the renderer case stubbed.
Also adds `mxcli syntax rest.call`, which documents the activity and all five
RETURNS forms -- there was no topic for it before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
REST calls can store the response in a file document (mendixlabs#922), and DESCRIBE stops guessing String
The nightly failed on the Mendix 10.24 matrix entry, on both engines, in TestMxCheck_DoctypeScripts: Execution error: this project does not store the model setting DecimalScale 14-project-settings-examples.mdl set it unconditionally. Measured against a blank project of each version: 10.24 stores 11 model settings, 11.6.6 stores 12, and DecimalScale is the only difference. Executing the other five settings from that statement one at a time on 10.24, each is accepted -- so DecimalScale alone is at fault, and because the refusal covers the WHOLE statement it took five portable settings down with it. mxcli's refusal is correct and is not changed here: Studio Pro will not open a model carrying a property its version does not define, and mxbuild does not catch it. The example was simply not version-gated, despite its own comment three lines above explaining that which settings a project stores depends on its version. Also adds TestDoctypeScriptsParseAfterVersionFiltering, which filters every doctype script for each version in the nightly matrix and asserts the result still parses. It needs no mxbuild, so a mis-gated script fails in seconds on push instead of hours later in one nightly job. Writing that guard found the trap that makes this easy to get wrong, and the first attempt at this fix walked straight into it: a `/** */` block is a DOCUMENTATION comment bound to the statement after it. Gating the statement while leaving its doc comment outside the section orphans the comment, and the script dies with "no viable alternative at input '/**...'" -- reported at the NEXT statement, tens of lines further down, so it reads like an unrelated syntax error in code nobody touched. The comment now sits inside the gated section; `--` line comments are free-standing and safe either side. Verified: the full doctype suite (59 scripts, both engines) passes on 10.24, where it failed before; the changed script passes on 10.24, 11.6.6 and 11.13.0; the control -- the pre-fix file -- still reproduces the reported error on 10.24; and the new guard, with the doc comment moved back outside the gate, fails on exactly the 10.24 entry and passes once restored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
Fix the 10.24 nightly: version-gate DecimalScale, and guard the doctype corpus against mis-gating
Studio Pro's version-control view showed an entire nanoflow as changed after editing one argument of one JavaScript action call, and the same one-literal change to a microflow read as a wholesale replacement. Measured on the reporter's project: 36 of 37 element identities re-minted in the nanoflow, 21 of 22 in the microflow. The damage was cumulative — changing a value and changing it back produced a semantically identical document sharing none of the original's element IDs. Elision (ADR-0008) never covered this. It answers "did anything change?" and keeps a no-op write off disk; when something *did* change the document was still rebuilt from the MDL, and every sub-element of a rebuild gets a freshly random $ID. canon.TransplantIDs matches the incoming document against the stored one element by element and puts the stored $ID back on every element that still corresponds. Alignment is by $Type plus the shape one level down (Action=Microflows$LogMessageAction) plus Name where there is one, LCS-anchored within each list with positional fill in the gaps — the deeper key is what tells two otherwise identical ActionActivity wrappers apart when an activity is inserted, without which the newcomer inherited its neighbour's whole subtree. References are rewritten in the same pass, which is the rule PR #125 broke: a pointer is a primitive binary property that a containment walk never sees. The substitution therefore covers every 16-byte binary in the document rather than a maintained list of pointer property names, on canon's own insight that any occurrence of one of the document's element IDs is a reference by definition. It is applied in place on a copy, so a fixed-width binary keeps the framing intact and nothing is re-marshalled. The correctness bar is lower than it looks and the code says so: a wrong match only makes a diff bigger, because every reference moves with its element and nothing outside the unit remembers an $ID. The one real failure is two elements sharing one, guarded by dropCollisions (run to a fixed point) and by reading the result back and checking the id set. GUIDs are untouched — those are the database's identity and were already preserved. Measured, both engines: 37 of 37 identities kept on the reported nanoflow with the BSON diff down to the one changed argument; a change plus its revert returns to the original bytes exactly; inserting or deleting an activity mints IDs only for the genuinely new elements (22 of 22 kept, 6 new). mx check on the reporter's app is unchanged at its one pre-existing CE0117. TestWriteMicroflowTwice_ControlChurnsWhenElisionOff had to change: it proved elision was doing the work by forcing writes with MXCLI_ALWAYS_WRITE and watching the bytes churn, and identity preservation now makes a forced write byte-stable too. The control moves down a layer to the raw codec output (TestRebuildChurnsSubElementIDs), and the forced-write case becomes a positive assertion about the transplant. The shell equivalent is documented as a control on mtimes rather than hashes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Re-running an already-applied script printed
Modified javascript action: MxCore.JS_LoadAiAdvisor
Replaced nanoflow: HomeScan.ONL_AIAdvisor
against a project whose files were not touched — not even their mtimes. The
elision was real and correct (ADR-0008); only the reporting was wrong, and it
is wrong in the direction that costs most: someone diagnosing version-control
churn from console output concludes mxcli rewrites everything on every run,
which is exactly how mendixlabs#910 was first mis-diagnosed.
The handler cannot tell, because ctx.Backend.UpdateNanoflow returns nil whether
or not storage kept the write. Both engines' writers now count unit writes
offered versus written at their single choke point, backends expose that
through an optional backend.WriteStatsReporter, and ExecContext.ReportMutation
downgrades the verb to "Unchanged" when writes were offered since the last
report and none of them landed.
The sampling is per report rather than per statement, so a handler that
rewrites several documents in a loop (constants, module roles) labels each on
its own merits. The verb is only downgraded on positive evidence, so a mutation
that never reaches unit storage — a theme file, a mock backend, any engine with
no notion of units — is reported exactly as it was before.
WriteStatsReporter is deliberately not part of FullBackend: it says nothing
about the model, only about what a storage engine did, and a backend with no
units has no honest answer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…endixlabs#927) Studio Pro can bind a leaf several levels below the object element it belongs to: tick a nested leaf without ticking its parents and it is offered on the nearest object element you kept. Mendix stores that as ONE multi-segment JsonPath -- "(Object)|customer|name" on a value element whose parent object element is at "(Object)", with nothing mapped for "customer". One entity, values pulled from several depths. MDL could not express it, so a generated module needed an entity per object level purely to reach a value -- one endpoint added 21 entities, almost all pass-throughs holding nothing but an association. Attr = customer/contact/email `/` is the MDL spelling of Mendix's `|`: it reads better here, and on this side of `=` it cannot collide with the association form on the other side. The member is resolved one segment at a time, so every step keeps the raw-key/exposed-name tolerance from mendixlabs#882. DESCRIBE was mis-reading the shape, which is the half the report did not mention. Value elements were printed as the last segment of their JsonPath alone, so a project holding "(Object)|customer|name" described as `CustomerName = name` -- a description of a model that does not exist -- and re-executing mxcli's own output failed with `"name" is not a member of the JSON structure at (Object)`. Members are now rendered RELATIVE to the enclosing object element, on both engines and for both mapping kinds. Nothing was ever corrupted: the mendixlabs#882 guard refused the bad re-execution. The parent path is used verbatim when computing that relative member. An array's object element already carries the ITEM path, so trimming the "|(Object)" marker off it made a child of that item render as "(Object)/sku" -- caught by the new array cases, and visible in mendixlabs#915's existing test. Two shapes are REFUSED rather than written, each measured on mxbuild 11.13 rather than assumed: * An EXPORT mapping cannot collapse levels. Three-way control: the same member in an import mapping is 0 errors, the same export mapping with only top-level members is 0 errors, and the collapsed export is CE5015 "There is no child mapping matching schema element". An export has to PRODUCE the intermediate node, so something must map it. * An import member cannot cross a 0..* element. Patched into a stored mapping, mxbuild answers CE0256 "Between value mapping 'sku' and parent element '(Object)' is a schema element with wrong occurrence (0..*)" -- the rule is occurrence, not "array" loosely, and the message says so. Both refusals name the build error they prevent, because each would otherwise be valid MDL that passes `mxcli check` and fails only in the build. Verified: the collapsed mapping builds at 0 errors on mxbuild 11.13 and round-trips byte-for-byte through DESCRIBE on BOTH engines; a control with the relative-member fix stubbed fails exactly the collapsed cases while the direct-child cases keep passing; 15 shipped mapping examples still parse and the association forms of both mapping kinds are unaffected; 76 unit packages green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
…AP01) CI failed on PR #188 with FAIL (negative test unexpectedly passed): 927-mapping-nested-member-path.fail.mdl `make check-mdl` runs plain `mxcli check` and expects every .fail.mdl to fail THERE, but both mendixlabs#927 refusals lived in the executor, so check exited 0. The Makefile's own note above check-mdl describes this trap exactly -- a working rule made to look regressed, from mendixlabs#891 and mendixlabs#892 -- and prescribes the split, which is what this does: * The EXPORT refusal is purely syntactic: whether a member contains "/" is visible in the statement, no project required. It moves into the no-project pass as MDL-MAP01, beside ValidateGrantRoles, which is there for the same reason (mendixlabs#836). So `mxcli check` now tells the author before a script starts writing, and the negative fixture legitimately fails check. * The array-crossing refusal genuinely needs the project's JSON structure to know an intermediate is 0..*, so it cannot fail check. It is out of the .fail.mdl and covered by the array case in TestJSONSchemaIndex_ResolvePath, which is what the Makefile asks for. The fixture says so, rather than leaving the omission to be rediscovered. The executor keeps its refusal for a statement that reaches exec another way; both now raise the same message through nestedExportMemberError, so the author sees one wording wherever the statement is stopped. Tests mirror the grant rule's: fires without a project, ignores an IMPORT mapping (where collapsing is the supported feature this PR adds), and does not mistake an object element's `Assoc/Entity` -- a `/` on the other side of the mapping -- for a nested member. Verified: `make check-mdl` exits 0, 76 unit packages green, gofmt clean. Also merges origin/main, which had moved on and left the PR unmergeable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
… none can
On macOS and Windows the local loop executed whatever Linux binary the
cache held:
fork/exec ~/.mxcli/mxbuild/11.12.0/modeler/mxbuild: exec format error
MxBuildCDNURL branches on GOARCH and never on GOOS, and both URLs are
Linux tarballs — Mendix ships the macOS mxbuild inside Studio Pro, not on
the CDN. So an arm64 Mac caches a Linux *aarch64* ELF: the architecture
matches, which is why nothing notices until exec.
The rule was already in the codebase twice. `setup mxbuild` asks
NativeMxBuildForSetup, and `docker build` resolves Studio Pro before the
cache — its comment even says "On Windows, CDN downloads are Linux
binaries". The local loop did neither: runlocal.go called DownloadMxBuild
directly and StartServe looked only at the cache.
Three changes:
* The local path resolves through NativeMxBuildForSetup, so Studio Pro
wins over the cache on any non-Linux host. Linux is unchanged.
* LocalRunOptions.MxBuildPath is honoured. It was documented as an
override and never read for the serve binary, so a user hitting this
had no way out.
* A magic-byte check (ELF / Mach-O incl. fat / PE) refuses a foreign
binary before exec, naming Studio Pro and --mxbuild-path instead of
"exec format error". An unrecognised format is allowed through: a
shell wrapper has no magic, and guessing would block a working setup.
The download is deliberately NOT blocked on Windows — the cache holds a
Linux binary there on purpose, for Docker builds. The fix belongs at
resolve and exec time.
The host OS is injected into the helpers rather than read from
runtime.GOOS at the point of use, because this bug is platform-specific
and is otherwise unreachable from a Linux runner. Reverting makes the
explicit-path test fail by spending 34 seconds downloading from the CDN,
which is the ignored override made visible.
Not verified: no macOS host was available, so resolveStudioProDirMacOS
itself is still covered only by its existing tests. The failure mode was
reproduced on Linux by planting a Mach-O at the cache path, which
reproduces the reported message verbatim.
Detection appended the .exe Windows needs; every consumer built the path by hand and did not. The path passed to mxbuild as --java-exe-path, and the one exec.Command runs to boot the runtime, were both "<jh>\bin\java" — so a correctly-detected JDK was handed on in a form that need not resolve, which from outside reads as "mxcli does not detect Java". Five sites built that join; they now share JavaExePath. The sixth would have repeated it. On the JDK search: "add Studio Pro's JDK" turned out to be a non-task. Mendix's install guide lists Eclipse Temurin JDK 21 as the prerequisite and installs it when absent — Studio Pro bundles no JDK of its own, so the existing Adoptium glob already IS Studio Pro's JDK, and inventing a Mendix\<version>\jdk path would have been dead code. What was genuinely missing is the per-user install location (%LOCALAPPDATA%\Programs\…) that winget and the Temurin MSI can produce, which no Program Files glob reaches. The not-found error now lists every location searched, and says which JDK Mendix itself uses. "JDK 21 not found" alone sent a user reading mxcli's source to find out what it had looked at. Gradle needs no change and is worth recording: it ships inside the mxbuild bundle (modeler/tools/gradle) and mxbuild invokes it. mxcli never calls gradle, so a "Gradle missing" from a local run points at a foreign or incomplete mxbuild bundle rather than a missing system Gradle — the same root as the platform mismatch in mendixlabs#916. goos is injected into jdkSearchPathsFor and javaExeName because the only Windows CI job is the tunnel seam, scoped with -run: it compiles Windows code and executes almost none of it, which is how a missing .exe survives. Reverting the suffix fails TestJavaExeName. Not verified: no Windows host was available. This is a code-level fix with OS-injected tests, and the report came via a user relay rather than a reproducible case.
The write-stats reporting judged a statement on unit writes alone, which was
wrong for code actions: a JavaScript or Java action's *body* does not live in
its unit — the unit carries the signature, the source lives in
javascriptsource/<module>/actions/<name>.js. Editing only the body elided the
unit write, so the statement reported
Unchanged javascript action: DT.JS_Ping
while the user's edit had just landed in a file. That is a worse lie than the
one the reporting was introduced to fix, and it was found by measuring the fix
across document types rather than only on the nanoflow from the report.
The generated source file now goes through javaactions.WriteSourceIfChanged,
which reports whether it differed, and both engines fold that into WriteStats.
Skipping an identical file is worth having on its own: an unconditional rewrite
moved the file's mtime on every run, which git does not notice but an
incremental build's caching does.
Also fixes the page path, which printed "Created page X" on the replace path
too, so re-running a script against an unchanged page claimed to create it
every time. It now reports Replaced/Unchanged like every other document type.
Deleting duplicate same-named pages is a real change that unit-write counting
cannot see, so only a one-for-one replacement is eligible for the downgrade.
Measured across document types on one project, changing one thing in each:
enumeration 10/10 element identities kept, microflow 22/22, nanoflow 16/16,
page 65/65, and the domain model 29 of 30 — the single new element being the
attribute's type node, correctly unmatched because Integer and Long are
different $Types. All 8 GUIDs survived, including the retyped attribute's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
… editable (mendixlabs#928) Two reported bindings that passed `mxcli check`, were written by `exec`, and did nothing. They share one cause: the allow-lists behind MDL-WIDGET01 and MDL-WIDGET07 are widget-type AGNOSTIC. isBuiltinPropName is a single flat list holding both ContentParams and Editable; it answers "is this a real MDL property name anywhere", and both validators read it as "is this valid on THIS widget". The engine then acts on each only for the widget kinds that support it, so the rest is dropped in silence. Bug 1 -- FIXED. A pluggable widget's text-template property imageUrl: '{1}', contentparams: [{1} = PictureUrl] stored a template with an EMPTY parameter list, and mxbuild answered CE0720 "Place holder index 1 is greater than 0, the number of parameter(s)" on the FIRST write -- no describe round-trip needed, contrary to the report. A dynamictext with identical syntax stored the parameter correctly, which is the control that localised it to the pluggable path. The engine took the parameters path only for mxcli's `{AttrName}` spelling, so Mendix's own numeric `{1}` had no route. Both spellings now reach the same stored shape via SetTextTemplateWithClientParams, added to the builder interface and to both implementations. Verified: the reported script is 0 errors and the Image's template holds one parameter bound to Product.PictureUrl, byte-comparable with the dynamictext control. Bug 2 -- NOT FIXABLE AS ASKED, so reported instead (MDL-WIDGET20). Mendix models editability on INPUT widgets only: measured against generated/metamodel, exactly eleven Pages types carry Editability / ConditionalEditabilitySettings -- ten inputs plus DataView -- and not one of the fourteen button types does. There is no field to write, so `editable:` -> Editability on a button cannot be implemented; the issue's own second option is the right one. The warning names conditional visibility, which buttons do support. Both MDL spellings are caught. `editable: 'x'` lowers to `Editable`, the bracket form `editable: [expr]` to `EditableIf` -- and the bracket form is the one that genuinely works on inputs, so leaving it unflagged on a button would have been the more surprising silent drop. The type list is a hand-maintained bridge between MDL and Mendix names, so a test parses the metamodel and fails if that set of eleven changes. Also MDL-WIDGET21, the residue of fixing bug 1: contentparams with no `{N}` placeholder to consume them still had nothing to attach to and were dropped without a word. Neither rule is a .fail.mdl. Both are warnings, so `check` exits 0 and such a fixture would report "negative test unexpectedly passed" -- the trap the Makefile documents above check-mdl, and the one that broke mendixlabs#927's CI. They are demonstrated by a plain .mdl and pinned by unit tests. Verified: reported script 0 errors under mx check (was CE0720); controls with each fix removed reproduce CE0720 and drop all three MDL-WIDGET20 cases; input widgets and 339 shipped examples produce no new warning; `make check-mdl` exits 0; 76 unit packages green; gofmt clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
feat(mappings): reach a nested JSON leaf without an entity per level (mendixlabs#927)
A one-line change no longer rewrites every element identity, and a skipped write no longer reports one
fix(run-local): use an mxbuild this host can execute, and say so when none can
Bind a pluggable text template's contentparams, and report editable where Mendix has none (mendixlabs#928)
`import from mapping M.IMM($json)` — no range keyword — built a document the
Mendix runtime refuses:
MicroflowException: key not found: Path(QName(None,),None,)
at ...integration.importer.mapping.MappingCache.storeValueMappingElement
The model is valid, so nothing static caught it: mxcli check, mx check (0
errors) and mxbuild all pass. It fails only when the activity runs, and the
repo had no runtime coverage of import mappings — every existing test stopped
at mx check.
The Range and the result variable's cardinality are separate axes (mendixlabs#881), but
an unauthored range set neither pointer, so ForceSingleOccurrence and
ConstantRange.SingleObject both fell back to SingleObject — true for an
object-rooted mapping. That is Studio Pro's First ("take one of a list"), not a
single-object import. Studio Pro writes both flags false and expresses "one
object" solely through VariableType=ObjectType.
`all`, `first` and limit/offset each set the pointers explicitly, so only the
bare form was affected — the form the shipped examples use
(06-rest-client-examples.mdl:1506, :1541). `first` is unchanged.
Measured against a Studio Pro-authored app on 11.13.0: in one boot, over the
same mapping and the same JSON, `... all` imported and the bare form threw;
with the fix both import. The cross-test is what located it — an mxcli
microflow calling Studio Pro's own mapping fails too, which rules out the
mapping document, the JSON structure and the entity.
Also updates TestImportRange_UnauthoredKeepsTheOldInference, which asserted the
faulty fallback. It passed only because it exercised the list-rooted case,
where the fallback is false either way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
fix(mappings): an unauthored import range is All, not First
…EST02)
`Body: file from $Doc` on a consumed REST operation sent the four bytes `$Doc`
instead of the document. Measured against httpbingo with an 8090-byte PNG:
Content-Length: 4
data:application/octet-stream;base64,JERvYw== -> b'$Doc'
mxcli check passed, mx check reported 0 errors and the call returned HTTP 200.
Every signal a user or an agent checks said success.
Both engines folded FILE into the TEMPLATE branch and wrote a Rest$StringBody
whose ValueTemplate is the expression text — consumed_rest_write.go:218 (the
default modelsdk engine) and writer_rest.go:250 (legacy). `describe` renders it
back as `Body: template '$Doc'`, so the round trip looked self-consistent.
There is no better type to write. Mendix's 11.13 metamodel has exactly three
request-body types — Rest$JsonBody, Rest$StringBody, Rest$ImplicitMappingBody —
and none is binary, so binary upload is not expressible in MDL at all. The fix
is therefore to refuse the clause, as MDL-REST01 refuses a mapping document in
an inline mapping, rather than degrade it into something that looks like it
works. A Java action is the route.
One function guards both the check pass and exec, so `mxcli check` and
`mxcli exec` cannot disagree — and because it sits in buildRestClientOperation,
`--no-check` does not reopen the silent write either.
The refusal is narrow: `Response: file as $Doc` downloads correctly and is
untouched.
Two shipped examples advertised this as a working "File Upload" feature and
never worked; 06-rest-client-examples.mdl is corrected — one is rewritten as
the download that does work, the other replaced by a note.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
…nts`)
Binary POST was reported as impossible in MDL — a consumed REST operation has
only Rest$JsonBody, Rest$StringBody and Rest$ImplicitMappingBody, none binary,
so the previous commit refused `Body: file` and pointed at a Java action.
That was the right conclusion about the wrong document. Mendix models a binary
request body on the microflow REST CALL activity:
"RequestHandling": {"$Type": "Microflows$BinaryRequestHandling",
"Expression": "$FirstFileDoc/Contents"},
"RequestHandlingType": "Binary"
It is a Microflows$ type, which is why grepping the metamodel for Rest$*Body
finds only the three non-binary ones and appears to prove it impossible. A
Studio Pro-authored example (ako/TestApp, 11.13.0) settled it.
mxcli could PARSE that shape and could neither write, read on the modelsdk
engine, nor describe it. So a Studio Pro binary POST described as a REST call
with no body at all, and re-executing that DESCRIBE produced a request that
sent nothing.
Wired full-stack: grammar (`BODY BINARY expression`), AST, visitor, builder,
both writers, the modelsdk reader and the DESCRIBE formatter. The expression is
the FileDocument's Contents MEMBER and is carried as source text — quoting it
would send the path as a string literal.
RequestHandlingType was hardcoded "Custom" in both engines regardless of the
handler. Only the Binary case is derived; the others are left alone, having no
measured Studio Pro reference and working today.
Verified by re-executing mxcli's DESCRIBE of the Studio Pro microflow and
diffing the BSON: same $Type, same discriminator, same expression, mxbuild 0
errors. `go test ./...` green, `make check-mdl` 354 PASS / 0 FAIL.
MDL-REST02's message, the syntax help, the shipped examples and the symptom
table now point at this route instead of a Java action.
Adds cmd/bsondump, a dev helper that prints a .mxunit as canonical extended
JSON — the technique the symptom table prescribes for this class of bug (plain
bson.M loses key order and hides int32 vs int64).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
… write Four defects in the REST call's request handling, all measured against Studio Pro microflows (ako/TestApp, Mendix 11.13.0) covering Custom, Mapping, FormData and Binary. 1. The export-mapping body wrote "ParameterVariable". generated/metamodel gives MicroflowsMappingRequestHandling exactly three properties — contentType, mappingId, mappingVariableName — so mxcli wrote a key the type does not own AND omitted the real one. An unknown property is the shape mxbuild tolerates and Studio Pro refuses to open. mxcli's own READER had known the correct key since mendixlabs#843; the writer was never corrected. 2. ContentType was written empty, which is not a member of the enum (Json|Xml). Studio Pro writes "Json". 3. RequestHandlingType was hardcoded "Custom" in both engines regardless of the handler, so an export-mapping body claimed to be a custom template. Studio Pro pairs Mapping/FormData/Binary/Custom with the matching sub-element; it is now derived. (The previous commit derived only Binary, the one case with a reference at the time.) 4. FormDataRequestHandling and AdvancedRequestHandling can be parsed and not written, so CREATE OR REPLACE/MODIFY dropped the body. Nothing reported it: DESCRIBE omits a clause it cannot express, and a REST call with no body builds clean, so the app posted nothing and every signal said fine. The rewrite is now refused (guard-don't-drop, ADR-0005), mirroring the queued-call guard. The allow-list is the writable set, so the guard stops refusing as soon as a type becomes expressible — as Binary just did. Verified by round-tripping Studio Pro's own microflows: DESCRIBE → exec now reproduces the export-mapping action exactly (type=Mapping, ContentType=Json, MappingVariableName=NewItem), and the form-data microflow is refused by name instead of silently emptied. `go test ./...` green, `make check-mdl` 354 PASS / 0 FAIL. Not fixed here: the legacy engine encodes MappingId as binary where Studio Pro stores a qualified-name string. Noted in the symptom table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
Re-granting a role on an entity rebuilt the rule from that one statement,
so anything an earlier GRANT had allowed came back None: (READ (Name,
Email)) followed by (READ (Phone)) left a rule reading Phone alone.
Structural rights went the same way -- (CREATE, DELETE, READ *, WRITE *)
followed by a narrow re-grant lost create, delete and every write right,
which the report did not mention. Nothing was said at any point.
The reported trigger was wrong in a way that matters: WHERE is not
required. The same loss reproduces unconstrained and with READ *, so a
fix scoped to the constrained path would have satisfied the repro and
left most of the bug. The legacy engine has merged additively since it
shipped (mergeAccessRule), making this a regression in the codec engine
-- the default -- and explaining why the documented contract ("GRANT is
additive ... never removes permissions") held on one engine only.
A second defect surfaced while measuring the first: both engines matched
a stored rule on its module-role set alone, ignoring XPathConstraint, so
GRANT ... WHERE 'A' followed by WHERE 'B' folded the second onto the
first and overwrote its constraint. Mendix combines the rights of every
rule naming a role ("Rules are additive", refguide/access-rules), so one
rule per constraint is the ordinary way to write row-level security -- a
pattern MDL could not express. The constraint now belongs to the match
key on both engines, with the empty constraint treated as a value rather
than a wildcard, so constrained and unconstrained rules coexist and
re-running a script stays idempotent (ADR-0008).
Consequently formatAccessRuleResult needed the constraint too, or the
Result: line echoed a different rule's rights back at the user; REVOKE
passes anyXPath since it narrows every rule the roles appear in.
Rights merge on None < ReadOnly < ReadWrite, so a merge only ever widens.
Narrowing stays REVOKE's job, which keeps the two commands inverses
rather than two spellings of "set". The lattice is shared via mdl/types
so the engines cannot drift.
Verified: 7 new backend tests (failing first, with the reported
map[Email:None Name:None Phone:ReadOnly]); the reported repro end to end
on both engines; mx check 0 errors on the two-rule form; make check-mdl
green; 76 packages green.
Fixes mendixlabs#936
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
The code changes landed with syntax help, the quick reference and the examples updated, but not the two SKILLS that `mxcli init` syncs into user projects, nor the docs site. Those are where an agent or a developer actually looks: - rest-client.md has a "Body Types" section listing json/template/mapping. It now says there is no binary body on a consumed operation, that `body: file from $Doc` is refused as MDL-REST02, and where binary POST does live. `response: file as $Doc` is called out as unaffected. - write-microflows.md's REST CALL section gains a binary POST example next to the JSON-body one. - docs-site gains a "POST a Binary Body (File Upload)" section. The docs-site example is a complete microflow, so it was validated the way the rest of this work was rather than by eye — and the first version was WRONG: `RETURNS response` binds a System.HttpResponse, and returning it from a `RETURNS String` microflow is CE0117 at the end event. `mxcli check` passed it; mxbuild caught it. The committed version returns Boolean from the status code and is mxbuild-clean, and the page now states the constraint. `mdl-examples/bug-tests/rest-binary-post.mdl` was validated the same way (0 errors), since check-mdl only runs `mxcli check` on fixtures. make check-skill-mdl: 217 blocks checked, all pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
…Content cell Two defects reported as mendixlabs#935, both verified on Mendix 10.24.20.105674 (the reported version) and 11.13.0. **1. A dotted column target still corrupted the page (the crash).** mendixlabs#891 fixed the BARE form — `insert after NextRunAt { … }` — by refusing it and pointing authors at `grid.NextRunAt`. That form skips the guard entirely, because the target is legitimate. What is wrong is the pairing: the executor routes an all-`column` body to InsertColumns/ReplaceColumn, and anything else fell through to the generic widget path and was serialized into the grid's COLUMN list. So the fix for mendixlabs#891 named the route to the same unloadable document: System.InvalidCastException: Unable to cast object of type '…LayoutWidgets.DivContainers.DivContainer' to type '…CustomWidgets.WidgetObject' mx check aborts on load, before reaching a single check. Refusing at the pairing covers INSERT (before/after/into) and REPLACE at once. The column is resolved first, so a mistyped name still reports not-found with the available names rather than the refusal. Measured: "Altered page" then a load abort, before. Refused, and the project still checks at 0 errors, after. **2. A widget inside a customContent cell bound nothing.** The entity-context walk descended into a pluggable widget's own widget properties but not into an object-list ITEM's — a DataGrid2 column keeps its cell widgets one level deeper, at Objects[].Properties[content].Value .Widgets. That is the descent findInWidgetChildren gained in mendixlabs#834; this second walk never learned it, so ALTER PAGE could FIND those widgets (and mendixlabs#834's fix made that the recommended way to edit a cell) while building their bindings with an empty entity context. An association-navigating ContentParams path then could not resolve into AttributeRef + EntityRef steps and was stored as the attribute NAME: CE1613 at build time, and a describe that silently dropped the first hop. CREATE PAGE got the identical page right, which is what made this read as a storage bug rather than a context bug. The walk also never read a pluggable widget's datasource at all, so the grid's own entity was invisible even one level up. Both readers now share entityFromEntityRef, which also gives the pluggable one the IndirectEntityRef (association) case the plain one already had. The descent is keyed on the BSON shape, not on the "columns" property key, so Accordion groups and PopupMenu items are covered by the same code. Measured: CE1613 before, 0 errors after, with the two-hop path round-tripping through describe. Note on the controls: reverting fix 1 makes its unit test nil-deref in the fixture rather than reproduce the symptom, so the honest control there is the CLI plus mx check, run against a real 10.24 project. Fix 2's five tests do fail with the reported "" before the fix, including a false-positive control (a nearer DataView must still shadow the grid, and a widget outside every bound container must still report no entity). Repros: mdl-examples/bug-tests/935-alter-page-widgets-at-column-target.mdl and 935-customcontent-column-entity-context.mdl. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
fix(rest): request bodies — correct what was silently wrong, add what was missing, refuse what cannot be written
mendixlabs#923) DESCRIBE MICROFLOW renders control flow as nested if/then/else, which only works for properly nested graphs. When a branch re-enters a sibling's path the describer walks it as a tree and emits MDL that means something else, silently. Measured on the reporter's own graph, reconstructed from the coordinates in their describe output: with their actual expressions the original ALWAYS logs and the description NEVER does -- the exact inverse, not merely a semantic difference. The same root cause explains the tangled diagram they reported separately: findSplitMergePointsForGraph and commonMergeAfter disagree about which node is split1's merge, so the emitted @merge places it before an activity that structurally follows it. Records the negative BSON finding that forecloses the obvious design: Microflows$ExclusiveMerge has no name, caption or documentation field, per generated/metamodel, modelsdk/gen, and real 11.13 Studio Pro documents. Labels therefore cannot be stored -- but they do not need to be, since a label is an artifact of the emitted text and only has to be deterministic, not persistent. Proposes three modes: structured (today, unchanged), a faithful merge/join label form that also subsumes the @merge annotation, and an opt-in normalized form that recombines guards. Normalization is bounded by Bohm-Jacopini -- it works where the extra edges land on a shared suffix, and is refused where branches genuinely interleave, since that needs either activity duplication or a variable the user never wrote. Phase 0 is the detector, shipped as a lint rule so prevalence can be measured before Modes 1 and 2 are scheduled. It is independently the fix for the issue: a silent inversion becomes a named refusal. Refs mendixlabs#923 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
The skill covered the receiving half end to end — JSON structure, entities, import mapping, REST CALL — and said nothing about sending a body. Adds: - Step 6 listing the four inline REST CALL body forms (template, expression, export mapping, binary) - an upload example: the expression is the file document's Contents MEMBER, the content type goes on a header, and the parameter must be a System.FileDocument specialization - why a consumed REST CLIENT document cannot do this (MDL-REST02), so the contrast with the sibling skill is explicit - two gotcha rows: the CE0117 end-event type error when a `returns response` result is returned from a `returns string` microflow, and the 200-with-4-bytes upload that MDL-REST02 now refuses The complete microflow was validated the same way as the docs-site one — exec'd into a real project and checked with mxbuild (0 errors) — because check-skill-mdl skips the block and `mxcli check` alone would not have caught the return-type error that the last documented example shipped with. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
Two failures in one session came from trusting local state instead of checking
it, and both were reported to the user as fact before being caught.
1. The container is ephemeral and is re-cloned when reprovisioned, so commits
made earlier in a session can be absent from the working copy while still
present on the remote. It happened twice. The second time it was misread as a
code bug: a grammar rule was "missing" from the tree, and several turns went
into bisecting a parse failure in a binary built from the rolled-back tree.
The rule had been committed and pushed hours earlier.
2. Commits were pushed onto a branch whose pull request had already merged, and
described as "added to PR #N" when #N was closed and contained none of them.
A merged PR cannot take new commits.
SessionStart now reports, and stays silent otherwise:
- HEAD behind origin/<branch> — the stale-checkout signal
- bin/mxcli built from a different commit than HEAD (the Makefile already
stamps it via -X main.Version), so behaviour observed through the binary is
known to come from the code under test
PostToolUse on Bash runs after any command containing "git push" and reports a
branch that is behind origin/main, which is the state both PR failures shared —
main had absorbed the branch's earlier commits via the merge. It distinguishes
"behind with commits of its own" (a merged PR cannot carry them) from "behind
with none" (stale checkout or merged PR) and prints the recovery command for
each. Nothing is printed for the normal ahead-of-main push.
Naming the PR directly would be better, but this environment's egress proxy
intercepts api.github.com and answers 403 ("GitHub access is not enabled for
this session"), so a shell hook cannot ask. What it can compute locally is
enough to stop the wrong claim being made.
Matched on Bash rather than the hook's `if` filter: that is a PREFIX match and
would miss `git add … && git commit … && git push …`, which is how most of these
pushes are actually run. The script exits immediately on a non-push command.
Both hooks output through jq as JSON so the message reaches the user
(systemMessage) and the model's context (additionalContext) — the latter is
where the incorrect claim was made.
Verified by running session-start.sh directly and by firing the PostToolUse hook
with a temporary sentinel. The branch check earned its place immediately: it
found that d867785 (this branch's upload-section commit) was never in PR #193,
which merged at c6bd29c, and that restarting the branch from main had moved off
it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
…ully DESCRIBE MICROFLOW renders control flow as nested IF/THEN/ELSE, which only works when the graph is properly nested. A Mendix microflow is an arbitrary graph, and when a branch re-enters a sibling branch's path there is no nesting that means the same thing -- the describer emitted one anyway, with no error and no warning. Measured on the reporter's graph, reconstructed from the coordinates in their own describe output: the log activity ran on NOT c1 OR c2 and was described as c1 AND c2. With their actual expressions (not(true) on both decisions) the original ALWAYS logs and the description NEVER does -- the exact inverse, so re-executing it produced the opposite program. The separate complaint about the diagram coming back tangled is the same cause: findSplitMergePointsForGraph and commonMergeAfter are two independent merge-finders that agree on every nested graph and disagree here, so the emitted @merge lands before an activity that structurally follows it. Phase 0 of PROPOSAL_structured_microflow_description.md: detection only. - mdl/microflowgraph: post-dominance plus branch-body overlap, classifying an overlap with one entry as recombinable (the guards fold) and two or more as interleaved (needs activity duplication or a synthetic boolean per Bohm-Jacopini, so it is refused rather than rewritten). Deliberately does NOT reuse the describer's join search: mxcli has two and they disagree on exactly these graphs, so a detector built on either inherits whichever is wrong. It also cannot live in mdl/executor, which imports mdl/linter. - MDL-FLOW01 reports them, with different advice per class. - DESCRIBE emits a -- WARNING: comment naming the decision's position and refusing the round trip. Not registered in mxcli report: that score grades the model, and the model is valid -- what fails is mxcli's ability to describe it. The false positive would be worse than the bug, so the negative controls pin the shapes most likely to trip it: if with no else, an inner split whose join is the outer's, branches that both return, retry-loop back edges, and error-handler flows. Wiring proved with a forced-fire control against a real project, since a rule that never runs and a rule that finds nothing look identical. DgDemo reports the same 25 issues as before. Refs mendixlabs#923 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
fix(security): GRANT widens an access rule instead of replacing it
fix(alter-page): refuse widgets at a DataGrid2 column target, see into a customContent cell
chore(hooks): catch a stale checkout and a merged-PR push, plus the REST upload docs #193 missed
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.
REST request bodies (5 commits) — the import-range runtime failure, body: file sending the expression text, binary POST becoming expressible, the export-mapping body's three wrong properties, and file-document responses (#922)
Mappings and describe fidelity — multi-segment member paths (#927), MDL-MAP01, irreducible-graph description (#923)
Idempotent writes — $ID preservation, skipped-write reporting
Platform — mxbuild architecture refusal, Windows .exe and per-user JDK
Pages, widgets, security — contentparams (#928), alter page column targets, and GRANT widening rather than replacing an access rule
Contributor tooling — the two session hooks