Skip to content

Sync ako/mxcli: folded-decision inversion, external-action associations, marketplace update, module version/type, test-endpoint cleanup - #1739

Merged
ako merged 25 commits into
mendixlabs:mainfrom
ako:main
Nov 3, 2026
Merged

ako merged 25 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Syncs ako/mxcli:main into upstream: 12 commits since #1722 — 5 upstream issues addressed, module settings in MDL, and a CI change for the macOS runner flake.

Microflows: rewrites that silently changed behaviour

  • A condition edit no longer inverts a folded decision. describe prints a decision whose true path goes straight to the merge as if not(X) then <false path>. Fed back through create or modify with only the condition changed, the in-place edit stored the printed not(Y) while the flows stayed, inverting the decision — and check, exec and diff all looked clean. The stated condition is now negated back when describe printed it folded, decided from the stored flows rather than the text. A decision in a loop body is refused, not inverted. Fixes create or modify microflow inverts a decision when only its condition changes: describe folds it to "if not(X)", the in-place edit stores not(X) literally #1728
  • A call external action keeps its "Associations to include" on rewrite. The tree (a list of Microflows$IncludedAssociation per parameter mapping, nestable) had no field in the model, so restating the call wrote none and the OData request silently left out the associated objects at runtime. It is now read and written in Studio Pro's shape, and a rewrite carries the stored tree onto the restated call, matched on service, action and parameter name and only while the parameter binds the same entity. MDL syntax to set it is not added. Refs CALL EXTERNAL ACTION — no MDL syntax for IncludeAssociations flag #1721

Grammar

  • A member named Error before message (validation feedback $E/Error message 'x', what describe writes) no longer lexes as the error message clause. The fix is a predicate on the compound token, placed on its right edge so the lexer's DFA cache stays on (a left-edge version doubled check time over 300 scripts). All 1210 .mdl files in the repo check identically to a control binary. Fixes roundtrip for validation feedback on member named 'error' does not work as expected #1720

Marketplace

  • marketplace update no longer drops the module and then fails. It looked for the package at target.mpk while the download wrote t.mpk, and opened it only after the destructive steps, leaving the project half-updated. The download path is now returned rather than re-spelled, and a preflight checks the reference project and the package before the first write; a failure there reports "nothing was changed". Fixes Unable to update marketplace module #1732

Modules

  • A jar dependency change keeps the module ID and Marketplace checksum. UpdateModuleSettings rebuilt the unit from the keys mxcli models, so PackageId and Checksum were wiped and detailed troubleshooting switched on. The write now starts from the stored unit and overwrites only the modelled keys.
  • New: describe and set a module's version and module type (Studio Pro's Module settings → General):
    create or modify module Inventory (Version: '2.3.1', ModuleType: addon);
    alter module Inventory set (Version: '2.4.0');
    
    describe prints the non-default settings, list modules gains Version and Type columns. Measured on 11.16: the type is ExportLevel (Source = App, Protected = Add-on). Version must be SemVer; the type is gated to 9.12+. Proposal included.

run --local --test-endpoint / test --attach

  • run stop removes a test endpoint the run could not. On Windows taskkill /F skipped the run's teardown, leaving MxTest, its after-startup microflow and its theme/java folders, and the next build failed with CE1613. The run now writes a restore record before installing anything, and run stop (or the next --test-endpoint run) restores from it when the owner is gone.
  • test --attach no longer leaves the hosting app's pages 404. The attach built through the dev loop's mxbuild --serve without its recovery, and a later build there replaces web/dist with a bundle that has no page modules. It now builds through the same recovery on a private serve process.

Together these Fix #1723.

CI

The macOS tunnel-seam leg runs on pushes to main, on dispatch and nightly, no longer on PRs: the hosted macOS queue sometimes cancels it before it gets a runner, and on a PR the Windows leg already exercises the same !linux stub. The seam test moves to scripts/test-tunnel-seam.sh so the two workflows can't drift

claude and others added 25 commits November 3, 2026 07:53
The hosted macOS queue is this workflow's one known runner flake: it
sometimes cancels tunnel-seam-cross-platform (macos-latest) before it gets
a runner, failing a PR that has nothing wrong with it. It is also the
scarcest queue (5 concurrent on GitHub Free).

On a PR, the Windows leg already executes the same !linux stub, so macOS
added only a darwin compile. It now runs on pushes to main and on
workflow_dispatch, and in a new nightly job (tunnel-seam-macos) that does
not gate the release, so a darwin-only break is still caught within a day.

The seam test body moves to scripts/test-tunnel-seam.sh so the two
workflows cannot drift; its vacuous-run guard is unchanged (on Linux, where
the !linux tests are excluded, it fails with "matched 0"). The steward
skill's note on this job is updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82simPxcdrRu9PUSPcQ
…message clause (#1913)

`validation feedback $E/Error message 'x'` -- what describe writes for an
attribute named Error -- failed check with "missing MESSAGE": ERROR_MESSAGE
is one compound lexer token, so the member name and the MESSAGE keyword after
it were lexed as the `error message` clause (mendixlabs#1720).

ERROR_MESSAGE now carries a left-edge predicate: it never matches right after
a `/` or `.` member separator on the same line. A block comment's `*/` and a
line comment ending in `.` or `/` are not separators, so the clause keeps
lexing everywhere it did.

gen-completions rebuilt the rule from its first line, which is now the
predicate, and dropped ERROR MESSAGE from the LSP keywords; it strips a
leading predicate first.

Proven by stubbing the predicate to false (the reported error returns in the
visitor and describe round-trip tests), by the newline-skipping draft failing
the comment controls, and by checking all 1210 .mdl files in the repo with a
control binary and the fixed one: identical results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
)

The left-edge predicate added for #1913 is evaluated by ANTLR at every
token start, which disables the lexer's DFA cache: `check` over 300 example
scripts went from 43 s to 91 s and the conformance and upgrade corpus tests
hit the 30-minute timeout, while every unit test passed.

On the right edge the predicate runs only once `error message` has matched
(42 s vs 44 s, within noise). The character before the token is read from
TokenStartCharIndex, since LA(j) for a negative j is relative to the current
index. gen-completions strips the group's opening paren instead of a leading
predicate, and the generated keyword list is unchanged.

Re-proven on this version: stubbing the predicate brings back "missing
MESSAGE", the newline-skipping variant fails the comment controls, and all
1210 .mdl files check identically to the control binary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
ci: run the macOS tunnel-seam leg on main and nightly, not on PRs
…24rqgv

fix(grammar): a member named Error before `message` is not the error-message clause
On Windows `run stop` ends a detached run with `taskkill /F`, so the run's
own teardown never ran and the project kept the MxTest module, its
after-startup microflow, themesource/mxtest/ and javasource/mxtest/. The
next build failed with CE1613 once the module was dropped
(mendixlabs#1723). A crash, or the SIGKILL after stopGrace, skipped it
on any platform.

The run now writes a restore record before it installs anything: the
original after-startup, whether MxTest pre-existed, and the project file
with its document-tree digest. Remove deletes it after a clean removal.
RestoreAbandonedEndpoint performs the same removal when the record's owner
is gone; `run stop` calls it once the run's processes are gone, and a new
--test-endpoint run calls it before capturing the project's state.

TestRunStop_RestoresTestEndpointOfAKilledRun (integration, linux, mxbuild
11.12.6) SIGKILLs the detached run - what taskkill /F is - and runs
`run stop`: before the fix the after-startup, MxTest and 14 files were left;
after it the .mpr and every touched file are byte-identical to before the
run. A binary with only the runStop call stubbed fails it again. The
graceful SIGTERM stop is the control and leaves no record.

The other half of #1917, page bundles 404 after `test --attach`, has a
separate cause and is not addressed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
…e on rewrite (#1914)

"Associations to include" on a call external action parameter had no
field in the semantic model, the reader skipped it and the writer built
each mapping from scratch, and MDL has no syntax for it. So a create or
modify that restated the call wrote it with none, and the OData request
silently left out the associated objects at runtime while check, lint
and the build all passed.

It is a tree, not a flag (measured on ako/TestApp, Studio Pro 11.16.0):
a list of Microflows$IncludedAssociation per parameter mapping, an
association ticked under another nested in it, marker 2 at both levels.
The model now holds it, the reader reads it with its $IDs, the writer
writes Studio Pro's shape, and a rewrite carries the stored tree onto
the restated call, matched on service, action and parameter name, and
only while the parameter binds the same entity. Two stored calls to one
action with different trees are refused rather than guessed at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82ttaGzay5CwiC2V7cf
…ndex (#1917)

The run stop note added for #1917 took the body to 701 lines
(TestLargeSkillsWereSplit) and left the section index stale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
…pendency change (#1920)

UpdateModuleSettings rebuilt the Projects$ModuleSettings unit from the
nine keys mxcli models, and the write path filled the rest with
defaults: a jar dependency change wiped PackageId (Studio Pro's Module
ID) and a Marketplace module's Checksum, and switched detailed
troubleshooting on. The write now starts from the stored unit and
overwrites only the modelled keys, so what mxcli does not model —
including keys Studio Pro adds later — keeps its stored value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82ttaGzay5CwiC2V7cf
…e type (#1921)

Measured on Mendix 11.16: the module type is ExportLevel (Source = App,
Protected = Add-on; ProtectedModuleType is AddOn on every module), the
version is ModuleSettings.Version, and Location is
Projects$ModuleImpl.FromAppStore. Proposes a property list on create
module and alter module set, describe printing the non-default
settings, and Location as a later key. Supersedes the read-only
show-describe-module-settings.md, whose storage notes were wrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82ttaGzay5CwiC2V7cf
…ssociations

fix(microflows): keep a call external action's associations to include on rewrite (#1914)
fix(modules): keep the module ID and Marketplace checksum on a jar dependency change (#1920)
…1921)

The General tab of Studio Pro's Module settings dialog, as a property
list on create module and alter module set:

  create or modify module Inventory (Version: '2.3.1', ModuleType: addon);
  alter module Inventory set (Version: '2.4.0');

describe module prints the settings that differ from a new module's
(App, 1.0.0) and the location as a comment; list modules gains Version
and Type columns. The type is ExportLevel (Source = App, Protected =
Add-on), measured on 11.16 — ProtectedModuleType is AddOn on every
module. Version must be SemVer, the type app or addon (9.12+, gated);
check and exec refuse the same statements, and restating the stored
settings writes nothing. Writes go through UpdateModuleSettings, which
since #1920 keeps the module ID and checksum.

A second `create or modify module` in a script is no longer MDL-DUPDEF,
since it now applies settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82ttaGzay5CwiC2V7cf
…rioritization-24rqgv

# Conflicts:
#	CHANGELOG.md
…24rqgv

fix(run): run stop removes a --test-endpoint the run could not
After a green `mxcli test --attach` against `run --local --test-endpoint`,
the app answered 404 on dist/pages/*.js and every page showed the generic
error dialog until it was restarted (mendixlabs#1723).

The attach built through the dev loop's mxbuild --serve. Its builds are never
that process's first, and on the prebuilt-client Mendix versions a later
build replaces web/dist with a bundle that has no page modules
(mendixlabs#1580). The dev loop's own reloads recover through
serveRecovery by restarting their serve process; the attach called /build
directly and got the defect without the recovery.

docker.AttachedServe runs the attach's builds through the same
serveRecovery, over a process whose Restart starts a private mxbuild --serve
on a free port rather than touching the dev loop's, which keeps its own
recovery for its next build. The attach stops the private one when it ends.

TestTestAttach_KeepsTheHostingAppsPageBundles (integration, linux, mxbuild
11.12.6) GETs a page module the boot produced (200 before the attach): on
main it answers 404 afterwards with 0 page modules left; with the fix, 200.
A binary whose AttachedServe.Build bypasses only the recovery fails it again.
Unit tests: the host's serve is never restarted, the private one takes the
cleanup build and is stopped on Close, and a complete host build starts
nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
feat(modules): describe and set a module's version and module type (#1921)
…24rqgv

fix(test): --attach no longer leaves the hosting app's pages 404
…1934)

Every `marketplace update` failed with "open package target.mpk: no such
file or directory" after it had dropped the installed module and copied in
units, leaving the project half-updated (mendixlabs#1732).

The update spelled the package path out as work/target.mpk while
referenceFor(…, "t") downloads to work/t.mpk. referenceFor now returns the
path it downloaded to, and the update uses it, so the two cannot drift.

And PerformUpdate opened the package only after the destructive steps.
preflightUpdate now checks, before the first write, that the reference
project opens and holds the module and that the package opens; a failure
there is ErrUpdateNotStarted, reported as "nothing was changed" rather than
"the project may be mid-update".

TestPerformUpdate_RefusesBeforeDroppingWhenAnInputIsMissing hashes every
file of a fixture project: on main a missing package or reference project
changed it (the reported error, after the drop); with the preflight stubbed
it fails again. The existing PerformUpdate/PerformInstall tests are the
control with valid inputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
…24rqgv

fix(marketplace): update no longer drops the module and then fails
…#1930)

describe prints a decision whose true path goes straight to its merge, and
whose false path holds the activities, folded: `if not(X) then <false path>`,
with the paths swapped. Fed back through `create or modify` with only the
condition changed, the in-place condition edit (#888) stored the printed
`not(Y)` as the decision's expression while its flows stayed: the decision
was inverted, and check, exec ("edited in place") and diff all looked clean
(mendixlabs#1728).

splitCondition now negates the stated condition back when describe prints
the decision folded. decisionPrintedNegated decides that from the stored
flows with describe's own merge computation, not from the text: describe also
strips a stored not(Z) to Z. negateExpression, extracted from
negateIfCondition, is its own inverse, so printed not(Y) stores Y and printed
Z stores not(Z).

TestCreateOrModify_FoldedDecisionKeepsItsMeaning runs the reporter's flow on a
pedapp copy: describe, change only the attribute in the printed condition,
exec under mdl 1. Both fold shapes kept the decision's $ID and store the
un-negated expression; with the negation stubbed both fail with the
inversion. A decision in a loop body is refused by the splice, not inverted
(second test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
…24rqgv

fix(microflows): a condition edit no longer inverts a folded decision
@ako
ako merged commit fe99b85 into mendixlabs:main Nov 3, 2026
15 checks passed
sergiusz-woznicki pushed a commit to sergiusz-woznicki/mxcli that referenced this pull request Nov 5, 2026
fix: a constant in an XPath constraint — parse it, refuse it (CE0161), stop losing it (mendixlabs#1685)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment