Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions contrib/inbox/2026-10-21-macos-mendix10-mxbuild-docker-shim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# macOS + Mendix 10: no native mxbuild, so the gate needs a Docker shim, and doctor.sh reads a shim that exits 3 on `--version` as broken

**From:** a field project (anonymised) — a Mendix 10.24.18 app changed with mxcli v0.27.0 and this toolkit, on an Apple Silicon Mac
**Date:** 2026-10-21
**Kind:** learning
**Field evidence:** `exec.sh`'s mxbuild gate and the microflow tests both ran on this Mac only
through a Docker shim; each fact below was re-checked on 2026-10-21.
Toolkit master 393d253. Related: MendixMau/mxcli-project-toolkit#525.
**Proposed target:** a "Mendix 10 on macOS" row in the lane table of `skills/doctor-triage.md`
(line 100 says the CDN toolchain is Linux-only), and the probe note in `bin/doctor.sh`

---

## What is true on this Mac

- The Studio Pro 10.24.18 app bundle has no `mx` or `mxbuild` (its `Contents/modeler` holds 502
files, none of them named `mx` or `mxbuild`).
- The cached 10.24.18 build in `~/.mxcli/mxbuild/` is a Linux ELF (`file`: ELF, ARM aarch64). `mxcli test --local` (v0.27.0) stops with "mxbuild from the Mendix CDN
is a Linux binary and cannot run natively on darwin".
- Mendix 11 differs here. The Studio Pro 11.12.4 bundle holds `mx` and `mxbuild` as Mach-O arm64
(`file`), and #525 reports a Mach-O mxbuild in the 11.14.0 cache. So doctor-triage's "Linux only"
is right for Mendix 10 on a Mac and wrong for the 11 builds #525 looked at. Not run: the toolkit's gate on Mendix 11 here.

## The shim

`find_mxbuild` honours `MXBUILD_PATH` (`project-bin/_common.sh:557`), so a script there replaces
mxbuild. The toolkit calls it as `mxbuild --java-home=… --java-exe-path=… --write-errors=<file>
--target=deploy <mpr>` (`_common.sh:909-910`) and reads the file's `problems[]` (severity `Error`) and
`errors[]` (why nothing was checked). Name the file for the model version: `_common.sh:1044-1047`
warns when the path doesn't contain it.

A working one is about 100 lines of bash:

1. Take `--write-errors=` and the one argument not starting with `--` (the .mpr); ignore the rest.
2. Read the version from the .mpr: `select _ProductVersion from _MetaData`. Require the matching
Linux `mx` in the mxcli cache; otherwise stop (a different version gives a false green).
3. Start a throwaway container with that `mx` and the project mounted read-only, copy the project
in, run `mx check <mpr> -j check.json`.
4. Convert `check.json` to `{"errors":[],"problems":[{"severity":"Error","errorCode","message",
"locations"}]}`, and compare its count with the line `The app contains: N errors`.
5. Exit 0 clean, 1 errors, 3 not run. Every failure to check (Docker down, wrong version, timeout,
count mismatch) exits 3 and writes `{"errors":[{"message":"…"}],"problems":[]}`: the gate reads
non-zero exit with no problems as "not verified" (`_common.sh:949`).

It does no deploy build (no Java compile, no theme bundling). On a 10.24.18 test app it printed
`The app contains: 0 errors.`, exit 0, in 52 s. A sibling script runs `mxcli test --local
--skip-app-startup` in a container with the same 0/1/3 exits; in the field it ran 110 tests, all
passed, exit 0 (hsqldb instead of PostgreSQL, the after-startup microflow not run).

## The doctor probe

