Eighteen changes: two writes that left a project unopenable, SOAP measured against a real reference, and Mendix 11.14 - #1090
Merged
Merged
Conversation
…ead NewType `mxcli diff-local` failed with "mprcontents directory not found" on an MPR v2 project whose mprcontents/ was populated (mendixlabs#1080). The modelsdk engine — the default — never overrode Backend.ContentsDir(), so it fell through to the generated `unimplemented` stub and returned "". gen_unimplemented.go's promise that an unoverridden method "fails loudly rather than silently dropping data" is conditional on the method having an error to fail through: it emits errUnimplemented for a method with an error result, a panic for one with no results, and a silent zero value otherwise. ContentsDir is in the third bucket, and "" is not nonsense there — it is the in-band answer for "MPR v1", so the gap looked like a v1 project rather than a bug. Three fixes, plus a guard for the class: - ContentsDir() delegates to the reader. The reported defect. - ParseMicroflowFromRaw() decodes through the codec. It was the other unoverridden error-less method, and it returned nil — which the caller guards by substituting a "-- parse failed --" stub. That stub is a constant, so both sides of the diff rendered the same text and an edited microflow diffed to NOTHING while the summary still counted it as modified. Fixing ContentsDir alone would have shipped a diff-local that silently omits microflow changes. - attributeBsonToMDL reads raw["NewType"], falling back to raw["Type"]. "Type" is the SDK name; Mendix stores the type object under the storage name "NewType", so every attribute rendered "Unknown" — on both engines — and narrowing String(200) to String(50) produced an empty diff for the same reason. Pre-existing and engine-independent. - InvalidateCache() delegates to the reader. Found by the new guard; it takes no results, so its stub is a panic rather than a silent zero. Nothing routes through the interface to reach it today. TestErrorlessBackendMethodsAreImplemented reflects over FullBackend for methods that cannot report failure and go/parses the package for methods actually declared on *Backend — reflection cannot tell a promoted method from an override, since Go synthesises a wrapper named (*Backend).X for both. Removing the ContentsDir override makes it fail, naming ContentsDir. Verified against a v2 project: diff-local output is now byte-identical to `MXCLI_ENGINE=legacy` for an edited microflow and for an attribute length change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
A container in a DataGrid 2 control bar with `Action: nanoflow M.ACT_UnLink`, against a nanoflow declaring a required entity parameter, passed `mxcli check --references` with "Check passed!" and then failed the build with CE1571 "No argument has been selected for parameter 'LogisticWhitelist'" at that container (mendixlabs#1082). The rule that exists for exactly this walked `w.GetDataSource()` only, so the identical fault on an action slot was silent. Control, one run: the same microflow and the same missing argument as a data source is reported with the fix spelled out; as an action, check passes and mxbuild raises CE1571. The report's stated root cause — that a container's `Action:` takes no argument list — does not hold. `actionExprV3` has always carried `NANOFLOW qualifiedName microflowArgsV3?`, and buildContainerV3 hands the action to the same buildClientActionV3 a button uses. Three pages, one script, one `mx check` on 11.12.0: the bare action is CE1571, the same container with `($LogisticWhitelist = $dgMaterials)` is 0 errors and round-trips through DESCRIBE, the actionbutton workaround is 0 errors; dropping the bare page alone takes the project 1 -> 0. Action slots are found by sweeping a widget's Properties for *ast.ActionV3 rather than by a list of keys: `Action:`/`OnClick:` share one key, `OnChange:` has its own, and a pluggable widget's named slot uses its own. A key list would have fixed the reported slot and left the rest with the bug being fixed. A control bar is not row-scoped, and that belongs in the shared context walk. Six containers carrying one identical fault, one `mx check`: matching dataview -> no error; mismatched dataview -> CE1571; page parameter of the exact type -> CE1571; inside a column -> no error; no context -> CE1571; inside the control bar -> CE1571. Fixing it in the walk closed a latent false negative on the data-source half too — a dataview whose microflow source needs an argument is CE1571 in the control bar and clean in a column. It drops only the data widget's own object: an outer context still reaches the control bar (dataview > datagrid > controlbar > action, no argument -> no error), so the control bar is walked with its parent's incoming context, not an empty one. Against the measured project the rule's verdicts now match mxbuild's five errors widget for widget. Both guards have a control: stubbing the action walk fails 7 of the new tests, and restoring the control bar's inherited context fails the two control-bar tests with `got []`. The doctype gate gains the reported shape written correctly — removing its argument reproduces CE1571 at 'cToggleSelected' from mxbuild. The bug-test repro is a plain .mdl, not a .fail.mdl: the rule needs a project to read the flow's signature, and `make check-mdl` runs check without one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEZmExJUvn2nWTWE9mrd4i
…r's remedy `mxcli syntax page.action` listed `Action: NANOFLOW Module.NF` with no argument variant, two lines under `Action: MICROFLOW Module.MF(Param: $val)` and `Action: SHOW_PAGE Module.Page(Param: $currentObject)`. That is how an author concludes a nanoflow action takes no arguments, which is the conclusion mendixlabs#1082 was filed on. The topic now carries the argument form, says that a flow action is a CALL needing an argument per parameter (CE1571) on every widget that takes one — a clickable CONTAINER as much as an ACTIONBUTTON — and its example is a control-bar container passing the grid's selection. MDL-BUTTON01 advised "move the button into a grid column, or pass a page parameter" and never mentioned the selection, the one remedy that keeps the widget where the author put it. It now names it, carrying the data widget's own name down the walk, because that name is the variable: a grid `dgMaterials` with `Selection:` set exposes its selected object as `$dgMaterials`. Same correction in the create-page skill reference (with the row-scoped column beside the control-bar container, so the contrast is on the page), the docs-site create-page reference, the error cheatsheet's CE1571 rows, and the MDL quick reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEZmExJUvn2nWTWE9mrd4i
ako/TestApp carries three Studio Pro-authored SOAP calls (Mendix
11.14.0) and a published service. The previous commit asserted no such
document existed and used that to justify mirroring legacy. It existed;
I had not looked outside this repo.
Measured against Clients.GetOrders / GetCustomerOrders / SaveOrder,
legacy is wrong in five places, all of which the new writer reproduces
faithfully:
- ServiceName is the WSDL SERVICE name ("OrdersWS"), not the local part
of the imported service's qualified name ("OrderSoapClient"). Studio
Pro writes both, and they differ.
- ImportMappingCall.ContentType is "Xml" for a SOAP import mapping, not
the hardcoded "Json".
- Range.SingleObject follows the operation's cardinality — false for a
list result, not always true.
- VariableType is the result's real type (DataTypes$ObjectType with an
Entity, DataTypes$BooleanType), not always DataTypes$VoidType.
- A SEND MAPPING is Microflows$MappingRequestHandling {ContentType,
MappingId, MappingVariableName}. Legacy refused it citing
"Mendix$AdvancedRequestHandling", a name that appears in none of the
three documents — and MappingRequestHandling is a type this engine
ALREADY writes for REST.
A sixth: operation ARGUMENTS are
Microflows$WebServiceOperationSimpleParameterMapping entries inside
RequestBodyHandling.ParameterMappings, keyed by an escaped ParameterPath
("http%3A//www.example.com/:GetOrder|OrderId"). Both engines write that
list empty.
So `send mapping Mod.Export` and the call's arguments are SILENTLY
DROPPED today on both engines — parses, checks clean, execs green,
absent from the model. Same class as the CE0008 drop the previous commit
fixed.
No behaviour changes here. The parity work stands: it was scoped to
stopping the silent drop, and reproducing what ships is what makes it
safe to land. What changes is that the code now says which reference
each expectation came from, so "matches legacy" is not read as "is
correct". Fixing the six is follow-up work against those documents.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
A SOAP call naming a mapping and a service that REALLY EXIST produced a
project Mendix could not LOAD. Measured on ako/TestApp (11.14.0,
baseline 0 errors) with one statement:
$Res = call web service Clients.OrderSoapClient operation GetOrder
receive mapping Clients.SoapOrdersImportMapping;
ERROR: Mendix.Modeler.Storage.StorageLoadException … The text
'c2d1682f-09de-4cc7-95a3-82d5ee5ef243' is not a valid
ImportMappingIdentifier.
mx check stopped before validation. Two independent defects, both about
a name:
1. resolveMappingRefForWrite converted the receive mapping's qualified
name to the mapping unit's $ID. ImportMappingCall.ReturnValueMapping
is an ImportMappingIdentifier — Studio Pro writes
"Clients.SoapOrdersImportMapping" there — so a UUID is unloadable,
not merely invalid. The resolver is deleted; both fields now carry
the author's qualified name.
2. ServiceName was the local part of the imported service's qualified
name ("OrderSoapClient"). It is the WSDL <wsdl:service name=…>
("OrdersWS"), and Mendix resolves the operation WITHIN it, so the
wrong one hides every operation: CE0386 "Operation 'GetOrder' does
not exist in consumed web service". Now read off the imported service
document (Description.Services[].Name, disambiguated by which service
declares the operation), with the old derivation kept as a fallback
so an unresolvable service is no worse off than before.
Also ImportMappingCall.ContentType: "Xml", not the hardcoded "Json" — a
SOAP response is XML, and both reference calls carrying an import
mapping write Xml.
All three are fixed in BOTH engines, so they stay byte-identical.
Why it survived: the UUID substitution only happened when the lookup
SUCCEEDED, and the only SOAP fixture names a service and mappings that
do NOT exist — deliberately, to show dangling references. exec took the
fallback every time and the qualified name survived. A valid reference
was the one input that triggered it, and no test used one. A test had
even locked the ids in as expected (cmd_microflows_builder_webservice_test.go);
it is corrected, with the reason.
Verification, working the chain on TestApp rather than stopping at the
first green — each error was hidden by the one before it:
StorageLoadException -> CE0386 -> CE0243 + CE0366 + CE0178
The three that remain are the three known divergences still open, now
each tied to the error it causes: VariableType (CE0243/CE0366) and
operation arguments (CE0178, which also needs MDL syntax —
callWebServiceStatement has no argument list). Both are documented in
microflow_webservice_write.go against the reference documents.
Revert control: stubbing resolveWebServiceName fails the new tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
A SOAP call assigning its result was rejected twice over, on a call
whose receive mapping plainly produces an entity:
[CE0243] "The mapping used to return a value of type 'Nothing', but
now returns a value of type 'Clients.Order'."
[CE0366] "Cannot store in variable when there is no return value."
Both engines wrote NewResultHandling.VariableType as DataTypes$VoidType
unconditionally. Void means the call returns nothing, so it contradicts
the mapping and makes the assignment illegal in its own right. Studio
Pro writes the entity the mapping produces — the Entity of the import
mapping's root ImportMappings$ObjectMappingElement.
resolveImportMappingEntity reads it off that document, the same way
ServiceName is read off the imported service. VoidType stays the
fallback when the mapping cannot be resolved, so nothing gets worse and
nothing is guessed.
Measured on ako/TestApp (11.14.0, baseline 0 errors), the same
one-statement script, four rounds — each error invisible until the
previous fix landed:
StorageLoadException -> CE0386 -> CE0243 + CE0366 -> CE0178
3 errors -> 1. What remains is CE0178 "Body parameter mapping needs to
be refreshed": the operation's ARGUMENTS. That one cannot be written at
all yet, because MDL has no syntax for it — callWebServiceStatement
carries no argument list — so it is a language change, not a writer fix.
Range.SingleObject also still differs from the references, and is left
alone deliberately: no error has been measured from it, and it is not
simply the mapping's cardinality (the reference roots carry MaxOccurs 1
while the calls carry SingleObject false), so changing it today would be
a guess.
Revert control: stubbing resolveImportMappingEntity fails the new test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Four of the six are now fixed in both engines, so most of that test agrees with Studio Pro as well as with legacy. The note still claimed it pinned all five as-is, which would have read as 'these values are known wrong' to the next person changing them. It now says which expectation is a deliberate fallback and which two divergences are genuinely open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…066) `mxcli marketplace update` exited 0, reported its units copied and identities preserved, and left the project at CE0066 "Entity access is out of date" at the updated module's domain model. Its own "Next, repair what a headless update leaves behind" block named CE0463 and CE6087 only, so the error read as needing Studio Pro — reported as "no headless fix available", which is what breaks an unattended upgrade pipeline. `marketplace install` had the same gap. A transplant is the one mxcli write path that does not reconcile as it writes: TransplantModule copies the incoming module's units in verbatim, and RestoreRoleGrants runs its statements one at a time rather than as a program, so finalizeProgramExecution — the only caller of ReconcileMemberAccesses outside the entity, grant and association handlers — never runs. An access rule that does not cover every member of its entity therefore arrived exactly as the package shipped it. PerformUpdate and PerformInstall now reconcile the module they copied in and report the count. The failure path is non-fatal — the module is already in place, so aborting would leave the project mid-update — and names `update security <Module>` as the repair. A module whose rules are already complete is not rewritten. That is the other half of the contract rather than an optimisation: reconciling unconditionally would make every install differ from its package, `marketplace diff` would read that as a local edit, and a local edit is what makes the next update refuse. Measured on a Mendix 11.6.6 app with Administration 4.3.2, mxbuild 11.14.0. The control first, since it is what localises the defect: transplanting the module unchanged checks at 0 errors, so the machinery does not create the stale rule, it transmits one. Removing one member entry from the incoming version's rules then gives exactly the reported CE0066, and 0 errors with the fix. Tests run over both engines, and fail with the reported symptom when the fix is stubbed out. Also documents UPDATE SECURITY, which has done this repair since mendixlabs#1047 but appeared in no user-facing surface — not the docs site, not MDL_QUICK_REFERENCE.md, not `mxcli syntax`, not the update's output — only in a skill and the changelog. Refs mendixlabs#1085 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-styling-1fgvpm # Conflicts: # CHANGELOG.md
`alter workflow M.W insert outcome 'X' on decision9 { };` printed "Altered
workflow", and the project then could not be LOADED: mx check died at "Loading
the mpr file" with System.InvalidCastException (UserTaskOutcome ->
ConditionOutcome) at UnitContentsLoader.FillProperties, before validating
anything, so Studio Pro would not open it either (#415). The MDL-WF04
class — the blast radius is the whole project — and every mxcli-side command
reported success, which is what let it survive later scripts.
An activity's outcome list is typed, and each inserting op writes exactly one
outcome type into it: INSERT OUTCOME a UserTaskOutcome, INSERT PATH a
ParallelSplitOutcome, INSERT CONDITION a *ConditionOutcome. generated/metamodel
types the receiving list per activity; none of the three ops read what it was
pointed at, in either mutator implementation.
The report named one combination. There are six, all measured on mxbuild 11.10.0
against a project first established at 0 errors:
INSERT OUTCOME on a decision / parallel split -> InvalidCastException
INSERT PATH on a decision / user task -> InvalidOperationException
INSERT CONDITION on a user task / parallel split -> InvalidOperationException
All six are refused now, in validateAlterWorkflowRefs — the function
`check --references` and `exec` already both call, so the two passes cannot
drift and no backend-interface change was needed. The guard is a type switch
over the semantic activity types rather than a table of storage-name strings, so
a kind cannot inherit a wrong answer by being absent from a list, and an
unmodelled kind is accepted rather than refused. The refusal names the op that
does fit the target instead of only saying no.
Two measurements kept the scope honest, both counter-intuitive:
- INSERT BOUNDARY EVENT on a decision does not corrupt — it is a silent no-op,
the document stays byte-identical while "Altered workflow" is printed. It is
refused as a reports-success-does-nothing defect, not as a load failure.
- The DROP ops, left open as unmeasured in the issue, are not the same defect.
Removing an element cannot write a wrong type: DROP OUTCOME and DROP PATH on a
decision each leave the project loadable at one ordinary CE6686.
A resolution failure is deliberately not a refusal — an ambiguous or unknown
activity ref returns nil and the mutator reports it, with the @n remedy.
The fixture is not a .fail.mdl: the guard needs the stored workflow to know the
target's kind and `make check-mdl` runs check with no project, so the bug-test
documents the six corrupting forms in comments and executes the correct form of
each (0 errors on mxbuild). `mxcli syntax workflow.alter` documented only two of
the four INSERT ops, which is how an author reaches for the wrong one; all four
are there now with the kind each takes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L45JFiJ6y58ftg2zWVEq7h
The three references a SOAP call stores — the imported service and the send
and receive mappings — are BY_NAME_REFERENCE properties. What is stored IS
the qualified name, so there was never anything to resolve.
Five functions resolved them anyway, and none could ever match:
- resolveWebServiceReference asked ListRawUnitsByType for
"WebServices$ImportedWebService". Nothing is stored under that name; the
stored $Type is "WebServices$ImportedServiceImpl". ImportedWebService is
the SDK name, which is why both generated sources use it.
- It then compared unit.ID against a value that was never an id. Measured
on ako/TestApp (11.14.0), Clients.GetOrders stores ImportedService
"Clients.OrderSoapClient"; modelsdk/gen registers the property as
codec.RefByName, and a test two packages away was already passing
"Mod.Service" through the same field.
- The two mapping resolvers compared the same way, against
ReturnValueMapping "Clients.SoapOrdersImportMapping".
Every call therefore fell through to a fallback returning the stored string,
which is the right answer. Nothing printed was ever wrong, and that is
exactly why this stood: a resolver whose fallback is correct cannot be
caught from its output. TestFormatAction_WebServiceCallResolvesKnownReferences
covered it and passed by building a world where ServiceID was a unit id and
the unit type was ImportedWebService — a fiction no project produces.
The replacement asserts on the input side instead: the mock backend fails the
test if it is consulted at all. Reverted against the old code it fails with
"DESCRIBE consulted the project for WebServices$ImportedWebService".
Worth recording alongside: the structured DESCRIBE branch is unreachable for
real calls in any case. All three of TestApp's SOAP actions carry 15 keys,
webServiceActionRequiresRawBSON supports 9, and mxcli's own writer emits the
same 15 — so today every SOAP call, hand-written or ours, describes as
`call web service raw '<base64>'`. Left as is here; it is what the operation
arguments proposal addresses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…mapping
CALL WEB SERVICE can say who to call and what to do with the answer, and
cannot say what to send. The two halves fail in opposite directions, and
both are measured against ako/TestApp (11.14.0, baseline 0 errors):
- `send mapping X` parses, is accepted, and is discarded. The mapping
name appears zero times in the written document on either engine, and
mxbuild rejects the call as CE0369 "Cannot use simple request body, as
the operation's body is complex".
- Operation arguments have no syntax at all, so a call to an operation
that takes parameters is CE0178 — the last unresolved error in the
four-round chain the CE0386 work recorded.
Proposed together because they are the two branches of one stored property,
Microflows$RequestBodyHandling, and today a statement can ask for both and
get neither. §3.1's refusal is only enforceable once both exist.
The syntax reuses callArgumentList, the named-argument form five other call
statements already use, attached to `operation` because ParameterPath is
built from the operation's RequestBodyElementName — which the imported
service document carries in structured form, so the author writes OrderId
and not http%3A//www.example.com/:GetOrder|OrderId. The send mapping gains
the variable it maps from, which is the half the current clause cannot
express even in principle.
Also records, since it is the same property: the structured DESCRIBE form
for SOAP is unreachable today. Real calls carry 15 keys and 9 are
representable, so every SOAP call — Studio Pro's and ours — describes as
base64. Six of the seven remaining keys are boilerplate; the seventh is
this proposal.
§6 lists what is not settled rather than leaving it to be assumed: a
ContentType of "Json" on an XML request seen in exactly one document, an
unmeasured ParameterName, the advanced per-parameter mapping with no
reference, and nested parameters that could change the path derivation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Check a widget action's arguments (CE1571), and teach the argument form
SOAP client: stop writing a project Mendix cannot load, and three names it cannot resolve
fix(marketplace): reconcile a transplanted module's access rules (CE0066)
fix(diff-local): implement the engine methods diff-local needs, and read NewType
One conflict, in CHANGELOG.md: both sides added an entry at the top of Unreleased/Fixed (this PR's #415 entry and main's mendixlabs#1085 marketplace-reconcile entry). Both kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L45JFiJ6y58ftg2zWVEq7h
fix(workflow): refuse an ALTER insert aimed at the wrong activity kind
…#146 gate Reported by a project as: `mxcli run --local --watch` refuses to start with `no rollup.config.mjs in deployment/web`, while plain `run --local` on the same project works. They routed around it with a restart per change. This is ako/mxcli-ledger #146 again, in the gate that fix did not touch. BuildWebClient exists to close a gap 11.13 and earlier left — client source plus a rollup config, but no bundler run — and its gate tested for the CONFIG, so it failed on 11.14, where the config is absent because its purpose has been served. #146 moved that gate onto the gap. But StartWebClientWatch carries its own independent os.Stat of the same file, and #146 changed only webclient.go, so `run --local` started working on 11.14 and `--watch` kept dying one call earlier on the same absent file. Its commit message asserted "both call sites are fatal"; the two it meant were both BuildWebClient's. Measured on a blank app, clean deployment, version as the only variable: 11.13.0 rollup.config.mjs PRESENT dist/index.js ABSENT 11.14.0 rollup.config.mjs ABSENT dist/index.js PRESENT On 11.14 there is no bundler to keep hot, so StartWebClientWatch returns a nil watcher rather than an error. That is only half the fix: the watch loop calls Generation, WaitForRebuild and Stop with no nil branch, so every method is nil-receiver-safe. Against the unfixed build the new tests fail in both halves — the gate test with the reporter's exact message, the nil-safety test with a SIGSEGV, which is what proves the second half is load-bearing rather than defensive. A control keeps the 11.13 path launching the bundler, so the fix cannot degrade to "never watch anything". Verified on real apps at both versions, `run --local --watch`: 11.13.0 bundler launched; build #2 applied via reload in 3.379s, client re-bundled (gen 2) 11.14.0 "Web client bundled by mxbuild; no incremental bundler needed"; app boots and serves That 11.14 run then exposed a SECOND, independent defect, which the gate had been hiding: the cold build creates only web/dist, but every incremental build writes the pre-11.14 one-file-per-page client into web/pages/ and web/layouts/ and fails on all of them at once. So on 11.14 the first build succeeds and every rebuild fails — reachable only through --watch, because nothing else asks for a second build. Creating the directories is not a workaround: measured, the same build then fails exporting pluggable widgets, because the incremental path is the older client end to end. The 11.13 control has both directories after its cold build, which is why it does not happen there. mxcli cannot fix that from the outside, but it must not let it read as the user's model being broken — the raw message names four absolute paths inside deployment/ and nothing about why, so the natural response is `rm -rf deployment/`, which costs a cold build and changes nothing. legacyClientBuildHint recognises it by its own shape (the message names a missing path under one of those directories AND the directory is actually absent), never by Mendix version, so a future mxbuild that fixes this goes quiet on its own and no other build failure is explained away. Verified firing on the live 11.14 app, and silent on model errors, SCSS errors, a widget-export failure, an unrelated missing path, and the 11.13 shape. Docs say plainly that --watch is not usable on 11.14 yet, with the restart-per-change command that is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…undler
Follow-up to 77849ffd, which diagnosed the second defect as "the
incremental build falls back to the pre-11.14 one-file-per-page client".
That was plausible, quotable and wrong, and the control that disproves it
is one byte in the model.
Three measurements, taken with mxcli removed from the picture entirely:
1. POST the SAME /build request twice to `mxbuild --serve` over curl, with
the model untouched between them:
build 1 Success
build 2 Failure — Compilation of the app bundle failed
ERR_MODULE_NOT_FOUND: …/deployment/web/rollup.config.mjs
imported from …/modeler/tools/node/rollup-runner.mjs
mxbuild's own bundler runner, on mxbuild's own second build, importing
the config file mxbuild's own first build declines to leave behind.
2. One-shot `mxbuild --target=deploy` run TWICE into the same deployment
directory: BUILD SUCCEEDED both times, `web/dist` only. So the 11.14
deployment shape is not the trigger — a fresh process is perfectly happy
with the directory a serve process chokes on. It is the serve process's
own state after its first build.
3. Flip EnableRspackBundler in the model (App Settings > Runtime > App
bundler) and repeat: IDENTICAL failure, naming rspack.config.mjs —
"Failed to load Rspack configuration file". Nothing falls back to
anything; both bundlers fail the same way, and switching bundlers is not
a workaround.
So the accurate statement is narrower and stronger: on 11.14 the first
build in a serve process does not leave the deployment in a state its own
incremental build can continue from. Neither the bundler config nor
web/pages/ + web/layouts/ survive it, and the next build needs both —
which one it dies on is only how far it gets before it needs one. The
missing-web/pages/ error I first saw is the model-changed path; the
bundler-config error is the model-untouched path.
The hint now covers both shapes (the bundler-config one puts its detail in
the response body, not the message, so build.Raw is passed too) and both
config file names. It still gates on the failure's own shape — the message
names an artifact AND that artifact is genuinely absent — so a fixed
mxbuild goes quiet on its own and no unrelated failure is explained away.
It now also says what does NOT work: `rm -rf deployment/` (the next second
build fails identically) and switching bundlers. Both are what a user
reaches for first, and neither costs less than a cold build.
Tests carry the real payloads for all three cases, Rollup and Rspack, with
the both-artifacts-present control for the pre-11.14 shape.
Also worth recording, since it bears on how much to expect here: --serve,
--host and --port appear in `mxbuild --help` but NOT in the reference guide
at docs.mendix.com/refguide/mxbuild/, which documents only the four
--target modes and exits-after-building. The interface the warm loop is
built on is undocumented and carries no compatibility promise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The deleted bundler config is recoverable, so it was worth ruling out
properly rather than assuming. It sits on disk for ~1.5s mid-build, and
the captured file carries nothing model-specific: no page list, no widget
list, no hashes, `input` is just index.js and page discovery is delegated
to rollup-plugin-mendix-pages at build time. Its only app-specific content
is absolute paths. So it could be harvested and put back, the way
`mxcli fix widgets` already harvests mxbuild's own output.
Measured against mxbuild 11.14.0 over its HTTP API, one request at a time:
build model deployment state result
1 - cold Success
2, 3 unchanged config restored Success
2' CHANGED config restored Failure — missing
web/pages/*.js
3' CHANGED config restored + dirs Failure — pluggable
widget export
Across 2' and 3', web/dist/index.js never moved off its cold-build
timestamp, and the widgets it refuses to export (Image, LanguageSelector)
ship with the blank app untouched.
So restoring the config rescues exactly the case a warm loop never needs —
rebuilding a model that did not change — and there are TWO regressions
here rather than one. The config deletion is the visible, recoverable half.
Underneath it the per-document client export expects deployment state
11.14's cold build no longer produces, and nothing outside the process
addresses that. This file therefore reports rather than repairs, and now
says so with the evidence.
The general lesson, recorded in the finding: test a workaround against the
case the feature exists for, not the case that is easy to set up. An
unchanged-model rebuild was quick to arrange and would have shipped a
green result for a fix that helps nobody.
No behaviour change — comment, docs and finding only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Start --watch on a Mendix 11.14 app, and explain the mxbuild defect underneath it
Reported in ako/ChipCoV1's FINDINGS.md. `[Ticket_Reporter = $currentUser]`
passed `mxcli check --references`, `exec` reported success, and mxbuild
then failed:
ERROR at MyFirstModule, Microflow 'MF_MyTickets', Retrieve object(s)
activity 'Retrieve list of Ticket from database':
Error(s) in XPath constraint.
An association named in a constraint must be QUALIFIED; an attribute is
bare. Nothing checked the difference.
Reproduced on a blank Mendix 11.14.0 app with the qualification as the
ONLY variable between two runs of the same script:
[Ticket_Reporter = $currentUser] check passed → build exit 3
[MyFirstModule.Ticket_Reporter = $currentUser] check passed → BUILD SUCCEEDED
That makes it the expensive shape rather than a cosmetic one: `exec`
applies statements one at a time and cannot roll back, so a script
carrying this in the middle writes everything before it and stops at the
build, leaving the model half-updated with nothing having reported a
problem. It is the same class as the ALTER PAGE SET and MDL-GRANT02 gaps.
The rule is deliberately narrow. It fires only on a bare name that is NOT
an attribute of the constrained entity AND IS a known association, so it
names the spelling to use instead of merely suspecting one. That is also
why it needs no XPath keyword list: `and`, `or`, `not` and `contains` are
not association names, so they cannot match. A name inside a string
literal is skipped, with Mendix's doubled-quote escaping tracked — without
that a literal swallows the rest of the constraint and a real reference
after it is missed. An association name defined in two modules is dropped
rather than guessed at, because offering one of two spellings would be
wrong half the time.
It reads the SCRIPT's declarations as well as the project's, which is most
of the value: the common shape is one script creating the entity, the
association and the microflow that constrains on it.
Worth recording how the first cut was wrong, because the trap is
structural. `scriptContext` has TWO parallel collectors over the same
statement types — `collectDefinitions` (whole program, up front) and
`collectSingle` (incremental) — and the cases went into `collectSingle`
only. The rule therefore fired against STORED associations and stayed
silent on script-declared ones, which is the majority shape and the one
that reaches a build half-written. Every unit test stayed green, because
the matcher was correct and was simply never given the data; the miss
surfaced only on running the real .mdl through `make check-mdl`. Both
collectors now call one `recordAssociation`/`recordEntityAttrs` helper,
and `TestBothCollectorsRecordAssociationsAndAttrs` asserts they agree —
reverting the `collectDefinitions` case fails it with
`associations[Ticket_Equipment] = ""`.
The negative case is a Go test rather than a `.fail.mdl`, because this
rule needs a project and `make check-mdl` runs `mxcli check` with none: a
`.fail.mdl` here would pass silently and assert nothing. The `-ok.mdl`
stays as the positive control, so the rule cannot degrade into rejecting
every XPath constraint and still look green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported in ako/ChipCoV1's FINDINGS.md.
The record-narrated-demo skill requires a mobile pass — "Recorded at both
a desktop viewport and a real mobile device profile" is a checklist item,
and the section above it argues at length for why that pass earns its
place. `take.js` could not perform one. `openTake` called
browser.newContext({ viewport, recordVideo })
and nothing else, while `userAgent`, `isMobile`, `hasTouch` and
`deviceScaleFactor` are all CONTEXT options. Mendix picks its navigation
profile from the USER AGENT and not from the viewport, so a take that only
narrowed `viewport` filmed the DESKTOP app in a narrow window: the phone
profile was never routed to.
That is the bad kind of broken. The take succeeded, produced a plausible
film, and simply filmed the wrong thing — a verification step that cannot
fail, counted as done. ChipCo shipped an app whose phone walk was
impossible while seven browser tests stayed green.
`contextOptions` now passes straight through, spread FIRST so `viewport`
and `recordVideo` still win: both are load-bearing for the cut, and a
device preset carrying its own viewport would silently letterbox every
take.
Two fixes to the caption plate in the same skill, from the same report:
- `#demo-narration` had no `pointer-events: none` — only `#demo-spot` did.
The plate is a full-width bar pinned to the bottom of the viewport,
which is exactly where Mendix puts a page footer's buttons, so any
control it covered became unclickable and Playwright retried for 30s
against "div.text from div#demo-narration subtree intercepts pointer
events" before failing the take. It killed a take on an Approve button.
The caption is read, never clicked, so it gives up pointer events free.
- `body { padding-bottom }` reserves the plate's height. pointer-events
makes a covered control CLICKABLE; this makes it VISIBLE, and the film
needs both — a click that lands under an opaque caption is a beat the
viewer cannot see happen, which is the same dead beat by another route.
Applied at install, before the take starts, so nothing shifts mid-shot.
The skill now documents the device-profile spelling, and records the fact
that makes a mobile take readable: a Mendix page carries its own layout
and the layout names the navigation profile, so the Phone profile controls
the home page and the menu only. A phone user routed to a page built on a
desktop layout gets the desktop frame whatever profile routed them there —
measured at 430x932, a 232px rail on a 430px screen with the row's action
42px past the right edge. The take is right to fail on that; the fix is
per-page layouts, not a theme tweak.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All four are the same mistake in different clothes: a value or an artifact inherited from somewhere it no longer describes. 1. The SessionStart hook re-downloaded 85 MB on EVERY fresh session when mxcli was already installed on PATH. bootstrap-mxcli.sh gated only on `[ ! -x ./mxcli ]` and never consulted PATH, while the bootstrap skill tells you to `rm -f <AppName>/mxcli` after moving the project to the repo root — so in a session image with mxcli pre-installed the guard could never be satisfied by the binary already on the machine, forever. It now hardlinks a PATH copy in (what `mxcli new` itself does), falling back to a symlink across filesystems then a copy, and downloads only when none of those work. Verified with both controls, which matter more than the happy path: nothing on PATH still downloads rather than silently no-opping, and an existing ./mxcli is left byte-identical. 2. `theme create --from <design>` seeded the palette and kept everything else from the base, so a scaffolded brand theme still described itself in `theme list` as "Cool slate, one teal signal colour" with Signal's six swatches — the one command whose job is to tell themes apart showing the wrong one. The colorway is derived from the seeded brand/info/success/warning/danger/ink-muted, falling back to the base's for entries a design did not declare (a partial seed is normal, and a hole in the swatch row reads as a defect). The summary names the design file rather than describing colours that are gone; --summary still wins. 3. It also vendored ~500 KB of IBM Plex woff2 the seeded --mxt-font never names, plus a SIL OFL licence for fonts the theme does not use. A family the seeded fonts no longer name now loses its @font-face rules AND its files, per family — a brand theme routinely changes its body font and keeps the mono one for code, and shipping four unused Sans weights because Mono survived is the same dead weight in smaller print. A design that says nothing about fonts keeps them all, which is the right default: dropping a font nobody asked to change would break the scaffold's own rendering. Two traps worth recording. The decision is made BEFORE the walk, because it is taken by reading the partial and applied to files elsewhere in the tree — inline, it depended on WalkDir's lexical order putting `_mxcli-<name>.scss` before `mxcli-fonts/`, true today only because of the leading underscore. And it touches two halves, where getting either alone wrong is silent: a surviving rule for a deleted file 404s in the browser, a surviving file nothing loads is the dead weight being removed. Unit tests on each half cannot catch a mismatch, so the guard is an integration assertion that a scaffolded theme ships exactly the fonts it loads — stubbing the file half fails it with "ibm-plex-sans-latin-700-normal.woff2 is shipped but no @font-face loads it". 4. The primary button was never the brand colour. Atlas derives --btn-primary-bg from --brand-primary-600 = color-mix(in srgb, <brand>, <contrast> 20%), so a brand blue #10069F rendered rgb(21,13,140) and Signal's #0f6e6b is likewise shifted. This also settles a mismatch inside the Atlas map. --btn-primary-color was pinned to --mxt-brand-ink, an ink each theme picks to sit on --mxt-brand — console pairs near-black #04211d with bright teal #2dd4bf — while the background was left to Atlas's derivative, so the pairing the theme designed for was never the pairing that rendered. The comment two lines above already called it "a brand-filled button"; now it is one. Hover and active keep Atlas's relationship to the base rather than being pinned flat, so a theme setting --mxt-brand-hover gets a real hover and one that does not still gets Atlas's. Measured in compiled CSS on a real 11.14.0 build: --btn-primary-bg: var(--mxt-brand), after Atlas's declaration, and the theme compiles last. BUILD SUCCEEDED. All three maps stay byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…a requirement
Reported in ako/ChipCoV1's FINDINGS.md — two `brain plan` failures in
opposite directions, both through `catalogResolver.Resolve`, needing
opposite fixes.
A requirement anchored at a bare MODULE reported BUILT the moment the
module existed. Two of theirs (@Maintenance for the theme, and for the
device profiles) read as built after slice 01 with none of their work
done.
A requirement anchored at a MODULE ROLE reported PLANNED forever, even
once the roles existed: `describe` refuses a role ("no describable
document named …") and `mxcli refs` finds nothing.
The module-role half is a LOOKUP gap. A role is in neither the catalog's
objects view nor FindDocumentUnit, because it is not a document, so both
lookups miss and it falls through to NotFound — which for a requirement,
whose anchors point FORWARD, means "not built yet" permanently. The
resolver now consults GetModuleSecurity. Case-insensitively, because
Mendix treats role names that way and an anchor is hand-written, so a
case-only mismatch reporting "not found" would be exactly the false
staleness the three-state resolver exists to prevent.
Verified end to end on a real 11.14.0 project, both directions:
@MyFirstModule.User 1 entries, 1 anchors, 1 resolved OK
@MyFirstModule.NoSuchRole NOT FOUND exit 1
The second line is the one that matters — without it the fix could be
"resolve every three-part anchor" and the first line would look identical.
The module half is SEMANTIC and no lookup can fix it: the anchor resolves
perfectly and is still useless, because resolution IS the progress signal
and a module resolves long before the work inside it. NewRequirement
refuses it at capture time and names the alternative — an author told only
"no" deletes the anchor, which loses the measurement rather than fixing
it. The failure it prevents is silent and flattering: the plan reports
progress that has not happened and nothing else in the system disagrees.
A module anchor stays legal on a DECISION and on a QUESTION, whose anchors
point backward — "this module exists" is exactly what a cross-cutting
decision about it asserts. That control is what stops the fix degrading
into "refuse every module anchor" while its negative test still passes.
The skill drops ChipCo's workaround advice ("anchor at documents only"),
which this makes obsolete for roles, and keeps the part that is still
true: a theme has no model element, so anchor the branding requirement at
the branded layout the slice adds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven defects from a real 11.14 app build (ChipCoV1 findings)
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.
Eighteen commits. The sharpest group is two writes that reported success and left a project Mendix could not LOAD — worse than a build error, because
mx checkdies before validating and Studio Pro will not open the file either. Then SOAP measured against a real reference document for the first time, and Mendix 11.14.Writes that made the project unopenable
An
ALTER WORKFLOWinsert aimed at the wrong activity kind (ako/mxcli#415).insert outcome 'X' on decision9printed "Altered workflow", and the project then failed at "Loading the mpr file" withInvalidCastException (UserTaskOutcome -> ConditionOutcome), before validating anything. An activity's outcome list is typed and each op writes exactly one type into it; none of the three ops read what they were pointed at, in either mutator. The report named one combination — there are six, all measured.A SOAP call naming a mapping that really exists.
resolveMappingRefForWriteconverted the receive mapping's qualified name to the unit's$ID, butImportMappingCall.ReturnValueMappingis anImportMappingIdentifier— Studio Pro writes"Clients.SoapOrdersImportMapping"there, so a UUID is unloadable: "The text 'c2d1682f-…' is not a valid ImportMappingIdentifier."mx checkstopped before validation.SOAP, measured against a real reference
The previous round asserted no Studio Pro-authored SOAP document existed and mirrored legacy on that basis. One exists. Measured against three real calls and a published service on 11.14.0, legacy is wrong in five places and the new writer had been reproducing all five faithfully:
ServiceNameis the WSDL service name, not the local part of the imported service's qualified name — Studio Pro writes both and they differ.ImportMappingCall.ContentTypeis"Xml"for a SOAP mapping, not the hardcoded"Json".Range.SingleObjectfollows the operation's cardinality, not alwaystrue.VariableTypeis the result's real type, not alwaysDataTypes$VoidType— which contradicted the mapping and made the assignment illegal in its own right (CE0243 + CE0366).DESCRIBEprinted resolved guesses: five resolvers existed for properties that areBY_NAME_REFERENCE, where what is stored IS the qualified name. One asked forWebServices$ImportedWebService; the stored$TypeisImportedServiceImpl. None could ever match.A proposal covers the half still missing:
CALL WEB SERVICEcan say who to call and what to do with the answer, and cannot say what to send.send mapping Xparses, is accepted, and is discarded (then CE0369); operation arguments have no syntax at all (CE0178). Both branches of one stored property, so proposed together.Mendix 11.14
run --local --watchcould not start a 11.14 app — the second copy of a gate whose first copy was fixed months ago, sorun --localworked and--watchdied one call earlier on the same absent file. The fix returns a nil watcher and makes every watcher method nil-safe; without the second half the startup refusal becomes a segfault on the first edit.Fixing it exposed a defect in mxbuild 11.14 itself: the first build in an
mxbuild --serveprocess does not leave the deployment in a state its own incremental build can continue from. Measured over mxbuild's own HTTP API with no mxcli involved — the same/buildrequest twice, model untouched — Success then Failure,ERR_MODULE_NOT_FOUNDon the config file imported by mxbuild's ownrollup-runner.mjs. Three remedies measured, none work: switching the app bundler, deletingdeployment/, or restoring the deleted config (which rescues only the case a warm loop never needs). Escalated to Mendix. mxcli reports it by its own shape rather than by version, so a fixed mxbuild goes quiet on its own.Worth noting
--serve,--hostand--portare inmxbuild --helpbut not in the MxBuild reference guide. The warm loop rests on an interface with no published contract.Checks that passed a script the build then rejected
A widget action's missing arguments (
mendixlabs/mxcli#1082) — CE1571. The rule that exists for exactly this walkedGetDataSource()only, so the identical fault on an action slot was silent. The report's stated root cause did not hold: a container'sAction:has always taken an argument list, proven with three pages and onemx check.An unqualified association in an XPath constraint — CE0161. Qualification as the only variable between two runs: unqualified passes
checkand fails the build; qualified passes both. Expensive rather than cosmetic, sinceexeccannot roll back.A silent engine gap, and a headless pipeline
diff-localfailed with "mprcontents directory not found" (mendixlabs/mxcli#1080). The default engine never overrodeContentsDir(), so it fell through to the generatedunimplementedstub — whose promise to "fail loudly" is conditional on the method having an error to fail through.ContentsDirhas none, so it returned"", which is the in-band answer for MPR v1. The gap looked like a v1 project.marketplace updateexited 0 and left the project at CE0066. A transplant is the one write path that does not reconcile as it writes, and the command's own repair block named CE0463 and CE6087 only — so the error read as needing Studio Pro, which breaks an unattended pipeline.Seven defects from building a real app
take.jscould not record the mobile pass its own skill mandates.userAgentis a context option and was never passed, and Mendix picks its navigation profile from the user agent rather than the viewport — so the "mobile" take filmed the desktop app in a narrow window. A verification step that cannot fail, counted as done.brain planmismeasured anchors in opposite directions — a module anchor reported built immediately, a module-role anchor planned forever.PATH.theme create --from <design>seeded the palette and nothing else — wrong summary and swatches, plus ~500 KB of unused webfont.One reported item was disproved before it cost anyone work: the phone/tablet topbar is not broken in the shipped themes. Compiled CSS shows
--m-header-bgis avar(--topbar-bg)reference, which the themes already bind.