Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4895932
Stop a module's bundled widgets rolling back newer ones
claude Aug 12, 2026
fb4e40b
Apply a configuration's constants at boot; stop diff inventing edits
claude Aug 12, 2026
44cab29
Resolve associations inherited from a generalization in GRANT
claude Aug 12, 2026
961a349
Read a java action's microflow parameter as a microflow, not a String
claude Aug 12, 2026
568726e
Skip the project lookup for code actions a script itself creates
claude Aug 12, 2026
844ec19
Keep a specialization's inherited associations when reconciling acces…
claude Aug 13, 2026
1d2d7f8
Reconcile access rules after dropping an association
claude Aug 13, 2026
c766a76
Correct what update_configuration does, measured against a live runtime
claude Aug 13, 2026
5fd3b43
Propose a precedence chain for constant values
claude Aug 13, 2026
89af9a2
Run --local tests with the same constants as run --local
claude Aug 13, 2026
353c5cb
Add --constant, for a value that reaches one run and nothing else
claude Aug 13, 2026
68505ee
Add a machine-local constant store, for a secret that must not be com…
claude Aug 13, 2026
6f9dd5c
Apply a constant change to a running app without restarting it
claude Aug 13, 2026
92e1df0
fix(microflow): author and round-trip the import-mapping Range
claude Aug 13, 2026
4fc5251
Stop an empty `with ()` from crashing every command that parses the file
claude Aug 13, 2026
db8449e
Record what a partial constants payload actually does, against the di…
claude Aug 13, 2026
7e35905
fix(mappings): resolve JSON members by either name, and stop inventin…
claude Aug 13, 2026
0ed74ab
fix(mappings): stop copying the JSON snippet's sample value onto mapp…
claude Aug 13, 2026
8289656
Stop rebuilding the same marketplace reference project every time
claude Aug 13, 2026
8071df6
fix(mappings): only refuse an unknown member when a schema says so
claude Aug 13, 2026
bd1835d
Merge pull request #140 from ako/claude/mxcli-issues-ovfoxk
ako Aug 13, 2026
4d97491
Merge branch 'main' into claude/mxbuild-diagnostics-spike-emta6h
ako Aug 13, 2026
4e4a2fb
Merge pull request #139 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 13, 2026
074c6e1
Merge remote-tracking branch 'upstream/main' into sync-check
claude Aug 13, 2026
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
14 changes: 14 additions & 0 deletions .claude/skills/fix-issue.md

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions .claude/skills/mendix/download-marketplace-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,61 @@ Dependencies include **widget content**, not only modules — `ConversationalUI`
`Markdown viewer` (230248) and `Events` (224259) widget packages, which surface as
`CE0462 "Could not find widget ... in the 'widgets' directory"`.

### Module packages bundle their own widgets — install order used to matter

A module's `.mpk` carries a copy of every widget its pages use, pinned to
whatever its author had at release time, and different modules pin different
versions of the **same** widget. Measured on the published packages:
Atlas_Web_Content 4.3.0 ships five Data Widgets at **3.4.0** that DataWidgets
3.11.3 ships at **3.11.3**.

`install` and `update` never roll a widget back: a bundled copy older than the
one in the project is kept out and reported.

```text
Kept 5 newer widget(s) the package would have rolled back:
widgets/com.mendix.widget.web.Datagrid.mpk — kept 3.11.3, package ships 3.4.0
...
```

Before this, updating modules in one order and then another silently downgraded
widgets, and nothing surfaced it — an older widget is not a `mx check` error, so
the app just ran old widget code. If you are on an older mxcli, check the
versions by hand:

```bash
for f in widgets/*.mpk; do
printf "%-50s %s\n" "$(basename $f)" \
"$(unzip -p "$f" package.xml | grep -oP '<clientModule[^>]*version="\K[^"]+')"
done
```

Read `<clientModule version>`, not the `<package version>` on the root element —
that one is the manifest schema and is `1.0` for every widget ever published.

A package that ships a widget **twice** (as a `.mpk` and as an unpacked tree —
FeedbackModule 5.0.0 does) installs only the `.mpk`; the unpacked twin is skipped
and reported.

### When the installed version has been unpublished

`update` and `diff` download the *installed* version to establish the "has anyone
edited this?" baseline, so both fail when that version is gone from the
marketplace. A blank 11.13 app ships NanoflowCommons 6.0.0 and the 6.x line now
starts at 6.1.1, so the module most in need of updating is exactly the one whose
baseline cannot be built.

```text
version "6.0.0" not found; run 'mxcli marketplace versions <id>' to list available versions
The installed version is the baseline for "has anyone edited this?", so it has to be
downloadable. It is not, and --force does not help: there is nothing to compare against.
hint: re-run with --no-baseline to update without that check (local edits are lost silently)
```

`--force` does not help — it overrides a *finding*, and here there is no finding.
`--no-baseline` accepts that the question cannot be answered and updates anyway.
Commit first: local edits to that module go without being named.

## Step 4 — Repair the model after the install (required, headless)

```bash
Expand Down Expand Up @@ -206,11 +261,29 @@ Administration — installed 4.3.2 (Mendix 11.12.1)
CONFLICT ENTITY Account
```

**Tell the user the first one is slow.** Answering needs a reference project —
a blank app with the published module imported — and `--to` needs two. Measured
on Administration at 11.12.1: **~50s** the first time, **~13s** afterwards, once
`~/.mxcli/marketplace-refs/` holds the blank app and the built references. Run
`diff` before `update` rather than instead of it: the `update` reuses the base
reference the `diff` just built, so the pair costs little more than the `diff`.

Set `MXCLI_NO_REF_CACHE=1` if a result looks stale and you want to rule the
cache out — it rebuilds everything without deleting the evidence.

It downloads the installed version's `.mpk`, imports it into a throwaway reference project
built **at the project's own Mendix version** (a mismatch is refused, not warned about —
Mendix's own conversions would otherwise read as your edits), and compares `DESCRIBE`
output on both sides.

**A "modified" verdict now means the difference is real.** Some element types
DESCRIBE renders imperfectly — a snippet whose body comes out `{ }`, a building
block under "Building blocks are read-only; they cannot be created via MDL" — and
two imperfect renderings can differ for reasons that have nothing to do with you.
Those are reported `unknown`, never `changed`, and `--save-edits` refuses to write
them: replaying `create or modify snippet X (Folder: 'Web') { }` would **empty**
the snippet.

**Read `verified`, not just `locallyModified`.** An element that cannot be described is
reported as `unknown`, never as unchanged, and `verified: false` means "no modifications
found" is not a conclusion:
Expand Down
57 changes: 57 additions & 0 deletions .claude/skills/mendix/json-structures-and-mappings.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,37 @@ A JSON structure defines the schema of a JSON payload. It stores a JSON snippet
### Import Mappings
An import mapping converts a JSON string into Mendix entity objects. It maps JSON fields to entity attributes.

#### Two names per member: the raw key and the exposed name

Every JSON structure element stores **both**, and for any lowercase-initial key
they differ:

| | Example | Used for |
|---|---|---|
| **Path** (raw JSON key) | `(Object)\|uuid` | what the **runtime** resolves by |
| **ExposedName** (derived) | `Uuid` | what **Studio Pro displays** |

Mendix derives the exposed name by capitalising the initial, and for an array's
item object by suffixing `Item` — so `total` → `Total`, `camelCase` → `CamelCase`,
`__Value` (array) → `__ValueItem` (its item). Keys already starting with an
underscore are left alone: `__returnedCount` stays `__returnedCount`.

This is **Mendix's own convention, not something mxcli does**. A blank app's
Studio-Pro-authored `FeedbackModule.JSON_AppInsightsResponse` stores
`ExposedName: "Uuid"` against `Path: "(Object)|uuid"`, and its `IMM_PostResponse`
binds `JsonPath: "(Object)|uuid"`.

Consequences worth knowing:

- **Either spelling works in MDL.** `Total = total` and `Total = Total` produce the
same stored mapping. Write whichever you have.
- **`DESCRIBE` emits the exposed name**, because that is the name Studio Pro shows.
A describe → edit → exec cycle is therefore lossless, but the text you get back
will not match the raw JSON keys you wrote.
- **A member matching neither spelling is refused**, listing what would have
worked. It is never written with a guessed path: such a mapping passed
`mxcli check` and failed later in mxbuild (CE5015) or at runtime.

#### Inherited attributes

Mendix inheritance is multi-table: all of a parent's attributes are members of the
Expand Down Expand Up @@ -304,6 +335,32 @@ $PetResponse = import from mapping Module.IMM_Pet($JsonContent);
import from mapping Module.IMM_Pet($JsonContent);
```

#### Range — how much of the result to bind

Optional trailing clause, matching Studio Pro's **All / First / Custom** setting
on the activity. Omit it and mxcli infers from the mapping's own root shape, as
it always has.

```sql
$Pets = import from mapping Module.IMM_Pets($Json) all; -- All (the default)
$Pet = import from mapping Module.IMM_Pets($Json) first; -- First: ONE object
$Page = import from mapping Module.IMM_Pets($Json) limit 10; -- Custom
$Page = import from mapping Module.IMM_Pets($Json) limit 10 offset 5;
```

`first` is a separate word from `limit 1` on purpose: `limit 1` is a *list* of
one, `first` binds a single *object*, so the result variable's type differs.

Two things the range does **not** do:

- **It does not change what the mapping returns.** An object-rooted mapping
binds an object under every range — `all` on one is Studio Pro's own default,
and the blank app ships one (`FeedbackModule.SUB_Feedback_PostToAppInsights`).
Only `first` narrows a list mapping to a single object.
- **`offset` is not accepted everywhere.** Mendix rejects it with
**CE6100** ("This entity does not support offset") unless the mapping's root
is a list; `limit` alone is fine either way. Verified on mxbuild 11.6.6.

### Export to Mapping (entity → JSON)

```sql
Expand Down
40 changes: 40 additions & 0 deletions .claude/skills/mendix/run-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,43 @@ secret registers it owner-less.
and `.../dist/index.js` also returns `200` (client bundle served).
- [ ] With `--watch`, editing a microflow logs `applied via reload`; adding an entity
logs `applied via restart` and creates the table in Postgres.

## Constant values come from a configuration

`mxcli run --local` applies the constant values of the project configuration it
is running, merged over each constant's default:

```text
Applying 1 constant value(s):
Encryption.EncryptionKey configuration "Default"
```

Before this they were ignored: mxbuild writes each constant's **default** into
`deployment/model/config.json`, and that map is what the runtime is handed — so
`alter settings constant … in configuration 'Default'` executed, round-tripped
through `describe settings`, and did nothing. An app ran for hours with an empty
encryption key while the model said otherwise.

- `--configuration <name>` picks one. With several configurations and none named
`Default`, mxcli applies **none** and says so rather than guessing which
environment this run means.
- A **private** override has no value in the model at all (the value lives on the
developer's workstation), so the default is used and the constant is named.
- The line prints in every case, including "no overrides" — silence used to mean
"your override is in effect" when it was not.
- `--constant Module.Name=value` (repeatable) sets a value for **this run only**.
It wins over the configuration, is never written to the project, and is
reported as coming from `--constant` so the output says which layer won. A
constant the project does not declare is refused before the app boots: the
runtime ignores a value for a constant that does not exist, so a typo would
otherwise be reported as applied and do nothing.

Setting a constant on an app that is *already running* is a different mechanism
again: `MicroflowConstants` over the M2EE admin port, which is how Mendix Cloud
injects per-environment values. Measured on 11.12.1, `update_configuration` is
**staged rather than applied** — the running app keeps the old value until the
next `reload_model`, while the call answers `result:0` — and the admin API has no
read-back to check against. Do not reach for
`--runtime-setting 'MicroflowConstants={…}'`: it replaces the map mxcli built
rather than adding to it, and at boot there is nothing to fall back on for
`BasePath`/`DatabaseName`. Use `--constant`.
65 changes: 65 additions & 0 deletions .claude/skills/mendix/test-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,71 @@ mxcli test tests/ -p app.mpr # Docker
same project while the tests run — the tests never write into the database you
are looking at in the browser. The database is created on first use.

### Constants

A `--local` run boots the app with the **same constant values `mxcli run --local`
uses**: the project configuration's shared overrides, layered over each
constant's default. It prints what it applied before the run:

```
Applying 1 constant value(s):
MyModule.ApiKey configuration "Default"
```

Pass `--configuration <name>` to pick one when the project has several and none
is called `Default` (it refuses to guess rather than run production's values by
accident). `--attach` takes neither flag: it runs against an app someone else
booted and inherits **that app's** constants.

To set a value for one run without touching the project, use `--constant`
(repeatable). It wins over the configuration and is never written to the model:

```bash
mxcli test tests/ -p app.mpr --local --constant MyModule.ApiKey=sk-test-123
```

A name the project does not declare is **refused**, before anything boots — the
runtime silently ignores a value for a constant that does not exist, so a typo
would otherwise be reported as applied and do nothing.

The value is visible in shell history and in `ps`. That is fine for a throwaway
test value and wrong for a real secret.

This is worth knowing when a test asserts on something a constant feeds. Before
this was wired up, `--local` ran with each constant's *default* while `--attach`
ran with the configuration's, so the same suite could pass one way and fail the
other with nothing in the output to explain it.

For a secret that has to **persist** across runs, use the machine store:

```bash
mxcli constant set MyModule.ApiKey 'sk-live-...' -p app.mpr
mxcli constant list -p app.mpr # values from the store are masked
mxcli constant unset MyModule.ApiKey -p app.mpr
```

It writes `<project>/.mxcli/constants.json` (mode 0600), adds `.mxcli/` to the
project's `.gitignore` if missing, and then **asks git whether the path is
really ignored** — refusing to write the value if it is not. It beats the
configuration and loses to `--constant`.

By default the new value takes effect at the next boot. Add `--apply` to push it
into a `mxcli run --local` that is already up, without restarting it:

```bash
mxcli constant set MyModule.ApiKey 'sk-live-...' -p app.mpr --apply
```

That is two admin calls, not one: `update_configuration` is *staged* — the
running app keeps its old values and the call still answers success — and only
the following `reload_model` applies them. mxcli does both. It cannot confirm
the result, because the admin API has no way to read a constant back, so it says
so and points you at the app itself.

This is mxcli's own store, not Mendix's. Mendix's private configuration values
are encrypted per user account by Studio Pro from 10.9, so nothing headless can
read or write them. See `docs/11-proposals/PROPOSAL_constant_values.md`.

---

## Test File Formats
Expand Down
22 changes: 22 additions & 0 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,28 @@ New scripts should bind every parameter to a real expression. Use `empty`
for a Java-action argument only when regenerating MDL from an existing project
that already had an unbound parameter.

## Microflow-Typed Java-Action Parameters

Some Java actions take a **microflow** — a callback the action invokes later.
`MCPServer.AddTool` (`ExecutingMicroflow`) and `MCPServer.CreateMCPServer`
(`AuthenticationMicroflow`) are the ones you meet first. Pass the microflow's
qualified name as a quoted string; mxcli resolves the parameter's declared type
from the Java action and stores a microflow reference, not a string literal.

```mdl
$Tool = call java action MCPServer.AddTool(
McpServer = $Server,
Name = 'memory_add',
Description = 'Stores a memory',
ExecutingMicroflow = 'MyModule.MF_MemoryAdd',
Schema = ''
);
```

`DESCRIBE JAVA ACTION` prints such a parameter's type as the bare word
`Microflow` (`Nanoflow` for JavaScript actions), and that spelling is what
`CREATE JAVA ACTION` accepts, so the round-trip is stable.

## Error Handling

MDL supports error handling for activities that may fail (microflow calls, commits, external service calls, etc.).
Expand Down
Loading
Loading