`probe_runs` (`bin/doctor.sh:145-166`) runs `<mxbuild> --version`, then `--help`; exit 126/127 means
a wrong-platform binary, and any other non-zero exit with both is "cannot run". `doctor.sh:892-899`
then reports "mxbuild exists but cannot run (exit 3)" and sets `GATE_OK=0`. A shim that wants an
.mpr and exits 3 without one fails this. Checked by running the extracted function on two stubs:
one without special handling returned 3; one answering `--version`/`--help` with exit 0 and a line of
text returned 0 (`how=--version`). So a shim should answer both with exit 0 before it looks for an
.mpr or starts Docker.
80 changes: 80 additions & 0 deletions contrib/inbox/2026-10-21-mxpatch-three-lookups-that-miss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# mxpatch: three lookups that find nothing on a domain model or a flow's own documentation

**From:** a field project (anonymised) — a Mendix 10.24 app changed with mxcli v0.27.0 and this toolkit
**Date:** 2026-10-21
**Kind:** learning
**Field evidence:** hit while writing a byte patch that rewords documentation (a change MDL can't
express without rewriting the element). Each lookup below was re-run on the toolkit's own fixture
model (`tests/wave2/fixtures/mxpatch/`, rebuilt from `TestApp.mpr.sql` with `executescript`,
Mendix 11.14.0) with `project-bin/mxpatch.py` on master 393d253, so nothing here depends on the
field app.
**Proposed target:** the LIBRARY section of the `project-bin/mxpatch.py` docstring (a short
"lookups that miss" list), or `skills/learned-mdl-preflight.md` next to the line that says some
changes are "not MDL at all"

---

A patch author reaches for `find_unit` and `find_element` and gets `NotFound` three times. None of
the three is a bug; each is how the model is stored.

## 1. An entity's `$Type` is `DomainModels$EntityImpl`

The natural call, with the type spelled `DomainModels$Entity`, fails:

```
find_element(dm.doc, "Car", "DomainModels$Entity")
-> NotFound: no element named Car (DomainModels$Entity) in this unit
find_element(dm.doc, "Car", "DomainModels$EntityImpl") # works
```

On the fixture model, `find_elements(dm.doc, type_="DomainModels$EntityImpl")` returns the three
entities (`Car`, `VCar`, `ApprovalContext`), and nothing else is stored under `DomainModels$Entity`.
Leave the type off (`find_element(dm.doc, "Car")`) if the name is unique in the unit.

## 2. A domain-model unit has no `Name`

The unit's `Name` key is empty (`None`), so the unit is identified by its module, not by a name:

```
m.find_unit("DomainModels$DomainModel", "MyFirstModule")
-> NotFound: no unit DomainModels$DomainModel MyFirstModule in <model.mpr>
mxpatch.py find <model.mpr> --type 'DomainModels$DomainModel'
-> DomainModels$DomainModel MyFirstModule.None mprcontents/84/77/<uuid>.mxunit
```

The `.None` in the `find` output is the missing name. The CLI's `get` and `set` require `--name`
(`mxpatch: --type and --name are required`), so they can't select a domain model at all. Use the
library and pick the unit by module:

```
dms = [u for u in m.units("DomainModels$DomainModel") if m.module_of(u) == "MyFirstModule"]
car = mxpatch.find_element(dms[0].doc, "Car", "DomainModels$EntityImpl")
mxpatch.set_value(car, "Documentation", "A car.")
m.write_unit(dms[0], apply=False) # dry run: prints the new byte count and ContentsHash
```

That sequence ran as written on the fixture model: `changed`, then a dry run for the one unit.

## 3. A flow's own documentation is on the unit root, and `find_element` skips the root

`find_elements` and `find_element` search "below the unit root" (`if d is doc: continue`,
`project-bin/mxpatch.py:181`). A microflow's `Documentation` is a key on the unit root,
so looking up the microflow by its own name finds nothing:

```
find_element(u.doc, "MyFirstLogic") -> NotFound: no element named MyFirstLogic in this unit
get(u.doc, "Documentation") -> '' # the key is on the root
```

Set it on the root. The CLI does this when `--element` is left out:

```
mxpatch.py set <model.mpr> --type 'Microflows$Microflow' --name MyFirstLogic --key Documentation --value 'Does X.'
-> Documentation: '' -> 'Does X.' (dry run; add --apply to write)
```

## Not checked

