Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c123a14
fix: lint location repeats the module name (at M.M.NAV_Home)
ako Oct 27, 2026
da9776a
fix: QUAL004 reports a navigation-started microflow as not called fro…
ako Oct 27, 2026
af4c7de
fix: diff-local counts a settings change as modified but prints nothi…
ako Oct 27, 2026
2bd9bbe
fix: grant on a published REST service created in a new folder report…
ako Oct 27, 2026
1efde4f
fix: refs and QUAL004 call a published OData service's read and authe…
ako Oct 27, 2026
eff13c3
fix: mxcli test stops at a parse error when a call has its own on err…
ako Oct 27, 2026
3317b13
fix: diff-local prints no difference for a grant on an entity (ako/mx…
ako Oct 27, 2026
130a404
fix: mxcli test --local fails CE0114 when the app's after-startup mic…
ako Oct 27, 2026
5481398
fix: refuse a widget property the widget does not take, e.g. Caption …
ako Oct 27, 2026
09b2c82
fix: describe and create layout keep a scroll container's LayoutMode …
ako Oct 27, 2026
abb4d4a
fix: diff-local shows no hunk for a grant-only change to a domain mod…
claude Oct 27, 2026
cf7151f
fix: diff-local shows no hunk for an ALTER SETTINGS change (ako/mxcli…
claude Oct 27, 2026
9d7e49b
fix: catalog query strips "Catalog." inside string literals (ako/mxcl…
ako Oct 27, 2026
4fb9018
fix: SHOW VERSION reports MPR v2 for an MPR v1 project (ako/mxcli#1553)
ako Oct 27, 2026
c060d53
fix: diff-local lists changed fields instead of "(no changes)" for un…
claude Oct 27, 2026
7d39177
fix: diff-local fails outside the project's git repo (ako/mxcli#1512)
claude Oct 27, 2026
621a1cb
fix: `call microflow X()` on a widget is a syntax error (ako/mxcli#1516)
ako Oct 27, 2026
cf9d5f0
feat: set the administrator password from MDL to resolve CE0129 (ako/…
claude Oct 27, 2026
577d481
docs(wiki): bug pattern for diff-local's "modified, no hunk" class
claude Oct 27, 2026
50d09b2
test: write the 1602 bug-test's constant in the canonical form
claude Oct 27, 2026
79e6637
style: gofmt the splice handler test
claude Oct 27, 2026
9086639
fix: playwright check waits out --wait on the sign-in form
claude Oct 27, 2026
76ed023
fix: playwright check fails a page on another host than the one it si…
claude Oct 27, 2026
8e9e310
fix: playwright check passes an unknown /p/ URL as the home page (ako…
claude Oct 27, 2026
d1af9dc
docs: playwright check's --wait default and the sign-in form
claude Oct 27, 2026
b83d87b
fix: --ensure-db provisions an empty cluster while the system cluster…
claude Oct 27, 2026
faf94c9
fix: run --local hangs silently resolving Java dependencies behind a …
claude Oct 27, 2026
b68b6e2
fix: run-local.json vanishes under a live run and run status gives no…
claude Oct 27, 2026
dd50a6d
docs(wiki): sync local-loop-silence with the cluster's six new findings
claude Oct 27, 2026
ed62ba8
fix: an OnClick whose value is not an action is not an unknown contai…
ako Oct 27, 2026
a4aa675
Merge pull request #1745 from ako/fix/1512-diff-local-silent-modified
ako Oct 27, 2026
cfd9280
Merge pull request #1746 from ako/fix/1484-qual004-navigation-callers
ako Oct 27, 2026
e4d9580
Merge pull request #1747 from ako/fix/1698-silent-property-drop
ako Oct 27, 2026
1b8bbc7
Merge pull request #1748 from ako/fix/1448-test-runner-generated-flows
ako Oct 27, 2026
9015480
Merge pull request #1749 from ako/fix/1486-rest-grant-folder
ako Oct 27, 2026
e5d7fc1
Merge pull request #1750 from ako/fix/1516-widget-action-empty-call-args
ako Oct 27, 2026
c728103
Merge pull request #1751 from ako/fix/1450-catalog-prefix-in-literals
ako Oct 27, 2026
26237f4
Merge pull request #1752 from ako/fix/1553-show-version-mpr-format
ako Oct 27, 2026
f8865eb
fix: diff-local --ref a...b reads every unit at ".b" and diffs nothing
claude Oct 27, 2026
b715c18
fix: diff-local skips a unit whose file moved to a new ID
claude Oct 27, 2026
994c00b
fix: playwright check reports a page the role may not open as the sig…
claude Oct 27, 2026
0c87e87
Merge origin/main into claude/loving-clarke-qun7w5
claude Oct 27, 2026
24b557f
Revert "style: gofmt the splice handler test"
claude Oct 27, 2026
229d029
Merge pull request #1755 from ako/claude/loving-clarke-qun7w5
ako Oct 27, 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
39 changes: 29 additions & 10 deletions .claude/commands/mendix/diff-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Entity: MyModule.Customer

## How It Works

1. **Finds changed files**: Uses `git diff --name-status` to identify modified `.mxunit` files in the `mprcontents/` folder
1. **Finds changed files**: Uses `git diff --name-status` to identify modified `.mxunit` files in the `mprcontents/` folder. Git runs in the project's own directory, so `-p` may point into a repository from anywhere. A unit file moved to another name is another unit ID, so it is reported as one unit deleted and one added
2. **Reads both versions**: Current version from disk, git version using `git show`
3. **Parses BSON**: Extracts the unit type and content from the binary BSON format
4. **Generates MDL**: Converts both versions to MDL for human-readable comparison
Expand All @@ -89,16 +89,34 @@ Entity: MyModule.Customer

The command generates MDL representations for:

- **Entities**: Shows attributes, constraints, and entity type
- **Microflows**: Shows parameters and return type
- **Nanoflows**: Shows parameters and return type
- **Enumerations**: Shows values and captions
- **Pages**: Shows title and layout reference
- **Snippets**: Shows basic structure
- **Layouts**: Shows basic structure
- **Modules**: Shows module name
- **Domain models**: each entity's attributes, constraints and entity type, its access rules as `grant` statements (as `describe entity` prints them), and the associations
- **Microflows / Nanoflows**: the whole body, as `describe` prints it, `grant execute` included
- **Pages / Snippets / Layouts**: the header properties and the widget tree, as `describe` prints them
- **Enumerations**: values and captions
- **Project settings**: as `describe settings` prints them, with configuration constant values masked
- **Modules**: module name

Other unit types show a generic representation with the type name.
Configuration constant values often hold secrets, so they are never printed. A
changed value still shows, as a masked pair:

```diff
-alter settings constant 'MyModule.ApiKey' value <masked: old value>
+alter settings constant 'MyModule.ApiKey' value <masked: new value>
in configuration 'Default';
```

### Changes the MDL does not show

A unit can change only in properties its rendering leaves out — navigation,
project or module security, a page's allowed roles, an entity index, a
constant's default value. Such a unit is printed as one line, in every format,
naming the properties that changed (never their values):

```
Modified: ModuleSecurity MyModule: changed in properties the MDL rendering does not show: ModuleRoles (1 -> 2 items)
```

Run `describe` on the element before and after to see the change itself.

## Summary Output

Expand All @@ -122,6 +140,7 @@ Summary: 2 new, 3 modified, 1 deleted
- Use `--format side` when comparing large objects with subtle differences
- Use `--ref HEAD~5` to see what changed in the last 5 commits
- Use `--ref feature-branch` to compare against a different branch
- Use `--ref main...feature-branch` (three dots) to review only what a branch changed: the old side is the merge base, so changes made on `main` since do not show
- After restoring a `.mxunit` with `git checkout`/`git restore`, run
`mxcli fix hashes -p app.mpr --repair`: git does not update the `.mpr`'s
ContentsHash index, and `mx check` does not notice the stale entry
Expand Down
80 changes: 31 additions & 49 deletions .claude/lint-rules/orphaned_elements.star
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
# - Work in progress that was forgotten
#
# Checks:
# - Microflows with no callers (except ACT_ which are UI entry points)
# - Pages with no show_page references (except entry pages)
# - Entities with no references at all
# - Microflows, nanoflows and rules nothing references (except entry-point
# prefixes such as ACT_)
# - Pages nothing references (except likely entry pages)
#
# "Referenced" means what `mxcli refs` and CATALOG.GRAPH_DEAD_ASSETS mean: any
# edge in the catalog's reference graph that points at the document.
#
# This rule requires REFRESH CATALOG FULL to populate the refs table.
#
Expand All @@ -34,13 +37,23 @@ ENTRY_POINT_PREFIXES = ["ACT_", "SCH_", "WS_", "REST_", "OData_"]
# Page name patterns that are likely entry points
ENTRY_PAGE_PATTERNS = ["Home", "Login", "Index", "Dashboard"]

# Reference kinds that mean "something causes this microflow to run". These are
# catalog RefKind values (mdl/catalog/builder_references.go); a kind missing here
# turns a live document into a false "not called from anywhere" finding.
MICROFLOW_ENTRY_KINDS = ["call", "schedule", "publish", "datasource", "action", "calculate", "settings", "event"]
# There is deliberately no list of the reference kinds that "count". Every edge
# into a microflow or a page is something that runs or opens it — a call, a
# widget, a calculated attribute, a scheduled event, a published service, an
# event handler, a project setting, the navigation — and the catalog keeps
# adding ways. A hand-kept list of them fell behind: it had the page kinds for a
# home page and a menu item but not the microflow ones, so a microflow started
# only from the navigation was reported with "Remove if unused" while
# `mxcli refs` listed the reference (ako/mxcli#1484). With no list, a new kind
# counts the day the catalog emits it.
#
# TestQUAL004CountsEveryRefKind (mdl/linter) drives every kind the catalog
# declares at a microflow, a nanoflow and a page, and fails on one that does not
# count.

# Reference kinds that mean "something opens this page".
PAGE_ENTRY_KINDS = ["show_page", "home_page", "login_page", "menu_item", "action"]
def is_referenced(qualified_name):
"""True when anything in the catalog's reference graph points at the document."""
return len(refs_to(qualified_name)) > 0

def is_entry_point_microflow(name, prefixes):
"""Check if a microflow name suggests it's a UI/scheduled entry point."""
Expand Down Expand Up @@ -70,34 +83,14 @@ def check():
if is_entry_point_microflow(mf.name, entry_prefixes):
continue

# Get references to this microflow
refs = refs_to(mf.qualified_name)

# Anything that causes the microflow to run counts as a caller, not just
# a literal "call" edge. A microflow reached only through one of the other
# kinds was reported as orphaned with the suggestion "Remove if unused":
#
# schedule a scheduled event runs it (Mendix's cron)
# datasource a page or widget uses it as a data source
# action a widget button calls it
# calculate a calculated attribute computes with it
# settings a project setting names it (after-startup, before-shutdown,
# health-check) — the RUNTIME calls it, and nothing in the
# model does
#
# The banking-app report hit the 'datasource' case: DS_CurrentCustomer and
# DS_MyAccounts are both page data sources and both were flagged. The
# CapTrackV4 report hit 'settings': the project's own AfterStartupMicroflow
# was reported as "not called from anywhere. Remove if unused", and taking
# that advice left a dangling name mx check did not catch either — only the
# runtime refused to start.
has_callers = False
for ref in refs:
if ref.ref_kind in MICROFLOW_ENTRY_KINDS:
has_callers = True
break

if not has_callers:
# a literal "call" edge. Each of these once reached a live microflow
# that this rule then advised deleting: a page data source (DS_ microflows in
# the banking-app report), the project's AfterStartupMicroflow (whose
# removal mx check did not catch — the runtime refused to start), a
# scheduled event, a published REST operation, an entity event handler,
# and a home or menu-item microflow (ako/mxcli#1484).
if not is_referenced(mf.qualified_name):
loc = location(
module=mf.module_name,
document_type=mf.document_noun_title,
Expand All @@ -116,21 +109,10 @@ def check():
if is_entry_point_page(page.name):
continue

# Get references to this page
refs = refs_to(page.qualified_name)

# A page is reachable if anything opens it. Navigation counts: a page that
# is only a home page, a login page or a menu item is reached by the
# client, not by a microflow. Counting only 'show_page' reported those as
# orphaned — masked until now by ENTRY_PAGE_PATTERNS, which happens to
# cover the pages most likely to be navigation targets.
is_shown = False
for ref in refs:
if ref.ref_kind in PAGE_ENTRY_KINDS:
is_shown = True
break

if not is_shown:
# client, not by a microflow.
if not is_referenced(page.qualified_name):
loc = location(
module=page.module_name,
document_type="Page",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"cmd/mxcli","date":"2026-10-27","symptom":"ako/mxcli#866: \"In a container restarted with the system cluster down, the `mxcli init` SessionStart hook runs `--ensure-db`, provisions a fresh **empty** cluster, and both apps boot healthy against it. Every captured row (7,966 laps here) is absent, and no error says so. An empty database is indistinguishable from a new one.\"","cause":"`startLocalPostgres` fell back to the #823 user-owned initdb cluster whenever no service manager made the port ready, without asking whether a system cluster for that port existed; once #984 made that fallback actually work on Debian/Ubuntu, 'PostgreSQL is down' became 'PostgreSQL is empty'. Separately, the bootstrap hook's banner named the LAST log line containing 'error', which behind a service-manager diagnostic is pg_ctlcluster's own 'Error:' line, not mxcli's","file":"`cmd/mxcli/docker/ensuredb.go` (`findSystemClusters`, `conflictingSystemCluster`, `systemClusterDownError`, `MXCLI_ENSURE_DB_NEW_CLUSTER`, the NEW-EMPTY warning in `startUserCluster`); `cmd/mxcli/init_hook.go` (banner takes the first `^Error:` line)","insight":"The fallback's success was the defect: an empty database passes every check above it (role/db creation, boot, health), so the only place the difference is knowable is the moment before initdb — decide there, and announce every initdb loudly. Detect by data directory, not by service: Debian's /etc/postgresql/<v>/<n>/postgresql.conf is world-readable and carries the port, while the data dir is 0700 postgres, so a non-root stat of PG_VERSION returns EACCES — evidence OF a cluster; a probe that counts only err==nil misses exactly the devcontainer user. Match on port so an unrelated cluster never blocks, and treat an existing user-owned cluster as a restart (note the stopped system one, do not refuse). Measured on Ubuntu/PG16 with 16/main down, via a harness calling EnsureDatabase as a non-root user without sudo (privileges dropped in-process, since su was unavailable): fixed -> refusal naming /var/lib/postgresql/16/main; guard stubbed -> 'Database ready' on a new empty ~/.mxcli/postgres cluster, err nil. Trap that cost a cleanup: the first control binary predated the setuid step and ran as ROOT, whose `service postgresql start` succeeded and provisioned into the real cluster — print the uid in any privilege-sensitive harness before believing it. Hermeticity: the stub-PATH test helper must pin the probe root too; dev hosts (this one) have a stopped cluster on 5432","refs":["ako/mxcli#866","#823","mendixlabs/mxcli#984"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "cmd/mxcli", "date": "2026-10-27", "symptom": "`mxcli test --local` fails the build with `[CE0114] A microflow that does not apply entity access can only call microflows that also do not apply entity access. — at MxTest / Microflow 'RegisterEndpoint' / Call microflow activity '<your ASU>'` whenever the project's after-startup microflow applies entity access; no test runs, and the error says it is \"in the project, not in this run's tests\"", "cause": "`GenerateEndpointMDL` wrote `MxTest.RegisterEndpoint` without `@applyentityaccess`, so it did not apply entity access, and since the 2026-08 chaining fix it calls the project's after-startup microflow. `buildFailureHint` called every error not in a `Test_*` flow \"in the project\", including ones in the two documents the run itself generates", "file": "`cmd/mxcli/testrunner/endpoint.go` (`GenerateEndpointMDL`), `cmd/mxcli/testrunner/build_attribution.go` (`buildFailureHint`, `splitRunnerDocuments`)", "insight": "Measure the rule's DIRECTION before choosing between mirroring the callee and a constant: with mx check on 11.13.0 (security Off and Production), non-applying -> applying is CE0114 while applying -> non-applying and applying -> applying are 0 errors (MDL-SEC22's table says the same for 11.14.0). So `@applyentityaccess` on the generated caller is compatible with every startup microflow, and needs no scrape of the user's flow — which the mirror fix would have added beside the DESCRIBE SETTINGS scrape that already broke once. The setting is neutral at runtime: it governs the caller's own activities only (a Java action ignores it), each callee runs with its own. Prove the chain still runs with a marker the test CONSUMES (`@cleanup none` + delete): a plain count passed on an earlier boot's marker under --skip-app-startup, i.e. the control could not fail. Generated documents with fixed names (RegisterEndpoint, RegisterTestEndpoint) are attributable exactly like the Test_* flows; say so and offer --skip-app-startup as the escape hatch", "refs": ["ako/mxcli#1654", "mendixlabs/mxcli#1645", "mendixlabs/mxcli#1500"], "ce": ["CE0114"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "cmd/mxcli", "date": "2026-10-27", "symptom": "`mxcli test` stops at injection with `Parse error: line 8:75 mismatched input 'ON' expecting ';'` (and a misleading `'Error' is a keyword` hint) when a test body's call has its own `on error rollback` / `on error continue` / custom handler — no test runs", "cause": "`attachOnError` appended the runner's `ON ERROR BEGIN … END ERROR` to every line `startsErrorRaisingCall` matched, unconditionally, so a call with its own clause got a second one. The line-based join also broke on a custom handler (its body's first `;` ended the 'statement') and on a trailing `-- comment`. Both runners had their own copy of the join", "file": "`cmd/mxcli/testrunner/generator_endpoint.go` (`attachOnError`, now shared by both runners), `mdl/visitor/flow_statements.go` (`FlowStatements`), `cmd/mxcli/testrunner/results.go` (`ParseLogResults`)", "insight": "Find statements by PARSING the body (in the generator's own wrapper and language version) and read each call's own onErrorClause from the tree: a regex cannot tell a clause on the next line from `on error` in a string, a comment or a nested handler. Decide per clause from what Mendix stores: `on error rollback` is the default spelled out (same ErrorHandlingType as no clause), so it gets the runner's handler — keeping it made the same exception an ERROR, failed a matching @throws, and aborted the legacy suite; continue/custom are kept verbatim. Keeping a custom handler opened a FALSE GREEN only a runtime run showed: a handler that ends normally rejoins PAST every statement reading the failed call's output (the flow builder's skip-var rule), i.e. past the @expect checks, and the test returned PASS with nothing evaluated. Fixed fail-closed: each assertion counts itself in both branches and a guard after the last fails a short count — an assertion on what the handler set still passes. The legacy log parser had the twin: a RUN line with no verdict read as PASS (StatusPass is the zero value); a re-raising handler did exactly that. Measured verdict table on 11.13.0 in mdl-examples/bug-tests/test-runner-1448-own-error-handler.test.mdl", "refs": ["ako/mxcli#1448", "mendixlabs/mxcli#1550"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"cmd/mxcli","date":"2026-10-27","symptom":"`mxcli playwright check /p/secret /p/items -p app.mpr --role User`, where User may not open /p/secret, printed `url=/login.html?messageCode=403#/secret LOGIN PAGE` and `FAIL the sign-in form is showing, not the page (pass --user/--password or --role)` for a user it had just signed in; every later run printed `login demo_user` again while the runtime's session count rose 1, 2, 3, 4, and /p/secret checked alone had its session signed out after every run","cause":"LoginForm (#usernameInput present) was the whole sign-in test, and the client puts a refused page on the same form: the runtime answers POST /xas/ get_session_data (referrer /secret) with 403 and the client goes to /login.html?messageCode=403#/secret. NeedsLogin() read the refusal as a forgotten session, so the stale-session retry signed in beside the live session (a new one per run), and the no-page-past-the-form rule signed a valid session out","file":"cmd/mxcli/docker/pagecheck_assert.go (AccessDenied, NeedsLogin, EvaluatePage), cmd/mxcli/docker/pagecheck.go (ACCESS DENIED in formatPageVerdict, wrongPage), cmd/mxcli/docker/pagecheck_check.go (credentials.roles/signedInAs; checkOrigin sets SignedInAs)","insight":"Record the network before choosing a signal: a 30-line Playwright script logging navigations and /xas/ statuses settled it in one run per case. Signed in and refused: 403, then /login.html?messageCode=403#/<page>. Anonymous, and with a session the runtime forgot: 401, then /login.html#/<page>, guest access on as well (11.15) — the runtime checks authentication before page access, so a 403 never means a stale session. The DOM cannot tell them apart: login.js reads the code as messageCode.split('=')[1], which is undefined, so #loginMessage stays empty and hidden on both and its http403 text (translated anyway) never shows; title, heading and text=34 are identical. The one predicate drove all three symptoms — the wrong advice, the per-run session leak and the needless sign-out — because the retry and sign-out decisions hang on 'needs a login'; check what else reads a predicate before rewording only its message. Count sessions with M2EE runtime_statistics rather than trusting the verdict. Aside: without401 matched the chunk hash 85ce401445201 in the 11.15 client's [Startup] error, which is why 11.15 printed one FAIL line fewer than 11.13 — substring-matching '401' is luckier than it looks","refs":["mdl-examples/bug-tests/pagecheck-403-role-denied.mdl","cmd/mxcli/docker/pagecheck_denied_test.go","ako/mxcli#1216"]}
Loading
Loading