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
1 change: 1 addition & 0 deletions .claude/commands/mendix/lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ mxcli lint -p app.mpr --baseline lint-baseline.json
| SEC003 | security | DemoUsersActive - Demo users should be off at Production security |
| SEC010 | security | PageEntityNotReadable - A page data widget lists an entity the user roles that open the page cannot read; at Production it renders empty, silently (info when only row-scoped) |
| SEC011 | security | XPathAssociationNotReadable - A page data source's XPath crosses an association a role that opens the page may not read; at Production the page fails to open ("No access rights for …") |
| SEC012 | security | ReachablePageNotReadable - A page a user role can reach shows an attribute or lists an entity the role may not read (mxbuild CE2729), found before a build |
| CONV011 | performance | NoCommitInLoop - Commits inside loops (commit actions, or create/change with commit) cause N+1 issues |
| CONV012 | quality | ExclusiveSplitCaption - Exclusive splits need meaningful captions |
| CONV013 | quality | ErrorHandlingOnCalls - External calls (REST/WS/Java) need custom error handling |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/catalog", "date": "2026-11-01", "symptom": "xpath_expressions.ComponentId for page and snippet data sources held `{0 [57 221 206 …]}` instead of a GUID, and no column named the widget the XPath belongs to (ComponentName is the source's $Type), so lint rule SEC011 could name a widget only when the page had one data widget over that entity.", "cause": "scanBSONForXPath formatted the decoded $ID with extractBsonIDString, whose default case is fmt.Sprintf(\"%v\", id); a decoded $ID is a bson.Binary, which no case matched.", "file": "mdl/catalog/builder_xpath.go", "insight": "A catalog package with one id formatter per builder drifts: extractBsonID, extractBinaryID and extractBsonIDString coexisted, and only the last had no bson.Binary case. Assert ids in a builder test against the formatter the joined table uses, not a hand-written string, or the test proves the format and not the join. Measured join after the fix: 6/6 on a probe app, 56/61 on FactoryManagement; the 5 misses are widgets the widgets table never indexes (MxModelReflection combo boxes), a separate gap.", "issue": "ako/mxcli#1879"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "modelsdk/mpr", "date": "2026-10-27", "symptom": "On MPR v2, a unit written twice in one `WriteTransaction` could commit the wrong bytes: writing A → B → A (A stored) committed B, with ContentsHash describing B, and no error. A → B → B counted the repeat as a landed write. Latent — no current caller writes a unit twice in one transaction (`codec.Store.FlushUnits` writes each map key once, `SaveUnit` is one transaction per unit) — but `WriteTransaction` is public library API", "cause": "`WriteUnit` reconciles every write against the stored unit (canon.Reconcile, ADR-0008), and on v2 the stored unit is the committed .mxunit; the transaction's own first write sits in `<unit>.mxunit.tmp` until Commit. So the second write of A matched the committed A, was elided as unchanged, and Commit renamed the pending B into place", "file": "`modelsdk/mpr/writer_core.go` (`WriteTransaction.storedBytes`, the read `WriteUnit` passes to `reconcileAgainst`)", "insight": "Found as the control of a different fix: the v1 WriteTransaction deadlock could also have been ended by widening the pool, and the A → B → A test was written to show why that was wrong (a second connection reads the committed bytes). Running the same three writes on v2 showed v2 had the exact failure the rejected v1 fix would have introduced, for a different reason. The invariant is one sentence: **a write in a transaction is reconciled against the transaction's view of the unit, not the committed one** — on v1 a read through the `*sql.Tx` gives that for free, on v2 it has to look at the pending temp file first. Elision is what makes the bug silent: without canon.Reconcile the second write would simply have landed. A → B → B is the companion test, since an over-eager fix (never elide while a write is pending) passes A → B → A", "refs": ["ako/mxcli#1159"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "modelsdk", "date": "2026-11-01", "symptom": "create or replace page with `Caption: ''` on a Data grid 2 column whose stored caption is 'Triage' reported 'Unchanged page' and kept 'Triage', on both the keyword `datagrid` and the generic pluggablewidget path.", "cause": "canon.CarryTranslations adds back every language a rebuilt text did not write, and the empty header was written as a Texts$Text with no translation at all, so the stored en_US 'Triage' was carried straight back. Neither the pluggable passthrough nor the per-property patch was involved.", "file": "modelsdk/canon/translations.go, mdl/executor/widget_engine.go, mdl/backend/widgetobj/builder.go", "insight": "Bisect by switching mechanisms off, not by reading them: disabling the passthrough, then the patch, changed nothing, and disabling CarryTranslations cleared the caption in one run. 'Unchanged' means canon.Equal saw the stored value in the rebuilt document, so the value is re-added after the build. A text with no translation is indistinguishable, to the carry, from one MDL cannot express; a stated empty text has to be written as (authoring language, \"\"), and the carry must drop such an item where the stored text has none in that language or every Studio Pro empty caption churns.", "issue": "ako/mxcli#1877"}
17 changes: 9 additions & 8 deletions .claude/skills/mendix/assess-quality/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ description: "Audit a Mendix project against best practices for naming, security
|---|---|
| 29-36 | When to Use This Skill |
| 37-96 | Assessment Workflow |
| 97-355 | Quality Guidelines Reference |
| 356-366 | Assessment Report Template |
| 367-379 | Executive Summary |
| 380-390 | project Metrics |
| 391-403 | Automated Findings |
| 404-423 | Manual Review Findings |
| 424-439 | Recommendations |
| 440-450 | Sources |
| 97-356 | Quality Guidelines Reference |
| 357-367 | Assessment Report Template |
| 368-380 | Executive Summary |
| 381-391 | project Metrics |
| 392-404 | Automated Findings |
| 405-424 | Manual Review Findings |
| 425-440 | Recommendations |
| 441-451 | Sources |

<!-- /skill-index -->
# Assess Mendix Project Quality
Expand Down Expand Up @@ -161,6 +161,7 @@ After reviewing automated results, assess the following areas manually. The guid
| All persistent entities must have access rules | SEC001 | Critical |
| Every role that opens a page can read the entities its grids, galleries and combo boxes list (else it renders empty at Production) | SEC010 | High |
| Every association a page data source's XPath crosses is readable by the roles that open the page, on the entity the step starts from (else the page fails to open at Production) | SEC011 | High |
| Every page a user role can reach shows only attributes and entities that role may read (else mxbuild fails with CE2729) | SEC012 | High |
| Do not grant Create or Delete rights on entity access rules | CONV006 | High |
| All entity access rules should have XPath constraints | CONV007 | High |
| Set default access rights to None | Manual check | High |
Expand Down
12 changes: 7 additions & 5 deletions .claude/skills/mendix/write-lint-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ description: "Write custom Starlark lint rules in .claude/lint-rules/ that run b
|---|---|
| 25-67 | Rule File Structure |
| 68-163 | Available Query Functions |
| 164-543 | Object Properties |
| 544-557 | Helper Functions |
| 558-644 | Common Patterns |
| 645-653 | Validation |
| 164-545 | Object Properties |
| 546-559 | Helper Functions |
| 560-646 | Common Patterns |
| 647-655 | Validation |

<!-- /skill-index -->
# Writing Custom Starlark Lint Rules
Expand Down Expand Up @@ -402,14 +402,16 @@ Returned by `xpath_expressions()`. Each row represents one XPath constraint used
| `document_id` | string | Owning document UUID |
| `document_qualified_name` | string | `"MyApp.GetActiveItems"` |
| `component_type` | string | `"RETRIEVE_ACTION"`, `"ACCESS_RULE"`, `"WIDGET"` |
| `component_id` | string | Component UUID |
| `component_id` | string | Component GUID (the data source, retrieve or access rule the expression is stored on) |
| `component_name` | string | Activity/rule name (may be empty) |
| `xpath_expression` | string | Raw XPath string, may include outer `[ ]` |
| `target_entity` | string | Qualified name of entity being queried, e.g. `"MyApp.Order"` |
| `referenced_entities` | string | Comma-separated qualified names of entities referenced by the XPath |
| `is_parameterized` | bool | True when the XPath contains `$variable` references |
| `usage_type` | string | `"RETRIEVE"`, `"SECURITY"`, `"DATASOURCE"` |
| `module_name` | string | `"MyApp"` |
| `widget_id` | string | For a page or snippet data source, the widget it belongs to — the `id` `widgets()` gives it; `""` otherwise |
| `widget_name` | string | That widget's name, e.g. `"dgOrders"`; `""` otherwise |

### expr

Expand Down
19 changes: 10 additions & 9 deletions .claude/skills/steward/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ description: Drive an mxcli pull request Claude opened to green and merge it. Co

| lines | section |
|---|---|
| 27-54 | Merge on green is the default |
| 55-72 | Before every push: `make ci-local` |
| 73-87 | Reading a red CI job |
| 88-103 | Conflicts this repo keeps hitting |
| 104-121 | The AI PR Review comment |
| 27-55 | Merge on green is the default |
| 56-73 | Before every push: `make ci-local` |
| 74-88 | Reading a red CI job |
| 89-104 | Conflicts this repo keeps hitting |
| 105-122 | The AI PR Review comment |

<!-- /skill-index -->
# Steward: Driving a PR to Merged
Expand All @@ -33,10 +33,11 @@ The user reviews a change **in the session, before the PR exists**. So:
- Once it is open, merge it without asking again as soon as all of these hold
on the **current head**:
- every job of `Build, Test & Lint` (`.github/workflows/push-test.yml`) is
green, `integration-passed` included. A push to the PR branch runs that
workflow twice on the same commit, for the push and the pull_request
event; a job that passed in one run and was cancelled before it got a
runner in the other counts as green;
green, `integration-passed` included. That workflow runs once per commit,
on the pull_request event only (#1886), and a new push cancels the
previous commit's run: a **cancelled** run on an older head is expected and
never blocks — only the current head's run counts. A branch push with no
PR gets no run; trigger one with `workflow_dispatch` if you need it;
- GitHub reports no merge conflict;
- no human review thread or changes-requested review is open;
- no Critical item from the AI PR Review comment is unanswered (see below).
Expand Down
25 changes: 24 additions & 1 deletion .github/workflows/push-test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
name: Build, Test & Lint

on: [push, pull_request]
# One run per commit (ako/mxcli#1886). With `[push, pull_request]` every push to
# a PR branch ran this workflow twice, ~102 runner-minutes each, and the pair kept
# the account at its 20-concurrent-job limit for most of a busy window. The
# pull_request run is the one kept: it tests the merge result, not the branch.
#
# A branch without a PR no longer gets CI on push; start one by hand with
# workflow_dispatch (Actions tab, or `gh workflow run push-test.yml --ref <branch>`).
on:
push:
branches: [main]
pull_request:
workflow_dispatch:

# A new push to a PR cancels that PR's run still in progress: the superseded
# commit's result is never read, and it held its runners until it finished.
#
# Only PR runs share a group. Everything else (main, workflow_dispatch) gets a
# group of its own via run_id, because a shared group cancels even with
# cancel-in-progress false: GitHub keeps at most ONE pending run per group, so a
# third push to main would silently drop the queued second one, and main's
# commits must each be tested.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read
Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- **SEC012 reports mxbuild's CE2729 before a build** (ako/mxcli#936) — a page a user role can reach that shows an attribute, or lists an entity, the role may not read. Reachability is per user role and measured against mxbuild: menu items the role may view, its home page (checked even when it may not view it, but not walked further), show-page actions, snippet calls, and microflows and nanoflows it may execute with their sub-microflows. On JTSBootLogboek with an access-less role it reports the same 27 attribute and entity errors mxbuild does, and nothing on FactoryManagement or JTSBootLogboek as they are, where mxbuild reports none. Association reads, System entities and URL-only pages are not covered yet.
- **SEC011: a page data source whose XPath crosses an association the page's roles cannot read** (ako/mxcli#945, #1337, mendixlabs/mxcli#1471) — at Production security such a page opens to an error dialog (`No access rights for Shop$Account/Shop.Order_Account`) while mxbuild, `check` and `lint` were all clean. The rule checks each association step from the entity it starts on, per user role that can open the page, and reports it as an error with the member to grant. Measured on 11.15: an association is checked only where it is a member (its FROM entity, and its TO entity when the owner is Both), and an XPath inside an entity access rule is not checked at all, so neither is reported.
- **MPR012 covers every widget measured as CE0582 under the React client** (ako/mxcli#564) — besides the static and dynamic image it now reports the classic drop-down, the legacy data grid and the reference selector a project already holds, each with its replacement (`combobox`, Data grid 2). The set is measured on 11.16 with mxbuild, controls included: the navigation tree, menu bar, file manager, scroll container, image uploader and the common input widgets build clean and stay silent. The rule is renamed `ReactUnsupportedWidget`; its ID is unchanged.
- **DROP over `--mcp` uses Studio Pro 11.16's `ped_delete_document`** (ako/mxcli#1851) — DROP of a constant, enumeration, microflow, nanoflow, page, workflow or business-event service calls the native delete tool. A server without it (Studio Pro before 11.16) gets an error naming the version it needs.
Expand All @@ -24,7 +25,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **A custom-content data grid column with an empty caption no longer shows its MDL name as the header** (ako/mxcli#535) — through the generic `pluggablewidget 'com.mendix.widget.web.datagrid.Datagrid'` form, `column colTriage (Caption: '') { … }` (or no caption) wrote `colTriage` as the header text, which end users saw; Mendix stores no name for a column. It now writes an empty header, as the keyword `datagrid` form already did; it builds clean (11.15.0) and Data grid 2 renders it blank. A page that already holds the name keeps it until the page is dropped and created again: `create or replace` does not yet clear a caption to `''`.
- **The catalog's XPath rows name their widget** (ako/mxcli#1879) — `xpath_expressions.ComponentId` for a page or snippet data source was a Go byte dump (`{0 [57 221 …]}`) and nothing tied the row to its widget. It is now a GUID, and the new `WidgetId` / `WidgetName` columns (`widget_id` / `widget_name` in `xpath_expressions()`) name the widget by the id `widgets` gives it. The catalog schema moves to version 32, so a cached catalog is rebuilt once.
- **`create or replace page` can clear a data grid column caption** (ako/mxcli#1877) — `Caption: ''` over a stored caption reported "Unchanged page" and kept it, because the translation carry added the stored text back to an empty header written with no translation. The header is now written as one empty translation, which the carry leaves alone; an empty translation where the stored text has none is dropped, so a Studio Pro empty caption still rewrites as unchanged. This also repairs pages that still carry a leaked column name (#535).
- **A custom-content data grid column with an empty caption no longer shows its MDL name as the header** (ako/mxcli#535) — through the generic `pluggablewidget 'com.mendix.widget.web.datagrid.Datagrid'` form, `column colTriage (Caption: '') { … }` (or no caption) wrote `colTriage` as the header text, which end users saw; Mendix stores no name for a column. It now writes an empty header, as the keyword `datagrid` form already did; it builds clean (11.15.0) and Data grid 2 renders it blank.
- **`--mcp http://localhost:<port>/mcp` reaches Studio Pro on a Windows machine with Docker Desktop** (mendixlabs/mxcli#1684, ako/mxcli#1833) — without `--mcp-dial`, mxcli dialled `host.docker.internal` whenever that name resolved, taking it to mean "inside a container". Docker Desktop for Windows puts the name in the hosts file, pointing at the LAN address, and Studio Pro listens on loopback only, so the connection timed out (`dial tcp <LAN IP>:7782: i/o timeout`) on the very machine Studio Pro runs on. mxcli now dials the URL's own host first and tries `host.docker.internal` only when that fails, so the same command still works from a devcontainer. `--mcp-dial` is unchanged: when given, it is the only address tried.
- **`--mcp-trace` and `--mcp-verbose` list the PED calls** (mendixlabs/mxcli#1684, ako/mxcli#1833) — they printed each MDL statement but never a tool call, on `exec`, `-c`, the REPL and `mxcli mcp capabilities` alike. A run that sent every write to Studio Pro read as one that sent none, which is how a report concluded that `exec --mcp` wrote the `.mpr` instead. The backend the CLI builds for `--mcp` now gets the tracer too, so each statement is followed by its `→ ped_…` calls.
- **A PED tool whose handler throws is reported as a failure** — Studio Pro answers `Error executing tool handler: …` with `isError:false` (measured on 11.16.0), which a caller trusting the flag read as success; the MCP client now classifies that text as an error.
Expand All @@ -37,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- **A source catalog build describes a project widget the same way every run** (#1799) — `refresh catalog full source` sometimes wrote a pluggable widget in the generic `pluggablewidget '<id>'` form instead of its own keyword (`vegachart …`), and since #1802 an incremental build kept that row until a `force`. Each parallel describe loaded its own widget registry, and with one definition that never counts as current (hand-made, or its `.mpk` removed), each load rewrote every `.def.json` while the other describes read them. The describes now share the build's registry, and a definition is replaced by rename, so no reader sees a half-written file.

- **A rewrite gives a stored list the marker Studio Pro writes** (ako/mxcli#1640) — a list an older mxcli stored with the wrong typed-array marker kept it through every later rewrite, because a rewrite copies much of a stored document as bytes (an unchanged pluggable widget's Type and Object, an existing element's unchanged properties). A Data grid 2's `SortBar.SortItems` stayed 3 where Studio Pro stores 2, and `drop page` plus the same MDL was the only way out. Every write now applies the measured owner/key markers to the whole unit, and lists without a measured marker are left as they are. A populated nanoflow parameter list (on a call nanoflow action or a nanoflow data source) is now written with 2, as Studio Pro does. mx check never reported either; the effect was a document that differed from Studio Pro's at the value level.
- **MPR v2: a unit written twice in one `modelsdk` write transaction commits the second write** — writing a unit A → B → A in one `WriteTransaction` committed B. The second write was compared with the committed file rather than with the transaction's own first write, which sits in a temp file until commit, so it matched A and was skipped as unchanged. It is now compared with the pending write. No mxcli command or `modelsdk` call writes a unit twice in one transaction, so this was reachable only by using `WriteTransaction` directly.

## [0.29.0] - 2026-10-30

Expand Down
Loading
Loading