- Only a dry run was done; the patches were not applied and opened in Studio Pro.
- Only the microflow case was run for gotcha 3. Other unit types that keep their own documentation
on the root (workflows, pages) were not tried.
80 changes: 80 additions & 0 deletions contrib/inbox/2026-10-21-testable-retry-loop-shape.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Retry an external call in a microflow: a loop shape that lints clean, and a decision flow that tests without the call

**From:** a field project (anonymised) — a Mendix 10.24 app built with mxcli v0.27.0 and this toolkit
**Date:** 2026-10-21
**Kind:** skill-draft
**Field evidence:** the shape retries a call to an external service up to N times in a field app
(68 of 68 of that app's microflow tests passed). It was rebuilt on a synthetic Shop module and
re-run: `mxcli check`, `mxcli lint`, `mx check` in Docker, `mxcli test` in Docker. Mendix 10.24.18,
mxcli v0.27.0.
**Proposed target:** `skills/learned-microflow-patterns.md`, a new section after "Per-row
isolation in a loop" (same family: keep the loop body small)

---

## The problem

"Call the service; if the reply is bad, call again, up to N times" is usually written as
`while true` with a `return` inside the loop. On mxcli v0.27.0:

- `mxcli lint` reports MDL-FLOW01 once per `return` inside the loop. Re-run here: two `return`s,
two hits. `mxcli check` and `mx check` (0 errors) were clean, so lint is the only warning.
- A statement placed after a `while true` that has no `break` can never run. `check` accepts it,
`exec` writes it, `mx check` fails with CE0104 (upstream mendixlabs/mxcli#1491, open when this
was written; not re-run for this note).

## The shape

Put every decision in one sub-microflow that returns a status string. The loop only repeats the
calls until the status says stop. `Shop.RetryState` is a non-persistent entity with `Attempt`
(Integer, default 0) and `MaxRetries` (Integer, default 3); the counter lives there, so a test can
hand the decision flow any `Attempt`.

```
Shop.SUB_CallService() -> String the external call; empty means it failed
Shop.SUB_HandleResponse(Body, State) -> String 'stored' | 'retry' | 'failed'
Shop.ACT_CallWithRetry() -> Boolean the loop
```

`SUB_HandleResponse` is the only place the counter changes:

```
if $Body != empty and $Body != '' then return 'stored';
if $State/Attempt < $State/MaxRetries then
change $State (Attempt = $State/Attempt + 1);
return 'retry';
return 'failed';
```

`ACT_CallWithRetry` has no `return` in the loop:

```
$State = create Shop.RetryState ();
declare $Outcome String = 'retry';
while $Outcome = 'retry'
begin
$Body = call microflow Shop.SUB_CallService ();
$HandleOutcome = call microflow Shop.SUB_HandleResponse (Body = $Body, State = $State);
set $Outcome = $HandleOutcome;
end while;
return $Outcome = 'stored';
```

With `MaxRetries = 3` the service is called at most 4 times: the first call plus 3 retries.

## What was checked (synthetic module in a copy of a 10.24.18 app)

- `mxcli check` passed; `mxcli lint` had no hit on the three flows; the `while true` version with
`return true` / `return false` inside gave 2 MDL-FLOW01 hits.
- `mx check` in Docker on a model holding both versions: `The app contains: 0 errors.`
- `mxcli test` in Docker: 5 passed, 0 failed, exit 0. Four tests call `SUB_HandleResponse` with
a chosen `Attempt` and a body of `'ok'` or `''`, and assert the string and the counter: success
(counter stays 0), a retry (counter up), the last allowed retry (`'retry'`, counter 3), the
limit (`'failed'`, counter stays 3). The fifth runs the whole loop against a stub that succeeds.
- With one expectation flipped in a copy, exactly that test failed (`expected $State/Attempt = 2,
actual: 1`) and the exit was 1.

## Limits

The tests do not exercise the loop's repeat path, because the stub always succeeds; they prove
the decisions and the wiring. Not checked: how Studio Pro draws the loop.
Loading