Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
942e776
feat(lint): CONV021 warns on a create/change directly followed by its…
claude Oct 30, 2026
b3b367f
Merge pull request #1823 from ako/claude/loving-clarke-qun7w5
ako Oct 30, 2026
40b2aac
fix(pages): write a hidden expression property's default into Express…
claude Oct 31, 2026
694f27d
feat(pages): spell an optional page parameter, and stop describe inve…
claude Oct 31, 2026
b5fe92a
Merge pull request #1824 from ako/claude/cool-keller-jtch0r
ako Oct 31, 2026
24d51eb
fix(catalog): describe a project widget the same way on every source …
ako Oct 31, 2026
08d8749
Merge remote-tracking branch 'origin/main' into fix/1799-widget-rende…
ako Oct 31, 2026
865b1e6
fix(catalog): record the references a message definition makes (#1781)
ako Oct 31, 2026
d03590e
fix(init,lint): install the bundled lint rules for every tool, and sa…
ako Oct 31, 2026
664e536
Merge pull request #1825 from ako/fix/1799-widget-render-determinism
ako Oct 31, 2026
1fad071
Merge pull request #1826 from ako/fix/1781-message-definition-refs
ako Oct 31, 2026
59488dd
Merge remote-tracking branch 'origin/main' into fix/1292-lint-rules-e…
ako Oct 31, 2026
4ee6748
test(rest client): expect the query-parameter version gate below 11.0
claude Oct 31, 2026
daa2fa9
docs(findings): record the REST query-parameter test pinned to the wr…
claude Oct 31, 2026
233698d
Merge pull request #1830 from ako/claude/mxcli-nightly-build-failure-…
ako Oct 31, 2026
c69b600
feat(mpr): say when the first command builds the unit index (#1806)
ako Oct 31, 2026
ae6ea1a
Merge pull request #1827 from ako/fix/1292-lint-rules-every-tool
ako Oct 31, 2026
4e52d44
Merge pull request #1831 from ako/fix/1806-unit-index-notice
ako Oct 31, 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
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 --exclude System --exclude Administration
| CONV013 | quality | ErrorHandlingOnCalls - External calls (REST/WS/Java) need custom error handling |
| CONV014 | quality | NoContinueErrorHandling - Don't silently swallow errors with Continue |
| CONV020 | quality | CreatedObjectNeverPersisted - A microflow creates a persistent object without commit and never commits, returns or passes it on, so it is discarded |
| CONV021 | quality | CreateChangeThenCommit - A create/change without commit directly followed by a Commit of the same object; set Commit on the create/change instead |

## Bundled Starlark Rules

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "cmd/mxcli", "date": "2026-10-31", "symptom": "`mxcli init --tool cursor` (and aider, continue, copilot, vibe, windsurf) installed no Starlark lint rules, so `mxcli lint` ran the built-in Go rules alone; a project copy without .claude/ did the same. No notice, exit 0: 210 findings from 17 rules read as cleaner than 1,984 from 49.", "cause": "init wrote .claude/lint-rules/ only inside the claude and opencode branches, although the lint config next to it was already written outside the per-tool branches for exactly this reason; projectLintRules treated a missing rules directory as an empty one without saying so.", "file": "cmd/mxcli/init.go", "insight": "Anything `mxcli lint` reads is tool-independent and belongs outside the per-tool branches of init; test it by iterating SupportedTools, not by naming the tools that happen to work. A smaller rule set is indistinguishable from a cleaner project in every output, so any path that runs fewer rules than it could must say so on stderr.", "issue": "ako/mxcli#1292"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-10-31","symptom":"The nightly goes red on the Mendix 10.24.24 leg only, in `TestRoundtripRestClient_WithQueryParams`: `want the QueryParameters refusal, got: query: ( … ) on a REST operation requires Mendix 11.0.0+ (project is 10.24.24.119349)`. Every 11.x leg, push CI and the unit tests are green","cause":"Not a product defect: the statement is still refused below 11.0, by a different layer. 9aa1df3e (ako/mxcli#1811 sweep) added a check-time gate, `rest_client_query_params` in documentTypeGates, which refuses the statement before the write is reached, while the test pinned the write path's refusal text (`Rest$RestOperation.QueryParameters (Mendix 11.0.0+)`). Only a pre-11.0 project takes that branch (`restClientHasMendix11Shapes`), and per-PR CI runs one newer version, so the PR that added the gate never ran it","file":"`mdl/executor/roundtrip_restclient_test.go` (`TestRoundtripRestClient_WithQueryParams`); the gate is `mdl/executor/version_gates_documents.go`","insight":"A test that pins a write-time refusal is a bet that nothing refuses earlier, and moving a refusal to check time is exactly the direction CLAUDE.md asks for, so every check-time gate added should grep the integration tests for the write path's message (`cannot store <Type>.<Property>` / `<Type>.<Property> (Mendix X+)`) and move those assertions to the gate's `<feature> requires Mendix X+`. The 9.24 leg cannot show this one: there `create rest client` itself is gated (10.1), which fires first and was already among that leg's 69 known failures, the same count before and after the gate landed. Reproduce in about 10s: `mxcli setup mxbuild --version 10.24.24.119349`, then `MX_BINARY=~/.mxcli/mxbuild/10.24.24.119349/modeler/mx go test -tags integration -run 'TestRoundtripRestClient_WithQueryParams$' ./mdl/executor/`. Controls: the old assertion fails at line 151 with the nightly's exact message; the new one passes, and fails with the write path's `cannot store Rest$RestOperation.QueryParameters (Mendix 11.0.0+)` when the gate's applies is stubbed to false","refs":["mendixlabs/mxcli nightly run 41183093941","ako/mxcli nightly run 41182765181","ako/mxcli#1811"],"test":"mdl/executor/roundtrip_restclient_test.go"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-10-31", "symptom": "A page parameter Studio Pro marks not Required (Forms$PageParameter.IsRequired = false, Mendix 11.5+), opened by a menu item with no argument (mx check 0 errors): describe page printed `Params: ( $Sale: MyFirstModule.Sale )`, describe navigation printed `show page MyFirstModule.Sale_View(Sale = $currentObject)`, and executing that output was refused with MDL-PAGEARG01 — the navigation profile could not be changed from MDL (Mendix 11.12.5)", "cause": "MDL had no spelling for the flag (the builder hard-coded IsRequired true), describe page did not print it, and pageActionParameters synthesised `= $currentObject` for every target parameter when no mapping is stored — right for a required one inside a data widget, wrong for an optional one on a context-less menu item. The reader could not be trusted either: gen's IsRequired() reads an absent key as false, so every pre-11.5 parameter would have looked optional", "file": "mdl/grammar/domains/MDLPage.g4 (pageParameter OPTIONAL?), MDLLexer.g4 + keyword rule; mdl/visitor/visitor_page.go; mdl/executor/cmd_pages_builder_v3.go (IsRequired: !Optional; snippet refusal); cmd_alter_page.go + mdl/backend/modelsdk/page_mutator_write.go (alter add parameters); version_gates_documents.go + sdk/versions/mendix-11.yaml (page_parameter_optional 11.5.0); validate_snippet_parameters.go (MDL087); mdl/backend/modelsdk/page.go (pageParameterIsRequired reads the raw key); cmd_pages_describe.go / cmd_pages_describe_output.go", "fix": "`Params: ( $P: Type optional )`, written as IsRequired false through CREATE and ALTER … ADD PARAMETERS, gated to 11.5+ (MDL-VERSION01) and refused on a snippet parameter (MDL087); describe page prints `optional`; the implicit `= $currentObject` mapping is only for required parameters; the reader treats an absent IsRequired as required", "insight": "Before trusting a gen bool accessor for a property newer than the oldest supported version, check what it returns for an absent key: Primitive[bool] gives false, which here would have flipped every pre-11.5 parameter to optional — read element.Raw() and treat absent as the platform's old behaviour; the control (revert to gp.IsRequired()) fails the no-stored-flag test. The real check settles both directions on the 11.6.6 fixture: menu item with no argument → 0 errors with `optional`, CE1571 without. No fixture page (58 parameters in TestApp + PedApp) is optional, so the Studio Pro shape was seeded. Left out: a check for the required case (`show page P` with no argument on a context-less widget is CE1571 only at mx check), and parameter DefaultValue (11.5), which has no spelling and no fixture evidence", "refs": ["mendixlabs/mxcli#1640", "ako/mxcli#1649"], "ce": ["CE1571"], "rules": ["MDL-PAGEARG01", "MDL-VERSION01", "MDL087"], "test": "TestIssue1649_OptionalPageParameterIsWrittenAndDescribed, TestIssue1649_DescribeNavigationOpensAnOptionalParameterPageWithNoArgument, TestIssue1649_AParameterWithNoStoredFlagIsRequired, TestIssue1649_OptionalIsRefusedWhereItCannotBeStored, TestIssue1649_AlterAddsAnOptionalParameter (mdl/executor/issue1649_optional_page_parameter_test.go); mdl-examples/bug-tests/1640-optional-page-parameter.mdl"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-10-31", "symptom": "Every combo box mxcli wrote stored customEditabilityExpression with PrimitiveValue \"false\" as well as Expression \"false\"; Studio Pro stores PrimitiveValue \"\" (ako/TestApp Rules.RuleAction_NewEdit). mx check accepted both, so mxcli's widgets differed from Studio Pro's and a page saved by one never re-ran Unchanged against the other", "cause": "A property the widget's editorConfig hides is reset to its declared default (hiddenUnnamedProperties / unmappedHiddenResets in PluggableWidgetEngine.Build), and both reset sites called builder.SetPrimitive whatever the property's value type. The template and the .mpk augment were already right (PrimitiveValue \"\"); the explicit `key: value` path routes by ValueType and was right too", "file": "mdl/executor/widget_engine.go (resetToDeclaredDefault, used by the two hidden-property reset sites)", "fix": "Write the declared default into the slot its value type keeps it in: SetExpression for an Expression property (PrimitiveValue left as the template's \"\"), SetPrimitive otherwise", "insight": "Measure the slots by the property's declared ValueType, read from the widget's own Type, not by a $Type pattern: over 221 TestApp + PedApp pages, snippets and layouts, 939 Expression-typed WidgetValues fill PrimitiveValue 0 times, and no other type fills Expression — strictly exclusive, which is what made the fix safe against CE0463. Bisect the pipeline before reading it: GetTemplateFullBSON already produced PrimitiveValue \"\", which put the defect after template load and pointed straight at the two SetPrimitive reset calls. mx check passes with and without the fix (control), so the test has to assert the stored slot, not mx", "refs": ["ako/mxcli#548"], "test": "TestIssue548_ExpressionPropertyIsNotWrittenAsAPrimitive (mdl/executor/issue548_expression_slot_test.go); mdl-examples/bug-tests/548-pluggable-expression-slot.mdl"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-10-31", "symptom": "`refresh catalog full source force` on an unchanged project described a project pluggable widget as `pluggablewidget '<id>' name` on some runs and by its keyword on others; once cached, the incremental build kept the wrong row.", "cause": "captureDescribeParallel built a fresh ExecContext per describe, so each of up to eight workers loaded its own widget registry; one unstamped .def.json (hand-made, or its .mpk gone) made every load call RefreshWidgetDefinitions(force), which truncated and rewrote every definition (os.WriteFile) while the other workers read them. A torn read aborted LoadUserDefinitions, its error discarded, so that describe's registry lacked the widget and pluggableWidgetHeader fell back as designed.", "file": "mdl/executor/cmd_catalog.go", "insight": "The tell was the SHAPE: two of twenty widgets, all on one page — a per-describe input going bad, not a broken lookup (that hits all twenty). It did not reproduce on a clean project; it needs a definition that stays stale forever. `strace -e trace=openat` settled it in one run: 16 writes and 30 reads of one .def.json during a single build. A per-goroutine clone of a context silently re-runs every lazy loader it holds — copy the loaded state in, and load it before the fan-out.", "issue": "ako/mxcli#1799"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/catalog", "date": "2026-10-31", "symptom": "`show references to` an entity, attribute or association never listed a message definition that exposes it (EvoraICX: EvoraCore.MD_ResolutionStep exposes InvestigationTask and its 15 attributes, 0 edges), so renaming or deleting one looked safe.", "cause": "memberRefSourceTypes, the closed list of unit types the raw member-ref walk visits, had the two mapping types but neither message-definition type, although a message definition stores its Entity/Attribute/Association by qualified name exactly as a mapping does.", "file": "mdl/catalog/builder_member_refs.go", "insight": "When a document type stores members by qualified name, the raw walk needs only a row in memberRefSourceTypes (plus the entity case alongside the mappings); no per-type extractor. List the document types the gen package registers RefByName props for (codec.DefaultRefRegistry) and diff against memberRefSourceTypes to find the next gap. TestSkillDocumentsRealRefObjectTypes checks only that documented values exist, not that every source type is documented, so the lint-rule doc must be updated by hand.", "issue": "ako/mxcli#1781"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "modelsdk/mpr", "date": "2026-10-31", "symptom": "The first command after upgrading to the unit-index-2 format (or on a fresh clone) opened every unit file to build the index — 641 opens where the previous build opened 383 — with no explanation, a visible stall on Windows where an open costs about a millisecond.", "cause": "By design the index is a pure cache and a cold build reads everything; nothing said it was happening. The new format stores Name and Excluded, which the old unit-types.idx never had, so it cannot be seeded from the old file.", "file": "modelsdk/mpr/reader_units_v2.go", "insight": "A one-time cost a user cannot see reads as a regression; one line on stderr turns it into an expected step. Keep the notice in the library behind a nil-by-default writer (mpr.ColdUnitIndexNotice) that the CLI sets with the banner, so quiet mode and library callers stay silent, and count units before buildUnitCache opens its row query rather than during it.", "issue": "ako/mxcli#1806"}
19 changes: 10 additions & 9 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-353 | Quality Guidelines Reference |
| 354-364 | Assessment Report Template |
| 365-377 | Executive Summary |
| 378-388 | project Metrics |
| 389-401 | Automated Findings |
| 402-421 | Manual Review Findings |
| 422-437 | Recommendations |
| 438-448 | Sources |
| 97-354 | Quality Guidelines Reference |
| 355-365 | Assessment Report Template |
| 366-378 | Executive Summary |
| 379-389 | project Metrics |
| 390-402 | Automated Findings |
| 403-422 | Manual Review Findings |
| 423-438 | Recommendations |
| 439-449 | Sources |

<!-- /skill-index -->
# Assess Mendix Project Quality
Expand Down Expand Up @@ -200,7 +200,7 @@ The Dutch Institute for Vulnerability Disclosure (DIVD) found widespread authori

### D. Maintainability

**Enforced by rules:** QUAL001-QUAL004, CONV009, CONV012, CONV014, CONV020, MPR002, MPR004, MPR006
**Enforced by rules:** QUAL001-QUAL004, CONV009, CONV012, CONV014, CONV020, CONV021, MPR002, MPR004, MPR006

| Guideline | Rule/Check | Threshold |
|-----------|-----------|-----------|
Expand All @@ -212,6 +212,7 @@ The Dutch Institute for Vulnerability Disclosure (DIVD) found widespread authori
| Caption on exclusive splits | CONV012 | All decision points must have captions |
| No "Continue" error handling | CONV014 | Never swallow errors silently |
| No created object silently discarded | CONV020 | A persistent object created without commit is committed, returned or passed on |
| Commit on the create/change, not after it | CONV021 | No Create/Change object activity directly followed by a Commit of the same object |
| ACT_ microflows: thin controllers | CONV010 | Only page activities + submicroflow calls |

**Manual checks:**
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/mendix/write-lint-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -561,13 +561,13 @@ Returned by `permissions()` (all types) or `permissions_for()` (entity-specific)
### reference
| Property | Type | Example |
|----------|------|---------|
| `source_type` | string | The document the edge comes FROM, upper-case: `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"SNIPPET"`, `"ENTITY"`, `"ASSOCIATION"`, `"WORKFLOW"`, `"NAVIGATION"`, `"SCHEDULED_EVENT"`, `"PUBLISHED_REST_OPERATION"`, `"PUBLISHED_REST_SERVICE"`, `"PUBLISHED_ODATA_SERVICE"`, `"CONSUMED_ODATA_SERVICE"`, `"BUSINESS_EVENT_SERVICE"`, `"PROJECT_SETTINGS"`, `"IMPORT_MAPPING"`, `"EXPORT_MAPPING"` |
| `source_type` | string | The document the edge comes FROM, upper-case: `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"SNIPPET"`, `"ENTITY"`, `"ASSOCIATION"`, `"WORKFLOW"`, `"NAVIGATION"`, `"SCHEDULED_EVENT"`, `"PUBLISHED_REST_OPERATION"`, `"PUBLISHED_REST_SERVICE"`, `"PUBLISHED_ODATA_SERVICE"`, `"CONSUMED_ODATA_SERVICE"`, `"BUSINESS_EVENT_SERVICE"`, `"PROJECT_SETTINGS"`, `"IMPORT_MAPPING"`, `"EXPORT_MAPPING"`, `"MESSAGE_DEFINITION_COLLECTION"`, `"MESSAGE_DEFINITION"` |
| `source_id` | string | Source UUID |
| `source_name` | string | `"Sales.ACT_Customer_Create"` |
| `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), `"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 |
| `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, or a message definition exposes it), `"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
Loading
Loading