Sync ako/mxcli: idempotent writes, describe data-loss fixes, and four upstream issue fixes - #871
Merged
Conversation
… and support SYNCHRONIZE Upstream mendixlabs#863 reported a nanoflow with a Synchronize activity describing as `-- Empty action`, with all three activities on its error branch gone and nothing on stdout or stderr to say so. Two independent defects, plus one missing feature. 1. The error branch was dropped by an early return, not by the missing action. emitActivityStatement returned as soon as a statement rendered as a line comment, before errorHandlerFlow was consulted — so EVERY activity the reader cannot map lost its handler, as the reporter suspected. The early return is itself correct (annotations before a comment make exec fail "no viable alternative at input '@position...'"), so it stays and the branch is now emitted commented-out. Emitting it live would be worse than dropping it: the next exec would run those activities unconditionally in the main flow instead of on failure. 2. `-- Empty action` was anonymous. An action with no reader mapping now reads back as an UnsupportedAction stand-in carrying the stored $Type and ErrorHandlingType, so DESCRIBE names what it could not render. This is not one straggler: of 58 MicroflowAction types the modelsdk reader — the default engine — maps 31, and 12 of the 27 unmapped are types the legacy parser handles, including the shipped ExecuteDatabaseQueryAction. 3. SYNCHRONIZE is now a first-class statement: read, write, grammar, DESCRIBE and version gating. `synchronize all | unsynchronized | $Var[, $Var...]`, with the usual ON ERROR clause. It is nanoflow-only, so a microflow using it is rejected as MDL057 rather than failing the build with CE0009. The enum values come from the Model SDK's SynchronizationType, not from Studio Pro's UI: the variable form stores Specific, though the UI says "Selected object(s)". Two failures mxbuild caught that the BSON dump could not: * CE0011 — a custom error handler needs its own outgoing error flow. * CE2004 — a Mendix array's first element is a version marker. Written without it a one-element VariableNames reads back empty, so the variable was plainly in the document and the build still said none was selected. Verified on Mendix 11.13.0: all five forms build with 0 errors and describe→exec→describe is byte-stable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
…ops losing it EXECUTE DATABASE QUERY is authorable in MDL and already had a DESCRIBE formatter, but the modelsdk reader — the default engine — had no case for it. The activity wrote correctly and read back with a nil Action, so a shipped, documented feature was write-only: describing the microflow rendered a placeholder and a describe→edit→exec cycle deleted the query. The storage $Type is DatabaseConnector$ExecuteDatabaseQueryAction, not Microflows$ — the action lives in its own sub-metamodel. The reader mirrors the keys the writer builds directly (Query / DynamicQuery / OutputVariableName / the two mapping lists) rather than the gen accessors, which expose the named query as QueryQualifiedName; pinning both sides to one key set is what makes the round trip mean something. Verified end to end on Mendix 11.13.0 against a real connection with a parameterised query and a custom error handler: the statement describes back verbatim and describe→exec→describe is byte-stable, with the project loading and no new check errors. This also supplies the end-to-end demonstration the previous commit could not: before the reader mapping, the same microflow described as "-- Unsupported action: DatabaseConnector$ExecuteDatabaseQueryAction" with its error handler preserved as comments — the mendixlabs#863 fix working on a real project rather than only in unit tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
…Mendix cannot open A database connection's connection string, username and password are ConstantIdentifier properties — BY_NAME references to a Constant document, typed as model.QualifiedName in the metamodel and bound as ByNameRef in gen. There is no literal alternative. The grammar offers a string-literal spelling anyway, and the executor passed it straight through into the reference. The result was a project that will not open in Studio Pro and that `mx check` cannot even load: StorageLoadException: One or more invalid values were detected while loading the project: Database Connection 'M.Conn' has an invalid value '' for property ConnectionString. The text 'jdbc:postgresql://...' is not a valid ConstantIdentifier. No CE code and no document named, because the failure is in the loader rather than the validator — so the whole project goes down instead of one unit. Same class as mendixlabs#854. Refused now as MDL058, at check AND exec: `check` reporting it does not help a script that never ran `check`. All three properties are reported at once in a fixed order, so fixing them does not take three exec runs. Refusing rather than auto-creating a constant from the literal is deliberate. Minting a document the author did not ask for is a silent side effect, and for `password` it would bake a secret into the model as a design-time default — precisely what the constant indirection exists to avoid. Found while verifying the ExecuteDatabaseQueryAction reader fix: the describe→exec→describe diff was byte-stable while the project was unloadable, which is why the build has to be run and not just the diff. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
Name unsupported actions, keep their error handlers, and support SYNCHRONIZE (mendixlabs#863)
The PreserveIDs attempt (reverted in f1371f7) rewrote element $IDs without rewriting the pointers that reference them, which makes a project unopenable with KeyNotFoundException at ResolvePostponedProperties. Records the symptom, why only some document types break, and the verification gap that let it through: a fixture with no microflow cannot exercise a bug in microflow sequence flows, and unit tests do not run mx check at all.
…nd the shared unnamed-element key §4 licenses renumbering "as long as each unit stays internally consistent". Two new investigation sections give that clause evidence and a cost. §5 — the constraint has now been violated in practice. PR #125 (reverted) carried stored element $IDs onto a rebuilt document and left the pointers referencing them untouched, which makes the project unopenable with KeyNotFoundException at ResolvePostponedProperties. Microflows and nanoflows corrupt; pages and navigation survive, which is why surviving document types are not evidence of safety: DataWidgets contains 0 Microflows$Microflow and Administration 8, per the §4 snapshots, and the two are updated together. The part that matters for scheduling Phase 2 is that mxcli's write layer cannot currently honour the constraint at all: pointers are primitive properties holding an element.ID, so a walk over child properties never sees a reference, and there is no way to enumerate reference-valued properties generically. This argues for import-and-transplant over anything element-level, stated as a design constraint — never rewrite an $ID inside a unit being otherwise preserved. §6 — §4's phantom add/removes from list-index keying are the same obstacle, and larger than they look: name-or-equal-length-position matching left 980 of 988 element IDs in one page unmatched, the bulk being Texts$Text, Forms$Appearance, Forms$ClientTemplate and Texts$Translation. A stable key for unnamed elements is the one piece of work this proposal and script idempotence both need.
Two goals (version-control hygiene, marketplace module upgrade) arrived at the same question and were nearly solved the same wrong way. Records what the measurements actually support, so the next attempt does not repeat PR #125. The decisions: compare before writing and skip when semantically equal; never renumber an $ID inside a unit being otherwise preserved; make the marketplace upgrade an import-and-transplant-GUID rather than a merge. Neither goal needs ID preservation, deterministic derivation, or reference remapping — those are deferred, not prerequisites. Adds the measurement the ADR rests on: scripts/mprsnapshot -canon per-unit canonical digests (IDs normalised away, plus a second digest masking fields mxcli regenerates by policy) scripts/idempotence-probe.sh runs a script twice over a project copy and classifies every unit as identical / volatile-only / real On a 27-unit project: 26 identical, 1 volatile-only, 0 real. The one is a microflow, differing solely in StableId — which microflow_write.go mints fresh on every write. That is the single policy blocking the approach on microflow-heavy projects. Two bugs found by using the probe rather than trusting it, both of the shape that reads as success: inModule(path, "") tests a prefix and is false for everything, so an empty filter emitted one unit and an empty diff looked like "nothing changed"; and keying by path collapsed the unnamed units that share one, pairing the wrong units between runs. Keyed by unit id now, which is stable across writes by construction.
Run on mxcli-sudoku (412 units, MPR v2, 30-document idempotent script set): 386 identical, 26 volatile-only, 0 real differences. Two things the run establishes beyond the headline zero. The split is type-determined, not arbitrary. StableId is registered on Microflows$Microflow only, and exactly the 26 microflows are volatile-only while the nanoflow, both pages and the navigation document are identical. Freezing StableId is therefore sufficient for decision 1, not merely necessary — a stronger claim than the ADR previously made. The zero is a measurement, not a blind spot. The same probe run against the withdrawn PreserveIDs binary reports 10 real differences on the same project — the 9 microflows and 1 nanoflow in that set, page unchanged — independently reproducing the corrupt/survive split and showing the probe can report non-zero on the failure this ADR exists to prevent. Also records the gap honestly: no domain-model documents were covered, because the domain scripts are not re-runnable, which leaves untested the one exception the ADR names — entity attribute IDs. Notes what is already true there (attribute IDs are reused by name since 06a9fac; create or modify still drops unlisted attributes, now with a warning) so whoever closes the gap checks rather than assumes. Measurement and negative control by the mxcli-sudoku session.
Freezing Microflows$Microflow.StableId was the last blocker on decision 1
(skip semantically-equal writes), and mxcli had no description of the field
-- it appears in none of the reflection data, the TypeScript SDK reference,
or the generated metamodel.
Answered from the modeler assemblies and a real build instead:
- Mendix declares it ModelPropertyAttribute("StableId",
RetentionType.DesignTime) with IsIdentifier = true.
- MicroflowStableIdConversion, a one-time conversion, seeds it once from the
microflow's storage $ID and thereafter retains it independently -- it is
the identity that survives the $ID renumbering Studio Pro does freely.
- PackageUtils.RescueStableIDs transplants it by Name on module package
import, beside RescueDataStorageGuids. Studio Pro's own marketplace path
is replace-wholesale-then-transplant, which is decision 3.
- Its value escapes the model: RuntimeOperationRegistry derives each
client-callable microflow's operation id from it. Verified by rebuilding
the derivation -- 10 of 10 callMicroflow entries in
deployment/model/operations.json reproduce as
base64(uuid5(projectId, StableId).bytes_le). com.mendix.webui.jar reads
that file.
So regenerating it on every write is not incidental churn, and the sudoku
split is explained: only Microflows$Microflow carries the field (16 of 16 in
the fixture; all 13 nanoflows lack it).
Also anchors the .gitignore rule for the stray mprsnapshot build output: as
written it also matched the scripts/mprsnapshot/ source directory, which
silently excluded it from `git add`.
Documentation only -- no behaviour change.
`alter page … set DataSource = $Param` was refused with "unsupported DataSource type for alter page set: parameter", while the identical retype through REPLACE succeeded. REPLACE rebuilds the widget through the CREATE PAGE builder, which has always handled every datasource type — so the capability existed, just not in the SET path. Two gaps stacked, neither aware of the other: convertASTDataSource had no `parameter` case, and the mutator's serializeDataSourceBson had no *pages.DataViewSource case. A parameter source is a Forms$DataViewSource with two BY_NAME halves — EntityRef naming the parameter's entity, SourceVariable naming the parameter — and Mendix resolves either one it cannot find to null, which is the mendixlabs#854 unopenable-project shape. The entity is not in the statement, so the mutator resolves it from the container's own Parameters list (the only layer holding that document) and refuses an unknown parameter name rather than writing an unresolved reference. Snippet-vs-page is read off the stored parameter's own $Type, so the two keys cannot be confused. The unhelpful message was half the report, so the types SET still cannot take now name REPLACE as the way through. Verified on Mendix 11.13.0: the reported operation applies, mx check reports 0 errors, and DESCRIBE renders `dataview dvOrder (DataSource: $Order)`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
A Mendix expression navigating an association must name the target entity fully qualified (`$Issue/MA.Issue_Person/MB.Person/FullName`). mxcli inserts that step itself, so the author may write the association alone — except it did not, in two independent ways, both ending in CE0117 with `exec` and `check --references` silent. Cross-module: lookupAssociation walked only dm.Associations. An association whose target lives in another module is a DomainModels$CrossAssociation in a separate list, where the remote end is the BY_NAME ChildRef — so the lookup failed, nothing was inserted, and the author's short form was written through unchanged. This is the third occurrence of that two-list trap, after mendixlabs#854 and issuetracker #19. Same-module with the entity named: the skip guard only fired when the next segment was already qualified. An author writing the bare entity name got the resolved step inserted IN ADDITION to their own, producing `MA.Issue_Tag/MA.Tag/Tag/Label` — four segments, as invalid as the three it was meant to fix. The report covered only the cross-module half; this one would have survived a fix aimed at it. All four spellings now converge on the one canonical form: association only, entity named bare, already qualified, cross-module or not. Verified on Mendix 11.13.0 with a control built into a single project — short and fully-qualified side by side gives exactly one CE0117, and dropping the short one gives 0 errors. After the fix all four build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
Implements ADR-0008 decision 1. Re-running an MDL script against a project already in sync now leaves the .mpr and mprcontents/ files byte-identical. Two mechanisms, both in the new modelsdk/canon package so there is one policy rather than one per engine: - Canonical comparison. Every element $ID is replaced by its index in a deterministic containment walk, so a difference in *which* UUIDs a rebuild happened to mint is not a difference. Comparing bytes would skip nothing: `create or replace` rebuilds the document and every sub-element gets a random $ID, making the stored bytes a function of the script and a random source. No knowledge of which properties hold references is needed -- the element IDs come from the walk, and any occurrence of one of them anywhere is a reference by definition. - Identity carrying. Microflows$Microflow.StableId is taken from the stored document rather than re-minted. It is the one field on a microflow whose purpose is not to change: Mendix declares it IsIdentifier, seeds it once via a one-time conversion, transplants it on module update (PackageUtils.RescueStableIDs), and derives every client-callable microflow's operation id from it. Identity is carried before the comparison, which is what lets an otherwise-unchanged microflow compare equal at all. Wired at the single write choke point of both engines: modelsdk/mpr (updateUnit and WriteTransaction.WriteUnit -- codec.Store reaches storage through the latter, so covering only the former would leave the codec engine's own writes churning) and sdk/mpr (updateUnit). Which engine ran is an --engine flag and must not be visible in a user's diff. Every failure path falls through to writing. A false "different" costs a redundant write, which is the previous behaviour; a false "equal" would silently discard the user's intent. Skipping is also safer than writing: the stored IDs are the ones every pointer inside that unit already agrees with, which is exactly what the reverted PR #125 broke by rewriting them. MXCLI_ALWAYS_WRITE=1 disables elision for bisecting. It deliberately does not disable identity preservation -- a forced write that re-minted StableId would renumber the deployed model's operation ids, which is a change to the app rather than a debugging aid. Verified end to end through the built CLI on a 371-unit MPR v2 project: a settled re-run changes 0 units, and the same measurement with elision disabled changes 2 (the two microflows the script writes), so the measurement can report non-zero. `mx check` reports 0 errors both on the elided project and on the one where identity-patched writes landed. Every test carries that control, because a test that only passes against fixed code has not been shown to detect anything.
…ently No-op elision is type-agnostic -- it compares canonical forms of raw BSON and knows nothing about document types, so a new one is covered the day it is added, and anything it cannot parse falls through to writing. Identity preservation is not: canon.identityFields is hand-maintained and cannot be generated, because Mendix's IsIdentifier flag lives in the modeler assemblies and not in the reflection data generated/metamodel is built from. The failure mode for a future document type is therefore silent -- the type gets an identity property, nobody adds a row, and elision quietly stops working for it. No error, no failing test. TestFreshGUIDFieldsHaveAnIdentityDecision closes the most likely path. A property registered as a codec FreshGUIDField is minted anew on every write, so by construction it makes a document differ from itself; the guard fails unless each one is recorded as either identity (carried) or deliberate churn (waived with a reason naming what reads the value). Verified to detect drift by removing the Microflows$Microflow row and confirming the failure, rather than only that it passes. It is a guard, not a proof: it cannot catch an identity property the encoder does not mint fresh. ADR-0008 now says so, and also records the invariant elision itself rests on -- that no binary pointer crosses a unit boundary (measured 0 of 9,910, not enforced). A future type referencing another unit by $ID rather than qualified name would let an elided unit's discarded IDs dangle from a unit that was written, which is PR #125's failure arriving from the opposite direction. Adds codec.FreshGUIDRegistrations and canon.IdentityFields as the accessors the guard needs. No behaviour change.
…ge must keep Follow-up to the compare-and-skip implementation, after it was confirmed green on mxcli-sudoku. - ADR-0008: records the post-implementation confirmation, closing the loop the pre-implementation measurement opened (the 26 volatile-only units were volatile only because of StableId). Also states plainly that a claim about idempotence needs the MXCLI_ALWAYS_WRITE=1 control alongside it, citing the measurement taken during implementation that reported zero changed units purely because the script aborted on its second run. - ADR index: 0008 was missing from the table in docs/13-decisions/README.md. - CLAUDE.md: new always-in-context section covering the three rules a future change has to keep -- never rewrite an element $ID without rewriting the references in the same pass, wire any new write path to canon.Reconcile, and give a new document type with an identity property a row in identityFields. Names the invariant elision rests on (no binary pointer crosses a unit boundary; measured, not enforced) and the requirement that a "nothing changed" test carry its control. Adds the feature to the implementation-status list. - check-syntax skill (synced to user projects): re-running a settled script changes nothing, so `git status` is now a reliable answer to "did this do anything". Docs only.
…fety claim Triggered by ADR-0008 landing and compare-and-skip shipping. architecture/mpr-read-write.md - Frames the two engines and the single updateUnit choke point each funnels writes through, which is what makes a cross-cutting write policy expressible without touching a serializer. - Adds the conditional-write model: why byte comparison would skip nothing, and why the canonical form needs no knowledge of which properties hold references. - Corrects a pre-existing inaccuracy. The page claimed v2 writes stage through WriteTransaction with temp files; in sdk/mpr that type has no callers and the ordinary path is a direct WriteFile. Temp-file staging and the _Transaction bump are the modelsdk engine. Caught only because the skill requires re-reading sources rather than synthesising from memory. - Sources: added sdk/mpr/writer_units.go (where updateUnit actually lives) and the modelsdk/canon/ADR set; dropped sdk/mpr/writer_widgets.go, since the BSON-convention claim is grounded in parser.go and widget specifics belong to the widget-engine page. models/element-identity.md (new, added to the seed table in the same run) - $ID / GUID / StableId as three identifiers with different scopes and lifetimes; the unit as identity boundary; why pointers are invisible to a containment walk and why "the pages still load" is not evidence. - Framing only: the decision and measurements stay in ADR-0008, the rules in CLAUDE.md, the field table in source. Two SYNC_LOG rows appended listing what was actually read. All wiki-links and relative source links resolve. Docs only.
mxcli-formula1 -- a multi-app solution using published OData services, external entities and the external database connector -- came back green. Recorded for what it actually adds: coverage, not another zero. Every measurement behind this ADR until now came from projects whose documents were microflows, nanoflows, pages and navigation, and it was open whether elision holds for document types nobody had exercised. It bears most on the invariant elision rests on -- that no binary pointer crosses a unit boundary. A multi-app solution with external entities and OData contracts is the shape where a cross-document binary reference would be most likely to exist, so its absence there says more than another microflow-heavy project would. Corroboration, not proof: a passing project shows no observed violation, not that none can occur. Deliberately not written up as an idempotence measurement. It is not established that the MXCLI_ALWAYS_WRITE=1 control was taken alongside it, and by this ADR's own standard a clean result without its control does not qualify.
…ping
A SNIPPETCALL for a parameterised snippet had no working spelling. Omitting
Params was refused by mxcli; supplying `params: {X: $currentObject}` was
accepted and then failed the build with CE0115 "The arguments that are passed
to snippet … do not match the expected parameters and need to be refreshed."
Studio Pro's own "Refresh snippet parameters" then deleted what mxcli wrote.
mxbuild shows the defect is narrower than reported: passing a REAL page
parameter always built at 0 errors. Only `$currentObject` failed, because it
was translated into
Forms$PageVariable{ PageParameter: "currentObject" }
a by-name reference to a page parameter that does not exist.
"Satisfied by the enclosing data context" is not a variable in Mendix — it is
the ABSENCE of a mapping. That is why Studio Pro emits an empty
ParameterMappings here, and why its refresh removes the invented entry.
So `$currentObject` now emits no mapping, and Params may be omitted entirely —
but only when the surrounding data context is the parameter's own entity. A
context of a different entity still gets the guidance, since nothing there can
satisfy the parameter and a build error later would be worse. Naming a real
variable still produces a real mapping.
Verified on Mendix 11.13.0: all three forms build at 0 errors in one project,
with empty ParameterMappings for the two context-satisfied pages and a real
mapping for the page passing its own parameter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
call workflow, get workflow data / workflows / activity records, open, lock
and unlock workflow, and workflow operation were all authorable in MDL and all
had DESCRIBE formatters — but the modelsdk reader, the default engine, had no
case for any of them. Each read back with a nil Action, so a shipped feature
was write-only: DESCRIBE showed a placeholder and a describe→edit→exec cycle
deleted the activity.
The reader mirrors the keys workflowMicroflowActionToGen writes rather than
the gen accessors, which disagree in one place per "get" action: gen binds the
result variable as VariableName, the model stores OutputVariableName. A reader
written against the accessors returns "" and the variable disappears — which a
reader-only test seeded with hand-written BSON would never notice, so these
are round trips that pin reader and writer against each other. All ten fail
against the pre-fix reader with the nil-Action symptom.
Finishing the reader exposed two defects that were invisible while nothing
rendered at all, both now fixed:
* `call workflow X ($Ctx)` — the model stores only the context variable,
never the parameter's name, so DESCRIBE can only emit the positional form,
which the grammar did not accept. Describe output would not parse back in.
A workflow has exactly one context parameter, so that form is unambiguous
and is now accepted alongside the named one.
* `workflow operation abort … reason '…'` — the reason lands in a
StringTemplate's Text, which is literal text, but the expression string was
stored with its quotes. Mendix rendered them at runtime and DESCRIBE
re-quoted what it read, so each round trip doubled the quoting without
bound.
Verified on Mendix 11.13.0 against a real workflow: every form builds at
0 errors and describe→exec→describe is stable.
Not fixed here: `lock/unlock workflow all` writes an activity mxbuild rejects
with CE1825 "The 'Workflow' property is required". A lock always needs a
specific workflow definition, and writing an empty selection does not satisfy
it either — so the MDL form has no valid representation. Recorded in the
symptom table and the example; it needs its own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
None of the 16 command files carried YAML frontmatter — each started with a bare `# heading` — so nothing supplied the `description` a client uses to list them. The commands were always loadable and invocable by full name; they just did not appear in the `/` menu, which reads as "not installed". Adds `description` to all 16 and `argument-hint` where a command takes arguments, across both namespaces. `mendix/` is embedded into `mxcli init` output, so user projects get the same listing. No behaviour change: the command bodies are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
Read the eight workflow call actions back (describe was write-only)
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.
TITLE: Sync ako/mxcli: idempotent writes, describe data-loss fixes, and four upstream issue fixes
Syncs
ako/mxcli:mainintomendixlabs/mxcli:main— 18 non-merge commits. Everything below was verified against a real project with mxbuild 11.13.0 (and 10.24 where the report named it).Fixes for filed issues
-- Empty action, and the fallback co mment silently drops the activity's entire error handler #863 — DESCRIBE rendered an unmapped action as-- Empty actionand silently dropped its entire error handler. Two independent causes: the describer returned as soon as a statement rendered as a comment, before consulting the error flow (so every unsupported action lost its handler), and the placeholder was anonymous. Handlers now survive as commented-out MDL — emitting them live would be worse, since the next exec would run them unconditionally in the main flow.SYNCHRONIZEis also now a first-class statement (nanoflow-only, MDL057, version-gated).alter page … set DataSource = $Paramwas refused while REPLACE worked. Both the executor and the mutator lacked the parameter case; the entity is resolved from the container's ownParameterslist, and an unknown parameter is refused rather than written as a dangling ref.TargetModule.qualifier on target entity #829 — cross-module association navigation wrote an expression without the target entity's qualifier (CE0117). Third occurrence of the two-list trap:CrossAssociationsis a separate list fromAssociations. The same-module case was broken in the opposite direction (double-inserting the entity) and would have survived a fix aimed only at the report.SNIPPETCALLfor a parameterised snippet had no working spelling: omittingParams:was refused, supplying$currentObjectproduced CE0115. "Satisfied by the enclosing data context" is the absence of a mapping in Mendix, not a variable — which is why Studio Pro's "Refresh snippet parameters" deleted what mxcli wrote.Projects that could not be opened
create database connectionwith literalconnection string/username/passwordwrote an.mprMendix cannot load (StorageLoadException: … is not a valid ConstantIdentifier). Those are ConstantIdentifier properties with no literal alternative. Now refused at bothcheckandexec, all three reported at once. Not a build error — the loader dies first, somx checkexits 0 on it.Write-only features (authorable, then unreadable)
The modelsdk reader — the default engine — mapped 32 of 58 microflow action types. Actions could be authored and then not read back, so a describe→edit→exec cycle deleted them.
ExecuteDatabaseQueryAction, the eight workflow call actions, andTransformJsonAction/CallExternalAction/RestOperationCallActionare now readable. 44 of 58, and zero of the remainder are authorable.TransformJsonActionwas worse than a describe gap: the modelsdk writer had no case either, sotransformwas written with no action at all and failed CE0008 — unusable on the default engine while the legacy writer handled it.call workflowdescribed to a positional form the grammar rejected, and an abortreasondoubled its quotes on every round trip.Idempotent writes (ADR-0008)
A unit whose new content is semantically equal to what is stored is no longer written, so re-running a script against an in-sync project leaves the
.mprbyte-identical and Studio Pro shows no version-control changes. Comparison is on a canonical form with element$IDs normalised away;Microflows$Microflow.StableIdis carried from the stored document rather than re-minted. Includes a guard so a future document type cannot lose identity preservation silently.Docs
ADR-0008 and its supporting measurements, a wiki sync on conditional writes and element identity, the dangling-ID-reference failure mode, and
descriptionfrontmatter on all 16 slash commands (without it they load but never appear in the/menu).Not included
ako#129 is still open on the fork — it closes the last write-only gap (
TransformJsonAction's missing writer, plus the final three readers). The write-only bullet above describes the state after that merges; if this syncs first,transformis still CE0008 upstream.