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
9 changes: 9 additions & 0 deletions .claude/commands/mxcli-dev/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ All executor code must go through the backend abstraction layer. **`sdk/mpr` no
- [ ] **Map iteration is deterministic** — any map iterated for serialization output must sort keys first (`sort.Strings(keys)` pattern); non-deterministic output causes flaky diffs and BSON instability
- [ ] **Pluggable widgets via WidgetEngine** — new pluggable widget support uses `.def.json` + `WidgetRegistry`; no hardcoded BSON widget builders in the executor

### Catalog builder passes
A refresh rebuilds only the units a save changed and keeps every other row of the cached catalog (ako/mxcli#1804). A pass that breaks the rules below is still correct, just slower on every refresh, which no correctness test notices. See [ADR-0014](docs/13-decisions/0014-incremental-catalog-builds.md).
- [ ] **Rows have an owner**: each per-document loop starts with `if !b.own(unitID) { continue }` (an entity's unit is its domain model) and ends with `b.disown()`. A table with rows from no document belongs in `projectWideTables` with a reason (`TestCatalog_RowsBelongToADocument`).
- [ ] **Listed through the scope**: documents come from `b.lister()` or a `cached*` accessor, not `b.reader.List*`, which decodes the whole project (`TestBuilderListsDocumentsThroughTheScope`, `TestCatalog_IncrementalRefreshDecodesOnlyWhatItRebuilds`).
- [ ] **Other documents through the tables**: a pass that needs another document's name or shape reads the catalog tables an earlier pass wrote (see `associationEnds`, `entityNamesByID`), not a decoded list, which holds only the rebuilt units.
- [ ] **Dependencies are refs**: if a document's rows depend on another document, the document has a `refs` edge to it, or the dependency is invisible to the plan. Add a step to `incrementalSteps` where the other document changes and this one does not; it must fail with the dependency removed.
- [ ] **New lookups are traced**: a new lookup of another document's name calls `b.dependsOn(site, name)`, or goes through the member-ref index, which probes for you. `TestCatalog_RefsCoverEveryCrossDocumentRead` must stay at 100%; `MXCLI_REF_COVERAGE_MPR=<copy of an app> go test ./mdl/executor -run TestCatalog_RefCoverageOfProject -v` measures a real project.
- [ ] **New document type**: an incremental step for it in `incrementalSteps`. A type no per-document pass owns makes every change to it a full build.

### Full-stack consistency for MDL features
New MDL commands or language features must be wired through the full pipeline:
- [ ] **Grammar** — rule added to `MDLParser.g4` (and `MDLLexer.g4` if new tokens)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "cmd/mxcli", "date": "2026-10-30", "symptom": "`.claude/bootstrap-mxcli.sh` on Windows (Git Bash) with no mxcli on PATH: `curl: (22) The requested URL returned error: 404` for `.../nightly/mxcli-mingw64_nt-10.0-26200-amd64`, and the session starts without mxcli because the hook runs `sh .claude/bootstrap-mxcli.sh || true`", "cause": "The download asset name was `mxcli-$(uname -s | tr A-Z a-z)-<arch>`. Under Git Bash, MSYS2 and Cygwin `uname -s` is `MINGW64_NT-10.0-26200` / `MSYS_NT-…` / `CYGWIN_NT-…`, never `windows`, and the Windows release assets carry `.exe`. The script also hard-coded `./mxcli` as its binary path everywhere", "file": "`cmd/mxcli/init_hook.go` (`bootstrapScriptTemplate`); tests `cmd/mxcli/init_hook_platform_test.go`", "insight": "**`uname -s` is a kernel name, not a GOOS.** Mapping it to the release asset needs the same table the Makefile `release` target uses for its output names (linux/darwin/windows plus `.exe`), and that table is easy to check against. The script's tests only ran on Linux, so no test ever hit the Windows branch, and `requireSh` skips Windows CI anyway. A fake `uname` on PATH reaches every platform branch from a Linux runner, and the curl stand-in must log the URL: before this fix it logged only the output file, which is why a wrong asset name could not fail any test. Detect the platform once at the top and keep the binary path in one variable (`$MXCLI` = `./mxcli` or `./mxcli.exe`); otherwise every `[ -x ./mxcli ]` guard has to rely on MSYS's implicit `.exe` lookup. On Windows, remove a stray non-executable `./mxcli` (the Linux build `mxcli new` leaves) before installing `./mxcli.exe`, because Git Bash resolves the exact name first. Remove it before the install and not after, since Cygwin's `rm ./mxcli` can also take `mxcli.exe`", "refs": ["mendixlabs/mxcli#1680"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/catalog", "date": "2026-10-30", "symptom": "Every catalog refresh rebuilt every table from every unit after any save: about 4.5 s of decoding and inserting on a 4,753-unit app, most of it for rows identical to the cached ones (5.6-8 s end to end per refresh)", "cause": "The builder had no record of which unit a row came from, so it could neither keep an unchanged unit's rows nor drop a changed unit's; every pass listed and decoded every document of its type", "file": "`mdl/catalog/builder_tx.go` (`builderTx` / `Stmt` record rowid ranges per owner), `mdl/catalog/builder_incremental.go` (`loadPrevious`, `planIncremental`, `dependentUnits`, `dropRebuiltRows`, `verifyIncremental`, `persistOwners`, `clearCatalog`), `mdl/catalog/builder.go` (`buildOn`, `buildAttempt`, phase list), `own()` at the head of every per-document loop, `mdl/backend/modelsdk/units.go` (`WithUnitScope`), `modelsdk/mpr/reader_units.go` (`ListUnitsByTypeWhere`)", "insight": "**Record ownership where rows are written, not per table.** A wrapper on the build's transaction notes the rowid range of every INSERT under the pass's current owner, so 130 insert sites needed no change. The per-document loops each call `own(unit)`, and a row written while the pass is skipping a document fails the build back to a full one, which caught a bug on its own: the backend lists System enumerations that are no units. Start from the cached catalog (sqlite3_deserialize) and delete the rebuilt units' rows rather than copying the kept ones: copying re-tokenized every carried full-text row and cost more than the rest of the build. Deserializing the same connection twice crashed SQLite (modernc) once its full-text tables had been used, so plan on a scratch copy of the same bytes and empty the tables with DELETE to fall back. The equivalence test passed its control at first because mxcli edits rewrite the documents that name what they change; only a step where a document's rows change while its unit does not (a delete over a repointed association) shows the dependents are needed", "fix": "Rebuild only changed units and the documents whose refs name them (members by prefix, specializations transitively), list and decode only those through a unit-scoped backend view, verify after the build that no document names a changed unit's new names (else widen once), fall back to a full build for units no per-unit pass reads (folders, modules, settings, security, navigation). Tests: `TestCatalog_IncrementalBuildEqualsFullBuild` and `TestCatalog_IncrementalFastBuildEqualsFullBuild` (every table vs a forced full build over 19 edit steps, chained incrementally), the source test, and unit tests for ranges, the skipped-document error and dependents"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/exprcheck", "date": "2026-10-30", "symptom": "`if trim($Value) != empty` passed `check --references`, `exec` and `mx check`, and was always true: a blank or empty value took the 'has text' branch and crashed later in the flow.", "cause": "Not an mxcli write defect — a platform semantics gap. Mendix String functions (trim, toLowerCase, toUpperCase, substring, replaceAll, toString) return '' for an empty input, never empty, so the comparison is constant. exprcheck had no rule for it; the expression is well-typed, so no kind check could see it.", "file": "`mdl/exprcheck/parser.go` (checkStringFuncComparedWithEmpty, called from parseCmp)", "insight": "Settled by measurement, not by reading docs: probe microflows run as `mxcli test --local --db-type hsqldb` on a fresh `mxcli new` 10.18.5 app (no Postgres needed), each classifying f(x) as EMPTY / '' / text for x in empty, '', blanks — with the bare variable as the control that DOES compare equal to empty. The same probe found `empty + ''` is the text 'null'; that one is a pitfalls entry, not a warning, because whether an operand is empty is not knowable at check time. Warn only on measured functions.", "refs": ["mendixlabs/mxcli#580"]}
27 changes: 18 additions & 9 deletions .claude/skills/mendix/check-syntax/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ description: "Validate MDL with `mxcli check` before presenting or executing it,
| lines | section |
|---|---|
| 29-35 | When to Use This Skill |
| 36-286 | `exec` refuses what `check` rejects |
| 287-422 | Pre-Flight Validation Checklist |
| 423-458 | Two rules that only real validation used to catch |
| 459-505 | Validation Workflow |
| 506-527 | Script Execution Behavior |
| 528-588 | Script Organization |
| 589-641 | Troubleshooting Parse Errors |
| 642-699 | Studio Pro MCP — Verification Only |
| 700-705 | Related Skills |
| 36-295 | `exec` refuses what `check` rejects |
| 296-431 | Pre-Flight Validation Checklist |
| 432-467 | Two rules that only real validation used to catch |
| 468-514 | Validation Workflow |
| 515-536 | Script Execution Behavior |
| 537-597 | Script Organization |
| 598-650 | Troubleshooting Parse Errors |
| 651-708 | Studio Pro MCP — Verification Only |
| 709-714 | Related Skills |

<!-- /skill-index -->
# MDL Syntax Validation Skill
Expand Down Expand Up @@ -272,6 +272,15 @@ reported by `check`:
- **`empty($List)`** — E014. `empty` is a Mendix **keyword**, not a function, so
the parser stops at the `(`. Write `$List = empty` or `length($List) = 0`.

A **warning** rather than a refusal, because the model is valid and builds:

- **`trim($v) != empty`** — E015. A String function (`trim`, `toLowerCase`,
`toUpperCase`, `substring`, `replaceAll`, `toString`) returns `''` for an
empty input, never `empty`, so the comparison is constant. Write
`trim($v) != ''`, which also covers an empty `$v`.
Not reported when the same `and`/`or` chain also compares that call with
`''` (`trim($v) != empty and trim($v) != ''`): redundant, but correct.

**One thing to know about hint ordering**: the reference check runs before
expression checking and **exits on its first error**, so an unrelated mistake
anywhere in a file hides every expression hint in it. If you expect an E0xx and
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/mendix/write-lint-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ Returned by `permissions()` (all types) or `permissions_for()` (entity-specific)
| `target_type` | string | What it points AT, upper-case: `"ENTITY"`, `"ASSOCIATION"`, `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"LAYOUT"`, `"WORKFLOW"`, `"WIDGET"`, `"JAVA_ACTION"`, `"JAVASCRIPT_ACTION"`, `"REST_OPERATION"`, `"REGULAR_EXPRESSION"`, `"ATTRIBUTE"`, `"ENUMERATION"`, `"ENUMERATION_VALUE"`. `LAYOUT`, `WIDGET`, `ATTRIBUTE`, `ENUMERATION` and `ENUMERATION_VALUE` are only ever targets; `SCHEDULED_EVENT`, `PROJECT_SETTINGS` and the service types only ever sources |
| `target_id` | string | Target UUID |
| `target_name` | string | `"Sales.Customer"`; three-part for an attribute or an enumeration value: `"Sales.Order.Total"`, `"Sales.OrderStatus.Open"` |
| `ref_kind` | string | How it references: `"call"`, `"create"`, `"retrieve"`, `"change"`, `"delete"`, `"commit"` (a commit action, or a create/change that commits — beside its `"create"`/`"change"` edge; a commit of a variable whose entity the flow cannot tell has no edge), `"show_page"`, `"datasource"`, `"action"`, `"layout"`, `"parameter"`, `"return"`, `"generalize"`, `"associate"`, `"home_page"`, `"login_page"`, `"menu_item"`, `"calculate"`, `"schedule"`, `"validate"`, `"settings"`, `"widget"`, `"sync"`, `"publish"` (a published REST operation, OData resource or action, or a service's authentication microflow), `"event"` (an entity event or a business event handler), `"member"` (binds/reads/writes an attribute or navigates an association), `"xpath"` (an XPath constraint names it), `"type"` (typed as an enumeration), `"value"` (an expression names an enumeration value), `"mapping"` (an import/export mapping maps the entity) — lower-case, unlike the types above. Attribute names used only through a variable in a free-text expression (`$Order/Total`) have no edge |
| `ref_kind` | string | How it references: `"call"`, `"create"`, `"retrieve"`, `"change"`, `"delete"`, `"commit"` (a commit action, or a create/change that commits — beside its `"create"`/`"change"` edge; a commit of a variable whose entity the flow cannot tell has no edge), `"show_page"`, `"datasource"`, `"action"`, `"layout"`, `"parameter"`, `"return"`, `"generalize"`, `"associate"`, `"home_page"`, `"login_page"`, `"menu_item"`, `"calculate"`, `"schedule"`, `"validate"`, `"settings"`, `"widget"`, `"sync"`, `"publish"` (a published REST operation, OData resource or action, or a service's authentication microflow), `"event"` (an entity event or a business event handler), `"member"` (binds/reads/writes an attribute or navigates an association), `"xpath"` (an XPath constraint names it), `"type"` (typed as an enumeration), `"value"` (an expression names an enumeration value), `"mapping"` (an import/export mapping maps the entity), `"split"` (a decision branches on an object being the entity) — lower-case, unlike the types above. Attribute names used only through a variable in a free-text expression (`$Order/Total`) have no edge |
| `module_name` | string | Source module |

### project_security
Expand Down
30 changes: 30 additions & 0 deletions .claude/skills/mendix/write-microflows/reference/pitfalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,36 @@ log info node 'Orders' 'Order ' + $Order/Number + ' shipped';
A template kept in a variable cannot be filled by `with (…)` at run time: write the
template in the statement, or build the whole text in the expression.

### 17. A String Function Never Returns `empty`

`trim`, `toLowerCase`, `toUpperCase`, `substring`, `replaceAll` and `toString` return
`''` for an `empty` input, never `empty` (measured on the Mendix 10.18.5 runtime). So
comparing their result with `empty` is constant: `!= empty` is always true, and a
blank value goes down the "has text" branch. `check` warns (E015).

```mdl
-- ❌ WRONG: always true — trim(empty) and trim(' ') are both ''
declare $HasText boolean = trim($Value) != empty;

-- ✅ CORRECT: trim(empty) is '', so this covers empty, '' and blanks
declare $HasText boolean = trim($Value) != '';
```

### 18. Concatenating `empty` Gives the Text `null`

`+` turns an `empty` String operand into the four characters `null`, not `''`:
`'[' + $Value + ']'` with an empty `$Value` is `[null]` (measured on the Mendix 10.18.5
runtime). A label built from an attribute that may be empty shows "null" to the user.
`check` cannot know whether a value will be empty, so it does not warn — guard it:

```mdl
-- ❌ WRONG: shows 'Customer: null' when Name is empty
declare $Label string = 'Customer: ' + $Customer/Name;

-- ✅ CORRECT
declare $Label string = if $Customer/Name != empty then 'Customer: ' + $Customer/Name else 'Customer: ';
```

## Implicit Variable Creation (CE0111 Duplicate Variable)

These statements **implicitly create a new variable** with the name on the left side:
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Headline: **Write what the project's own Mendix version stores, and commit it th

### Fixed

- **`show references` finds flows that branch on an entity** (ako/mxcli#1804) — an inheritance split (`split type $x when Module.Entity`) names the entity only on its outgoing flows, and the catalog recorded no edge for it, so `show references to` and `impact` on the entity missed the flow. It is now a `split` edge. Found by the new ref coverage trace, which checks that every document a catalog pass reads is reachable through refs.
- **A catalog refresh after a save rebuilds only what the save changed** (ako/mxcli#1804) — every refresh rebuilt every catalog table from every document, about 4.5 s of decoding and inserting on a 4,753-document project, and more on Windows, after any edit. The cache now records which document each row came from. The next refresh starts from the cached catalog and rebuilds only the changed documents and the documents that reference them, reading and decoding only those. A refresh after one edit on that project went from 5.6–8 s to 0.7 s end to end, with or without the source index. It falls back to a full build when a folder, module, settings, security or navigation document changes. Tests compare every table with a forced full build after each of 19 kinds of edit.
- **`playwright check` no longer fails a page checked by the app's own host name behind a proxy** (ako/mxcli#1800) — with `https_proxy` set and an app checked as `http://frontend.local:8180/` (an `/etc/hosts` name for `127.0.0.1`), a page that rendered failed on two console errors, `WebSocket connection to 'ws://frontend.local:8180/mxdevtools/' failed: … Unexpected response code: 502`, while the same check on `127.0.0.1` passed. The runtime serves its dev channel on that name; the 502 came from the proxy, because Chromium sends `ws://` to the HTTPS proxy and `no_proxy` listed only the loopback names. The browser that `playwright check`, `run --page-check` and `run --screenshot` start now reaches a host that resolves to this machine directly. No console error is ignored: the channel connects, and a genuine console error still fails the page.
- **Published OData entity sets, OData actions and older domain models have the property names Studio Pro writes** (ako/mxcli#1805) — on a Mendix 11.14.0 project `mxcli fix property-sets` reported seven `MIXED` names and could fill none of them. An entity set published without stating its modes was written without `ReadMode`, `InsertMode`, `UpdateMode` and `DeleteMode`, and an action whose microflow takes no parameters without `Parameters`; they are now written, as the values Mendix already read the missing ones as (read from the database, changes not supported, no parameters), so the service behaves as before and `describe` now prints the four modes. The domain-model names (`LocalizeDate` on a DateTime attribute, the four system-attribute flags on an entity without a generalization) are not left out by any current statement: they belong to entities an older mxcli wrote, which every later write of the domain model kept as they were. Both are now filled from values measured with `mx convert` on 10.24.24, 11.13.0 and 11.15.0, on the next write of the document or by `mxcli fix property-sets`. Run `mxcli fix property-sets` once to complete what an earlier mxcli wrote.
- **A retrieve over a self-reference is the one object Mendix makes it, and a list use of it is refused by `check -p`** (ako/mxcli#1793) — for a Reference from an entity to itself (`M.Order_Parent`), `create microflow` typed `retrieve $P from $Order/M.Order_Parent` as a list of `M.Order` unless `$P` was later used as an object, so `$n = count $P;` passed `check -p` and `exec` and failed `mx check` with CE0013 "Input variable 'P' must be of type 'List'.". Mendix follows a self-reference from its FROM end: a Reference gives the one object, a Reference set the list. The full build, `alter`'s splice and `check -p` now take that one rule, from the entity and from a specialization of it alike, and `check -p` and `exec`'s pre-flight report MDL-RETRIEVE01 for the one object a Reference gives from its FROM entity — a self-reference or not — taken by an aggregate (CE0013), a list operation other than `range` (CE0097), a `loop` (CE0100) or a list parameter (CE0117). `set` on a self-reference's object is refused like `set` on any object (CE7247); the Change list Replace it used to be written as passed `mx check` only because mx check does not type a Change list's variable. Measured on 11.13.0: before, `count $P` passed `check -p` and `exec` and was CE0013; now `check -p` refuses it, and the object uses and the Reference set's list uses build at 0 errors.
Expand Down
Loading
Loading