Skip to content

Eighteen changes: two writes that left a project unopenable, SOAP measured against a real reference, and Mendix 11.14 - #1090

Merged
ako merged 30 commits into
mendixlabs:mainfrom
ako:main
Sep 11, 2026
Merged

Eighteen changes: two writes that left a project unopenable, SOAP measured against a real reference, and Mendix 11.14#1090
ako merged 30 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

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 check dies 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 WORKFLOW insert aimed at the wrong activity kind (ako/mxcli#415). insert outcome 'X' on decision9 printed "Altered workflow", and the project then failed at "Loading the mpr file" with InvalidCastException (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. resolveMappingRefForWrite converted the receive mapping's qualified name to the unit's $ID, but ImportMappingCall.ReturnValueMapping is an ImportMappingIdentifier — Studio Pro writes "Clients.SoapOrdersImportMapping" there, so a UUID is unloadable: "The text 'c2d1682f-…' is not a valid ImportMappingIdentifier." mx check stopped 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:

  • ServiceName is the WSDL service name, not the local part of the imported service's qualified name — Studio Pro writes both and they differ.
  • ImportMappingCall.ContentType is "Xml" for a SOAP mapping, not the hardcoded "Json".
  • Range.SingleObject follows the operation's cardinality, not always true.
  • VariableType is the result's real type, not always DataTypes$VoidType — which contradicted the mapping and made the assignment illegal in its own right (CE0243 + CE0366).
  • DESCRIBE printed resolved guesses: five resolvers existed for properties that are BY_NAME_REFERENCE, where what is stored IS the qualified name. One asked for WebServices$ImportedWebService; the stored $Type is ImportedServiceImpl. None could ever match.

A proposal covers the half still missing: CALL WEB SERVICE can say who to call and what to do with the answer, and cannot say what to send. send mapping X parses, 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 --watch could not start a 11.14 app — the second copy of a gate whose first copy was fixed months ago, so run --local worked and --watch died 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 --serve process 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 /build request twice, model untouched — Success then Failure, ERR_MODULE_NOT_FOUND on the config file imported by mxbuild's own rollup-runner.mjs. Three remedies measured, none work: switching the app bundler, deleting deployment/, 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, --host and --port are in mxbuild --help but 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 walked GetDataSource() only, so the identical fault on an action slot was silent. The report's stated root cause did not hold: a container's Action: has always taken an argument list, proven with three pages and one mx check.

  • An unqualified association in an XPath constraintCE0161. Qualification as the only variable between two runs: unqualified passes check and fails the build; qualified passes both. Expensive rather than cosmetic, since exec cannot roll back.

A silent engine gap, and a headless pipeline

  • diff-local failed with "mprcontents directory not found" (mendixlabs/mxcli#1080). The default engine never overrode ContentsDir(), so it fell through to the generated unimplemented stub — whose promise to "fail loudly" is conditional on the method having an error to fail through. ContentsDir has none, so it returned "", which is the in-band answer for MPR v1. The gap looked like a v1 project.

  • marketplace update exited 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.js could not record the mobile pass its own skill mandates. userAgent is 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 plan mismeasured anchors in opposite directions — a module anchor reported built immediately, a module-role anchor planned forever.
  • The SessionStart hook re-downloaded 85 MB every session when mxcli was already on PATH.
  • theme create --from <design> seeded the palette and nothing else — wrong summary and swatches, plus ~500 KB of unused webfont.
  • The primary button was never the brand colour, resolving a contradiction already in the Atlas map.

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-bg is a var(--topbar-bg) reference, which the themes already bind.

ako and others added 30 commits September 10, 2026 13:02
…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>
`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)
@ako
ako merged commit 2f2d7b3 into mendixlabs:main Sep 11, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants