Skip to content

Sync ako/mxcli: mxcli git merge (#1674) with --plan over the engine's result, Concord dropped for Studio Pro's MCP, SEC011, MPR012 - #1714

Merged
ako merged 86 commits into
mendixlabs:mainfrom
ako:main
Nov 1, 2026
Merged

ako merged 86 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator

Syncs ako/mxcli:main into upstream. It carries 24 new commits from fork PRs ako#1870, ako#1871, ako#1873, ako#1874, ako#1876 and ako#1882.

Fixes #1471. Merges #1674.

About the commit count: the PR shows 61 commits. The other 37 are copies, under new SHAs, of fork commits already merged upstream. They come in through the history of #1674's branch, which ako#1882 merges unchanged, so that GitHub sees #1674 as merged. They change nothing beyond what is already on main.

mxcli git merge (#1674, by @engalar)

ako#1882 merges #1674 at its head 2c118daf, unchanged, with engalar's commits keeping their author and committer. It then fixes what review and a Linux run found, one commit per fix.

What #1674 brings:

  • mxcli git merge <ref> runs git merge. On a model conflict it runs mx merge -v over clean worktrees of base, ours and theirs.
  • What the engine resolves is committed with the mx_metadata note.
  • Conflicts are reviewed before they land:
    • a report of both sides;
    • a property-level report from mx diff;
    • a plan file the human edits, then --ours / --theirs, or Studio Pro.
  • It also changes git commit and git doctor:
    • The staged-files summary lists what the commit holds.
    • doctor judges only core.autocrlf, because the mendix.* counters are Studio Pro's own.

Fixes on top:

  • --plan resolves over the engine's result. The plan's commit used to be built from git's two sides, so it lost changes the engine had auto-resolved. For example, theirs' new entity was dropped when both sides added an entity to one domain model.
    • --plan now runs the engine again and works from its result.
    • The plan must name exactly the units the engine left open.
  • Merge conflict markers can be read and cleared: Reader.UnitConflicts lists the units marked in Unit.ContentsConflicts, and Writer.ClearContentsConflicts clears one. A marker left behind gives CE0137 in mx check.
  • A merge that git refuses to start says why. Git writes its reason to stderr, which was dropped.
  • The git-merge fixtures commit with a fixed identity.
  • Verified with real mx 11.13 on Linux:
    • the engine path, with exit 0 and exit 2;
    • a plan that keeps what the engine merged;
    • a control where a leftover marker gives CE0137.
  • Not in this PR: two branches that each create the same entity and microflow merge with "Merge successful". The result holds two of each, which fails mx check.

MCP backend: Concord is dropped

Studio Pro 11.16's own MCP server now covers what Concord, the optional second MCP server, supplied:

Before (Concord) Now
delete_document, the DROP fallback ped_delete_document only (11.16+)
--mcp-save removed. PED writes each change to disk as it lands, measured on 11.12 and 11.16
--mcp-check project-wide ped_check_errors (11.15+)
--mcp-run execute_handler mendix_app (11.16+)
  • Removed: --mcp-concord, --mcp-concord-dial and --mcp-save.
  • Capabilities: mxcli mcp capabilities reports the DROP, --mcp-check and --mcp-run paths, each with the version it needs.
  • Not yet called live: run itself, because it would start the maintainer's app.
  • Proposal: playwright check through Studio Pro's MCP browser.

Lint

  • New SEC011: a page XPath through an association that the page's roles can't read (Lint rule request: page XPath filters through an association the viewing role cannot read build clean, then fail at runtime #1471).
    • For example, where [Shop.Order_Account/Shop.Order/IsActive = true()] fails at Production security with SecurityRuntimeException: No access rights, while mxbuild, check, exec and lint were all clean.
    • This was measured on 11.15 with run --local and playwright check. The same XPath inside an entity access rule opens fine.
  • MPR012 reports every widget measured as CE0582 under the React client. Before, it covered only the static and dynamic image. It now also covers the classic drop-down, the legacy data grid and the reference selector, each with its replacement. This was measured on 11.16 with controls. Widgets that weren't measured are left out.

Pages

  • An uncaptioned custom-content data grid column gets an empty header. column colTriage (Caption: '') rendered "colTriage" in the header. An empty header builds clean and renders blank, which was checked at runtime on 11.15

claude and others added 30 commits October 28, 2026 17:49
#1753)

Symptom, from the issue: "On the after-startup test runner, a test with
more than one `@expect` is reported **PASS** when an earlier assertion
fails and a later one passes. `mxcli test` exits 0." Affected: every
Docker run and `--local --legacy-runner`; the endpoint runner is not.

Cause: writeExpectAssertion logs one verdict line per assertion,
MXTEST:PASS:<id> or MXTEST:FAIL:<id>:..., and ParseLogResults let every
line overwrite the test's status. The last line won, and the PASS case
also cleared the failure message. The generated flow returned
$AllPassed = false, but nothing reads it.

Fix: the parser folds a test's verdict lines with a sticky failure
(applyVerdictLine). Once a test has a FAIL or an ERROR, no later PASS or
SKIP line clears it. The first failure's message is reported, with
"(and N more failure(s) in this test)" when more followed. This matches
the endpoint runner, which evaluates an assertion only while $Verdict is
PASS and whose skipped-assertions guard never replaces a failure. The
skipped-assertions FAIL (#1448), logged after the PASS lines of the
assertions that ran, still wins over them: a sticky failure, not "first
line wins". A RUN line resets the fold, so a failure cannot outlive its
run. The fix is in the parser, so it also covers logs written by runners
generated before it. The generator is unchanged: logging one verdict per
test would change a flow whose verdicts were measured on 11.13.0, and it
would cover nothing more, because parser and generator ship together.

Measurement (unit level, the symptom's layer: the parser reads a log).
sticky_failure_test.go, run before the fix existed and again with
results.go reverted:

  --- FAIL: TestLaterPassDoesNotClearEarlierFailure
      FAIL then PASS: status = PASS, want FAIL
      FAIL then PASS: message = "", want "Expected $r = 1, actual: 2"
      FAIL then PASS: the suite passed, so `mxcli test` exits 0
  --- FAIL: TestFirstFailureMessageIsReported
      message = "Expected $r = 4, actual: 2", want "Expected $r = 1, actual: 2 (and 2 more failures in this test)"
  --- FAIL: TestLaterPassDoesNotClearEarlierError
      ERROR then PASS:test_1: got PASS "", want ERROR "Setup failed: Shop.Seed"

TestSkippedAssertionsFailureStillWins and the PASS-then-FAIL and
PASS-then-PASS cases pass both before and after; they are the controls.
With the RUN reset stubbed out, TestRunLineStartsVerdictAfresh fails.
mdl-examples/bug-tests/test-runner-1753-earlier-expect-fails.test.mdl is
the runtime reproduction. It was not run against an app for this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…he after-startup runner

Symptom, from #1753's sibling note: "on this runner,
`assignPattern` matches only upper-case `CALL|CREATE`. So mdl 1's
`$r = call …` is not renamed per test, and two tests that both use `$r`
fail injection (MDL063 / CE0111, measured)." Affected: every Docker run
and `--local --legacy-runner`. The endpoint runner gives each test its own
flow and does not rename.

Cause: the legacy runner compiles every test into one microflow, so it
gives each test's variables a `_N` suffix. assignPattern decides which
variables are renamed, and it matched only `CALL|CREATE` in upper case.
MDL keywords are case-insensitive, and mdl 1 and `fmt` write them in
lower case, so `$r = call microflow …` stayed `$r` in every test.

Fix: assignPattern matches `call|create` in any case, followed by a word
boundary so `$r = callback` does not count. No other pattern in
cmd/mxcli/testrunner makes the upper-case assumption: the rest lower-case
their input or use EqualFold.

TestThrowsCatchesEveryCallKind and TestVerdictCatchesEveryCallKind
matched the legacy output against the unrenamed lower-case statement, so
they passed only because of the defect. Their legacy assertions now
expect the renamed statement (legacyStmt).

The 1448 bug-test's note that the legacy runner "needs a distinct
variable name per test" is updated.

Measurement. rename_keyword_case_test.go with the old pattern (the
upper-case control subtest passes):

  --- FAIL: TestLegacyRunnerRenamesLowerCaseAssignments/call_microflow,_mdl_1
      test 1: no "$r_1 = call microflow" — the assignment was not renamed
      an assertion still reads the unrenamed $r
      MDL063: '$r' is created twice in this microflow — first by call
      microflow, then by call microflow. ... mxbuild rejects this with
      CE0111 "Duplicate variable name"
  (and the same for mdl 0, call java action, create object, create list,
  mixed case)
  --- FAIL: TestAssignPatternNeedsAWholeKeyword
      control: $r = call microflow was not picked up: map[]

I also swept every non-.fail `.test.mdl` in mdl-examples/ (16 files)
through GenerateTestRunner and executor.ValidateProgram. Before the fix,
10 files drew MDL063, among them the 1448, 1514 and 923 bug-tests and
doctype-tests/microflow-spec (86). After it, 2 do. Those 2 come from
`retrieve $Brands` and from `retrieve $Request` / `$Request = head(…)`.
They are a separate gap: assignPattern has never matched `retrieve`,
`declare`, list operations, `send rest request` or
`execute database query` in any case. This change does not address it.

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

Symptom (#1218): a bare `create or modify module role M.R;`
reported "Modified module role: M.R" and erased the role's stored
description. `describe` -> `exec` was unaffected because describe prints
the description; grants were untouched.

Cause: CreateModuleRoleStmt.Description was a plain string, so "no
description clause" and `description ''` were the same value, and the OR
MODIFY branch of execCreateModuleRole passed it to AddModuleRole, which
overwrites the existing role in place.

Fix: Description is a *string, nil when the statement has no clause, as
CreateUserRoleStmt's properties have been since #707. OR MODIFY
writes the stored description when it is nil, so the write is elided
and the run reports "Unchanged"; `description ''` clears it. Adopting an
auto-provisioned role still drops its marker description. Docs (skill,
docs-site reference, `mxcli syntax`) state the rule.

Measurement:
- Unit test (mdl/executor/issue1218_module_role_description_test.go)
  fails before the fix, and with the fix forced back, with the reported
  symptom: description written = "", want "Read the published OData
  services. Nothing else." / Modified module role: Shop.ApiUser.
- Real app, Mendix 11.13.0: role created with a description; bare
  statement with the fixed build -> "Unchanged module role", describe
  keeps the description, describe -> exec "Unchanged"; with the pre-fix
  build -> "Modified module role" and the description is empty.
  `description ''` with the fixed build clears it. mx check: 0 errors in
  every state, the same as the blank app, fault included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…fold at 10.0

Symptom (#1163, 02-microflow-examples on 9.24.30): every
count/sum/average/minimum/maximum in a microflow was refused with
"this Mendix 9.24.30 project cannot store
Microflows$AggregateAction.AggregateFunction (Mendix 10.0.0+)".

Cause: not an older storage shape. The vendored generated version data
gave aggregateFunction the row of reduceReturnDataType
({Introduced 10.0.0, Required}) and had no row for reduceReturnDataType,
so the property floor refused the key every aggregate carries and let the
fold's return type through unrefused.

Evidence: mendixmodelsdk 4.117.0 has aggregateFunction with no version
(its default even changes at 9.24.0) and reduceReturnDataType introduced
10.0.0. Studio Pro 9.24.0 stores AggregateFunction on 10 of 10 aggregates
in Workflow Commons 2.6.0. A floor-off build's aggregate survives
`mx convert` 9.24.30 with every key.

Fix: correct the Microflows$AggregateAction row by hand (gen cannot be
regenerated here) and regenerate the copy in modelsdk/version.

Measured: bug-test 1163-aggregate-on-mendix9.mdl on a fresh 9.24.30 app:
exec writes all five aggregates, mx check 0 errors, mx convert strips
nothing. Before, the first was refused. Control: the new tests fail on
the unfixed table with the reported refusal. 02-microflow-examples on
9.24 now stops at the expression aggregate (`sum … of …`, UseExpression
and Expression, Mendix 10.0+), which is correct.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Symptom (#1163, 25-rename on 9.24.30): every workflow was
refused with "this Mendix 9.24.30 project cannot store
Workflows$Workflow.PersistentId (Mendix 10.21.0+)". The same held on
10.0-10.20.

Cause: the workflow writer, and the ALTER WORKFLOW mutator for the
activities it inserts, mint a fresh PersistentId on the workflow and on
every activity, outcome and boundary event. mendixmodelsdk introduces
PersistentId in 10.21.0 on all four (Workflow, and the abstract
WorkflowActivity, Outcome and BoundaryEvent). The floor refused the
workflow's own. It never saw the others, which are declared on supertypes
it does not look up. `mx convert` 9.24.30 strips the PersistentId from
all 6 element types a floor-off build wrote.

Fix: canon.LowerToProjectShape drops PersistentId from every Workflows$
element below 10.21. It runs on every unit write before the floor, so
CREATE, CREATE OR MODIFY and wfmutator's raw inserts are all covered.
The ID is dropped rather than refused because the writer mints it; no
script states it, and Studio Pro 9.24 stores none.

Measured: bug-test 1163-workflow-on-mendix9.mdl on a fresh 9.24.30 app:
exec ok, mx check 0 errors, `mx convert -p` strips no key. 25-rename on
9.24: FAIL (the refusal) -> PASS. Controls: 25-rename and
24b-workflow-mendix10 PASS on 10.24.24 and 11.13.0. The unit test fails
on the unfixed code with the reported refusal.

Not fixed: a parallel split writes Workflows$EndOfParallelSplitPathActivity,
a 10.21 type, and 9.24 then does not load the project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Symptom (found while measuring #1163): with PersistentId
lowered, `mx convert -p` 9.24.30 strips nothing from an mxcli-written
workflow but adds WorkflowOnStateChangeEvent: null and
UsertaskOnStateChangeEvent: null. Studio Pro's next save then differs in
property names, which Mendix's merge engine refuses to compare (#1373).

Cause: the two defaults (mendixmodelsdk: introduced 9.12.0, removed
11.0.0) were measured on 10.24.24 only, and an entry is added only from
the lowest version it was measured on.

Fix: lower their measured floor to 9.24.30, where `mx convert` adds both
with the same null value.

Measured: bug-test 1163-workflow-on-mendix9.mdl on a fresh 9.24.30 app:
mx check 0 errors, and `mx convert -p` now neither strips nor adds a key
on the workflow. Before, it added these two. The test fails on the
previous floor (present false on 9.24). The 11.13 case stays absent as
the control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Symptom (#1163, 14-project-settings on 9.24.30):
`alter settings constant @M.C value '...' in configuration 'Default'`
was refused with "this Mendix 9.24.30 project cannot store
Settings$ConstantValue.SharedOrPrivateValue (Mendix 10.9.0+)". The same
held on 10.0-10.8.

Cause: the settings overlay writes a new override as a Settings$SharedValue
nested under SharedOrPrivateValue. From 10.9 that is the only shape Mendix
reads. Below 10.9 an override is {$ID, $Type, ConstantId, Value} with a
flat string (mendixmodelsdk: Value deleted and SharedOrPrivateValue
introduced in 10.9.0). With the floor off, `mx convert` 9.24.30 turns the
nested form into Value: "" and the override silently becomes empty.

Fix: canon.LowerToProjectShape replaces SharedOrPrivateValue with the flat
Value in its place, or fills the empty flat sibling the overlay leaves.
A Settings$PrivateValue has no flat form, so it is refused, naming the
constant. The reader already preferred a flat Value; a backend test now
pins reading and updating a 9.x flat override.

Measured: bug-test 1163-constant-override-on-mendix9.mdl on a fresh
9.24.30 app: describe settings shows 'broker:9092', mx check 0 errors,
and mx convert keeps the value and changes no key in the settings unit.
14-project-settings on 9.24: FAIL (the refusal) -> PASS. Controls:
10.24.24 and 11.13.0 PASS. The unit tests fail on the unfixed code with
the reported refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Symptom (#1163, 29-datagrid DG12 on 9.24.30): a page passing
`$currentObject` to a microflow or nanoflow was refused with
"this Mendix 9.24.30 project cannot store
Forms$MicroflowParameterMapping.Expression (Mendix 10.10.0+)". The same
held on 10.0-10.9.

Cause: bindParameterMappingValue writes such an argument as Expression
text with Variable null. Expression is 10.10.0 on both mapping types.
Before 10.10 a mapping holds only Variable, a Forms$PageVariable that is
required there. Measured with the floor off: 9.24 does not load the page
at all ("An error occurred when trying to set the 'Variable' property of
a Microflow parameter mapping ... Value cannot be null"). Studio Pro 9
stores no mapping for an argument that is the enclosing object; it
infers it. The 9.24.30 starter app and Workflow Commons 2.6.0 (9.24.0)
have 82 flow calls taking 1-2 parameters and no mapping on any of them.

Fix: below 10.10, canon.LowerToProjectShape leaves a $currentObject (or
empty) argument out of every ParameterMappings list. An argument bound
through Variable is kept; the floor drops its empty Expression. Any other
expression has no form there, so it is refused, naming the parameter.

Measured: bug-test 1163-flow-arguments-on-mendix9.mdl on a fresh 9.24.30
app: exec ok, mx check 0 errors, mx convert changes no key on the page,
and describe -> exec is Unchanged. 29-datagrid on 9.24 now stops at DG15
(page variables, 10.17, a correct refusal); the DG2 NullReferenceException
in mx check is the separate CE0463 gap. Controls: 29-datagrid PASS on
10.24.24 and 11.13.0. The unit tests fail on the unfixed code with the
reported refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Symptom (#1163, 10-odata and 18-folder on 9.24.30): a published
OData service was refused at write time with "this Mendix 9.24.30 project
cannot store ODataPublish$PublishedODataService2.IncludeMetadataByDefault
(Mendix 10.8.0+)". By then the script's earlier statements were already
written.

Cause: not the property. mxcli writes the service as
ODataPublish$PublishedODataService2, a document type mendixmodelsdk
introduces in 10.4.0. Before 10.4 a published OData service is a
Rest$PublishedODataService, which is deleted in 10.4.0: a different
document with its own children, and mxcli does not write it. The
generated version data has property floors only, so the floor tripped
over the first versioned key. Measured with the floor off: mx check
9.24.30 cannot load the project ("TypeCacheUnknownTypeException: The
type cache does not contain a type with qualified name
ODataPublish$PublishedODataService2"). Lowering the property would have
made an unloadable app.

Fix: a version gate. `create [or modify] published odata service` needs
integration.published_odata_service, 10.4.0. `check -p` and exec's
pre-flight report it as MDL-VERSION01 before anything is written, with
a hint naming the 9.x document. Writing Rest$PublishedODataService for
9.x is not attempted here.

Measured: bug-test 1163-published-odata-below-10-4.mdl with `check -p`
on a fresh 9.24.30 app reports MDL-VERSION01. 10-odata and 18-folder on
9.24 now stop at the gate. Controls: both PASS on 10.24.24 and 11.13.0.
The new gate cases fail without the gate (0 violations).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
The 9.24 doctype run for #1163 split five refusals three ways:
a wrong floor in the generated data (AggregateFunction), a document type
the version lacks (PublishedODataService2, where lowering would have
produced an unloadable project), and an older shape that stores nothing
(a flow argument that is the enclosing object). Record what settled each
one: the SDK's version data, Studio Pro-authored bytes from a Marketplace
.mpk, and `mx convert -p` on the older version against a floor-off build,
comparing values as well as keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Fix test-runner false pass, module role description loss, and Mendix 9/10 property rows
`git remote get-url` applies url.<base>.insteadOf, and doctorEnv left
the system, global and GIT_CONFIG_COUNT/KEY/VALUE configuration in
place. In a sandbox that rewrites git@github.com: to https://github.com/
through the environment, the SSH remote the tests add already read back
as HTTPS, and TestGitDoctor_SSHRemoteRewrittenByFix and
TestGitDoctor_SSHURLFormRewrittenByFix failed `make test` on main (#1670)
while CI was green. A ~/.gitconfig rewriting https to git@ fails the
same two tests the other way.

doctorEnv now sets GIT_CONFIG_GLOBAL to the null device,
GIT_CONFIG_NOSYSTEM=1 and GIT_CONFIG_COUNT=0. Control: before the fix
both tests fail under each rewrite separately (environment, and HOME
with an https->git@ insteadOf); with an empty HOME and no environment
rewrite they pass. After it, both rewrites pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
fix(test): git doctor tests no longer read the developer's git config
go.mod's toolchain line makes `go` (and `make fmt`) switch to go1.27.3,
but fmt-check ran a bare `gofmt`, which is whatever Go is installed. With
go1.24.7 installed, `make lint` failed main on a file CI accepts
(mdl/backend/mfmutator/splice_handler_test.go) and passed a file CI then
rejected (#1759).

GOFMT defaults to $(go env GOROOT)/bin/gofmt. Control: `make fmt-check
GOFMT=/usr/local/go/bin/gofmt` still fails on splice_handler_test.go;
the default passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
`make build test lint` covers less than CI's build-and-test job gates
on: check-mdl, check-findings, check-skill-index, check-wiki-pages and
the rest ran only in CI unless someone remembered them. ci-local runs
the job's steps in its order (check-skill-mdl standing for its three
skill/docs MDL steps), without govulncheck, which the job installs
@latest over the network, and without the integration jobs, which need
mxbuild.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Holds this repository's conventions for driving a PR Claude opened to
green: the user reviews before the PR is opened and it is merged on
green by default (merge commit, expectedHeadSha, branch restarted from
main afterwards); `make ci-local` before every push; the macOS
tunnel-seam job that is cancelled before it gets a runner, and so gets
one re-run; the conflicts this repo keeps hitting (CHANGELOG, generated
indexes and tables); and how to treat the AI PR Review comment's
Critical / Moderate / Minor sections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
`git clone` never fetches refs/notes/*, so on a fresh clone or a CI
checkout `git doctor` reported every commit as "without a note" and
exited 1, while the same report said `[x] origin has
refs/notes/mx_metadata`. `--fix` then wrote computed notes over commits
the remote already annotates, and the push it recommends was rejected
as non-fast-forward. Anyone who forced that push would overwrite the
real notes on the remote.

The remote check already reads the remote's notes commit with
ls-remote; it is now passed on. If the local ref is not, or does not
contain, that commit, the commits are not judged. The finding is the
unfetched ref, given with the command that fetches it. `--fix` fetches
it as a fast-forward only, so a note that exists only locally is never
dropped, and stops if that fetch is refused.

Test: TestGitDoctor_UnfetchedNotesAreNotMissing (bare remote, pushed
note, fresh clone). Before the fix it fails with the reported symptom:
"[ ] 9450425668 without a note" next to "[x] origin has
refs/notes/mx_metadata", and --fix replaced the remote's note on HEAD.

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

`git doctor` read the remote with `git remote get-url`, which applies
url.<base>.insteadOf. With a global https -> git@ rule (a common
developer setup), an HTTPS remote read as SSH and the doctor exited 1.
`--fix` then set the configured URL to HTTPS on every run, which the
rule rewrote straight back, and dropped the URL's `.git` suffix along
the way. #1765 isolated the tests from this and left the
product question open; this answers it.

The check now reads `git config --get remote.<name>.url`, the value
--fix actually writes. An SSH remote is still found and rewritten when
a rule makes it read as HTTPS. When the effective URL differs, the
report gives it as an [i] line naming the insteadOf rule, which --fix
leaves alone. It is not counted as an issue: the rule applies to every
git command on the machine, and changing the remote cannot undo it.

Tests: TestGitDoctor_InsteadOfRewriteIsNotAnSSHRemote and
TestGitDoctor_ConfiguredSSHRemoteFoundBehindRewrite. Both use a rule in
the test repository's own config, on a host nobody rewrites. Before the
fix: "origin: git@git.example.com:org/app.git — SSH; Studio Pro needs
HTTPS", --fix changed the configured URL to
https://git.example.com/org/app, and the configured SSH remote was
missed. The two existing SSH tests also pass now in an environment that
rewrites github.com URLs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
`mxcli git commit --dry-run` staged everything in the real index to
report it, then undid that with `git reset -q`. A reset goes back to
HEAD, not to the state before the run, so anything the user had staged
was unstaged. The docs promise that a dry run writes nothing.

A dry run now stages into a temporary copy of the index
(GIT_INDEX_FILE), so the real index is never touched: staged changes,
stat data and intent-to-add entries all stay as they were. A real
commit still uses the real index, as before. `gitOutInEnv` is the git
helper with extra environment.

Test: TestGitCommit_DryRunKeepsTheUsersStaging. Before the fix:
`staged after --dry-run = "", want exactly what the user had staged
(staged.txt)`. The existing dry-run test started from an empty index,
where the bug and the correct behaviour look the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
`mxcli git commit` dropped any path with a segment named
`.mendix-cache`, `deployment`, `theme-cache` or `.mxcli`, at any depth.
So `resources/deployment/endpoints.xml` was silently left out of the
commit. The only trace was a count in "Left out 1 file(s) Studio Pro
never commits".

Those folders are the project's own: Studio Pro, mxbuild and mxcli
create them directly beside the .mpr. isJunkPath now matches them there
only. When the project sits in a subfolder of the repository, that
subfolder is the anchor, taken from gitNoteRepo.MPRPath. The lock,
backup and user-settings file names still match at any level.

Tests: TestGitCommit_NestedFolderNamedLikeJunkIsCommitted and
TestGitCommit_JunkAnchoredAtProjectInSubfolder. Before the fix both
fail: "resources/deployment/endpoints.xml is not the project's own
folder and must be committed", and the same for
javascriptsource/theme-cache/, app/resources/deployment/ and
tools/deployment/. The project's own deployment/ and theme-cache/ stay
out, as the existing spaced-path test also checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
Add the steward skill and `make ci-local`; fmt-check uses the toolchain's gofmt
fix(git): doctor and commit — unfetched notes, insteadOf, dry-run staging, nested build folders
…fig hide rules

A pluggable widget's enumeration that its editorConfig hides must hold its
default, or mx check reports CE0463. mxcli refused that for the one Timeline
rule it read (`groupEvents: false`), but `widget describe timeline` showed
"2 of 6 editor hide-rules recognized". So `customVisualization: true,
groupByDayOptions: 'dayMonth'` and `groupByKey: 'day', groupByMonthOptions:
'monthYear'` passed check and exec and failed mx check (#1762,
mendixlabs#1668).

Two shapes were unread:

- terser folds the statements before a `switch` into its head,
  `switch(A ? hide : hide, B || hide, e.groupByKey)`, so the THEN branch's
  guard is bounded by the `switch(` paren, which parseGuard took for a
  conditional group. A `switch(`/`if(` head directly in a function body is
  now statement-level; a call's paren still is not.
- a hide in a `case`/`default` arm had no reading. switchArmCondition now
  states the arm's values as eq/in, or ne/notin over the other cases for a
  default arm, following fall-through and grouped labels. Anything it
  cannot state exactly (a guarded hide inside the arm, a non-literal
  label, an arm whose exit is unclear, a switch under an `if`) yields no
  rule, as before.

Timeline is 6 of 6 (3.1.1, 3.2.2, 3.2.3), so each hidden enumeration now
gets MDL-WIDGET10's existing verdict: an error for a non-default value,
which exec's preflight refuses before writing; a warning at the default;
written at the default when left out. Across every installed package
dumped before and after, only Timeline and HeatMap gained rules (HeatMap
folds an `if(` head the same way). MDL-WIDGET10 names an in/notin set
instead of "matches". WidgetDefGeneratorVersion goes to 20 so cached
definitions regenerate.

The custom-mode warning on title/description/timeIndication came from the
hand-written fallback table, whose comment claimed only TextTemplates can
cause CE0463. That comment, and the writer's, are corrected.

File Uploader's ternary-wrapped hidePropertiesIn (#258 item 1)
was already lifted by the else-if chain support of mendixlabs#1583.
It is pinned by a test against File Uploader 2.5.0.

Measured on Mendix 11.13.0 with Timeline 3.2.2, one widget per variant:
the six hidden variants were CE0463 and the three controls 0 errors before
the fix; check now refuses exactly those six.

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

Once the Timeline's hide rules are read (#1762), one of its two
format enumerations is always hidden: by customVisualization, or by the
groupByKey switch. The generic pluggable-widget describe printed every
stored primitive, so each Timeline came back with a hidden property at its
default and drew an MDL-WIDGET10 warning on mxcli's own output (11 warnings
on six measured pages). The same happened before for any widget hiding an
enumeration by a rule the extractor already read, such as a Timeline with
`groupEvents: false`.

Such a property carries nothing: exec writes a hidden property the script
leaves out at that same default. extractExplicitProperties now drops a
top-level property that the widget's own rules hide in the stored
configuration while it equals the stored schema's default. The rules are
the registry's, the ones `check` evaluates, so the two agree about what is
hidden. A hidden property holding a non-default value is still printed,
because that widget fails mx check with CE0463, and printing it lets
`check` say so.

describe -> check -> exec on three Timeline configurations: no MDL-WIDGET10,
and the stored page is byte-identical afterwards.

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

Re-running `create or modify microflow|nanoflow` with only a filter's
expression changed replaced the activity: the activity, its action and its
operation all got new $IDs, while a decision whose condition changed the
same way kept its own, and exec reported both as `spliced: 1 replaced`.
Studio Pro compares and merges by $ID, so it showed the edit as one activity
removed and another added (#1761, mendixlabs#1667).

A splice Replace by one activity of the same activity and action $Type now
edits the stored activity: the replacement is written into the stored slot,
at the stored size, and its elements are paired structurally with the stored
ones (same $Type at the same key; list entries by the member, parameter or
language they name; unnamed entries by position only for equal $Type
sequences). The stored $IDs go on at Bytes, rewriting every reference in the
same pass, so the executor can still name the fragment's activity until
then. This applies at top level, in a loop body and to an activity with a
custom error handler, for create or modify and for alter ... replace.

A run of adjacent statements each changed in its own place, pairwise of one
plain kind, is diffed one-for-one instead of as a replace-and-drop of the
whole run, so each is edited in place. The report tells the two outcomes
apart: `edited in place` for a node that kept its $ID (an activity of the
same kind, a decision's condition, a return's value, a node's notes) and
`replaced with new element IDs` for one that did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
The alter syntax help, the alter-microflow reference page and the microflow
and nanoflow skills now say that an activity replaced by one of the same kind
is the stored activity edited, keeping its $ID and those of its action and
the parts that pair up, and how exec reports it (`edited in place` against
`replaced with new element IDs`) (#1761).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…w Mendix 10.21

Symptom (#1163): on Mendix 9.x and 10.0-10.20 a workflow with a
`parallel split` made the project unloadable. exec reported "Created
workflow", then mx check died with "TypeCacheUnknownTypeException: The
type cache does not contain a type with qualified name
Workflows$EndOfParallelSplitPathActivity".

Cause: the builder and the ALTER mutator end every path with that marker,
because from 10.21 the engine runs a path only up to it. The type itself
was introduced in 10.21.0 (mendixmodelsdk 4.117.0). The generated version
data holds property floors only, so nothing compared a written $Type with
the project's version.

Older shape, measured: Studio Pro 9.4.0 (the Marketplace's "Example
project workflow extension" 2.0.0) stores a split whose
ParallelSplitOutcome is {$ID,$Type,Flow} and whose paths hold their
activities and nothing more. Lowering produces exactly that key set.

Fix: canon.LowerToProjectShape drops the marker from every Workflows$Flow
below 10.21, for every writer at once (create, create or modify, and a
path alter workflow inserts).

Measured on fresh apps, the bug-test exec'd then mx check:
- 9.24.30.51827: before TypeCacheUnknownTypeException, after 0 errors;
  mx convert 9.24.30 changes no key; describe -> exec "Unchanged".
- 10.12.19.81612: before TypeCacheUnknownTypeException, after 0 errors;
  mx convert 10.12.19 changes no key.
- 10.24.24 and 11.13.0: 4 markers, 0 errors, key sets identical to the
  unfixed build.
Control: stubbing the lowering fails
TestNormalize_ParallelSplitPathsBelowMendix10_21 on 9.24 and 10.20 with
the marker still in each path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…Mode their floors

Symptom (#1163): on Mendix 9.0-9.19 every pluggable widget's
schema lost the Type of its return types and kept an AssignableTo that
version does not have (49 return types across the embedded templates).
A return type reaching the floor there was refused with "this Mendix
9.18.0 project cannot store CustomWidgets$WidgetReturnType.Type (Mendix
9.20.0+)".

Cause: the vendored generator gave a property the row of the next
property in the SDK's versionInfo and left that one out, as it did
AggregateFunction. mendixmodelsdk 4.117.0 has WidgetReturnType
`type: {}`, `assignableTo: {introduced 9.20.0}`, and pages.GroupBox
`caption: {}`, `headerMode: {introduced 8.10.0}`. widgetTypeForVersion
drops undeclared keys from the widget schema by the same data, so it
dropped the right key.

Evidence: Studio Pro stores {EntityProperty, IsList, Type} on 208 of 208
return types in a 9.1.1 app and 326 of 326 in a 9.4.0 app (Marketplace
samples), and adds AssignableTo on 57 of 57 in a 9.24.3 one.

The group box row changes nothing mxcli writes: the caption is stored
as CaptionTemplate, which the table never keyed, and 8.10 is below
every supported version. Workflows$CallMicroflowTask.boundaryEvents was
already 10.14.0, as in the SDK, and stays. Measured on 10.12.19 with
that floor removed: mxcli writes BoundaryEvents [2] on every call and
mx convert strips it, so the floor's drop-empty is right. A test now
pins it; removing the row fails it.

Fix: correct both rows by hand and regenerate modelsdk/version. Only
those two rows change in the copy.

Measured: the bug-test on fresh 10.24.24 and 11.13.0 apps checks at 0
errors before and after, with identical key sets. 9.24.30 stores Studio
Pro 9.24.3's return type keys both times (its mx check stops on a
separate pre-existing NullReferenceException in WidgetValue.IsDefault).
Control: the new tests fail on the old rows (49 wrong key sets at 9.18;
the Type refusal).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…e Studio Pro's own (measured on the lab repo, S0)
engalar and others added 29 commits November 1, 2026 14:21
Concord, the optional second MCP server, is no longer needed: Studio Pro's
own server covers what it supplied.

- DROP calls ped_delete_document (11.16+) and no longer falls back to
  Concord; a server without it gets an error naming the version.
- --mcp-check runs a project-wide ped_check_errors (11.15+, all pages).
- --mcp-run starts the app through execute_handler mendix_app (11.16+),
  gated on tool presence and a query_handlers getDetails lookup.
- --mcp-save goes: PED persists every write to disk as it lands.
- --mcp-concord / --mcp-concord-dial, ConcordConnected and concord.go are
  removed; the capability report names the three paths instead.
- Includes the native-delete work: Client.HasTool, and CallTool classifying
  "Error executing tool handler: ..." (isError:false) as an error.

Docs, the live-edit skill, the feature matrix and PED_MCP_CAPABILITIES.md
are updated; MCP_SAVE_VERIFICATION.md (a --mcp-save procedure) is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Proposes 'mxcli playwright check --mcp <url>': the same verdict and
assertions, driven through Studio Pro 11.16's browser_* handlers instead
of a local Chromium. Maps each PageSignals field to its handler source,
records the absent evaluate/screenshot handlers, and leaves the handler
input schemas as an open measurement (query_handlers timed out).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ll-aa5aa4

# Conflicts:
#	.claude/skills/fix-issue/findings/mdl-backend/2026-11-01-mcp-drop-ignores-ped-delete-document.json
#	CHANGELOG.md
#	docs/01-project/MDL_FEATURE_MATRIX.md
#	mdl/backend/mcp/businessevent.go
#	mdl/backend/mcp/capabilities.go
#	mdl/backend/mcp/capabilities.yaml
#	mdl/backend/mcp/concord.go
#	mdl/backend/mcp/constant.go
#	mdl/backend/mcp/delete.go
#	mdl/backend/mcp/delete_test.go
#	mdl/backend/mcp/enumeration.go
#	mdl/backend/mcp/microflow.go
#	mdl/backend/mcp/nanoflow.go
#	mdl/backend/mcp/page.go
#	mdl/backend/mcp/workflow.go
MPR012 named only the static and dynamic image. The classic drop-down, the
legacy data grid and the reference selector are CE0582 under the React client
too, and lint said nothing about the ones a project already holds.

The set is measured, not taken from the documentation: mxbuild 11.16.0 on a
project switched to the React client reported CE0582 on the data grid and the
reference selector (also with a select page) beside both image widgets, and
11.14.0 on the drop-down. The controls stay silent and are pinned by the test:
navigation tree, menu bar, simple menu bar, file manager and scroll container
(11.16.0), image uploader (11.13.0), and the common input and container widgets
(11.15.0). Template grid and the reference set selectors were on no web page
available to measure and are left out.

The rule is renamed ReactUnsupportedWidget (ID unchanged), and each finding
names its replacement.

Closes #564

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Through the generic `pluggablewidget 'com.mendix.widget.web.datagrid.Datagrid'`
form, a custom-content column with `Caption: ''` (or none) got its MDL column
name as the header text, and end users saw "colTriage". Mendix stores no name
for a column, and Data grid 2 renders the header text as is
(`header.trim() || ' '` in Datagrid.mjs).

The name was a stand-in for an empty header, which ledger #54 round 2 took to
be CE0463. It is not: that CE0463 was a null TextTemplate, and the empty
Forms$ClientTemplate that update-widgets writes builds clean (ChipCoV8 #11;
0 errors on 11.15.0, where the keyword `datagrid` form already writes it).

Verified in the running app (11.15.0): the page written by the old build
renders "colTriage" in its header, the one written by this build does not.

Closes #535

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot read

At Production security the runtime checks member access on every association
step of a page data source's XPath, on the entity the step starts from, and
refuses the retrieve: the page opens to an error dialog, "No access rights for
Shop$Account/Shop.Order_Account". mxbuild, check and lint were all clean.

Measured on 11.15.0 with run --local and playwright check, as a user role
holding one module role:
- owner Both, traversed from the TO entity, member at None: refused;
- default owner, traversed from the FROM entity, member at None: refused;
- default owner, traversed from the TO entity (not a member there): opens;
- the same XPath inside an entity access rule: opens;
- control, members granted ReadOnly: every page opens.

SEC011 reports exactly the refused cases, per user role that can open the page
and reads the starting entity, as an error naming the member to grant. Live, it
flags the two refused pages of the measured app and nothing on the control;
on FactoryManagement (Production, 27 page XPaths with paths) it reports none.

Closes #945
Closes #1337

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ll-aa5aa4

# Conflicts:
#	cmd/mxcli/engine.go
#	cmd/mxcli/mcp.go
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… command line

Merges the pull request's head (2c118da) as it stands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
gitInE and the real-mx integration fixture ran git without the identity
gitIn sets. git merge needs one even when it stops on a conflict, so on a
host whose name gives git nothing to guess from (a bare "vm", no domain)
every git-merge fixture failed before the command under test ran. The
integration fixture drops the global config, so the command's own merge
and commit there need the identity in the process environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
When git merge stopped before any conflict existed, the error was built
from its stdout, which holds at most an "Updating a..b" line; git's
reason is on stderr. A dirty working tree came out as
"git merge side: Updating 5d6b73c..2c3fd47" and a missing identity as
"git merge side: " with nothing after it. Both the merge and the --plan
path now report git's own message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
When Mendix's merge engine stops on a decision it still writes a result
and marks each undecided unit in its Unit row: ContentsConflicts holds a
JSON list instead of '' (every other row holds '', never NULL; measured on
mx 11.13), TreeConflict is non-zero for a conflict about whether the
document exists. mx check reports CE0137 while a marker is present.

Reader.UnitConflicts lists the marked units. Writer.ClearContentsConflicts
records contents conflicts as decided, leaving the row as the engine writes
every unit it merged, under the Studio Pro guard and with _Transaction
bumped like every other write; tree conflicts are left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
applyPlan resolved from git's two sides: ours for every model file git
conflicted, theirs for the planned units. With the template listing only
the engine's ManualResolutionRequired units, a unit the engine merges by
itself was taken from ours wholesale. Measured with mx 11.13: each side
adds an entity to one domain model (AutoResolved) and both edit one
entity's documentation (ManualResolutionRequired); the plan names only the
second unit, and its commit lost theirs' entity, hash gate passing.

The plan step now runs the engine again over clean worktrees and
resolves over its result: everything it merged stays merged, each planned
unit is taken whole from its side's commit, and its conflict marker is
cleared. The plan must name exactly the units the engine left open (a
stale or hand-widened plan is refused), and nothing is committed while the
index still marks a unit as conflicted. The template and the check read
the same set (engineDecisions), with the result's markers as the fallback
when the engine prints no -v list.

The real-mx integration test applies a generated plan to that scenario on
a scaffolded project and asserts both entities, theirs' page, no marker,
and no CE0137 from mx check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
Its engine section (folder arguments, MINE as the working tree) is what
failed on Linux; the shipped design takes three .mpr paths over clean
worktrees, and --plan now resolves over the engine's result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
MPR012: report every widget measured as CE0582 under the React client
Write an empty header for an uncaptioned custom-content data grid column
…ve-driscoll-aa5aa4

# Conflicts:
#	CHANGELOG.md
…xpath-association-access

# Conflicts:
#	CHANGELOG.md
SEC011: page XPath through an association the page's roles cannot read
feat(mcp): drop Concord; check and run through Studio Pro's own MCP
feat(git): mxcli git merge (mendixlabs#1674), with --plan resolved over the engine's result
docs(proposals): playwright check through Studio Pro's MCP browser
@ako
ako merged commit 97923d0 into mendixlabs:main Nov 1, 2026
15 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.

Lint rule request: page XPath filters through an association the viewing role cannot read build clean, then fail at runtime

3 participants