Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f1787d2
fix(security): echo the rule a GRANT wrote, not a shared one
claude Oct 9, 2026
ea92fc0
fix(check): no MDL-WIDGET07 for an explicit pluggable widget id witho…
claude Oct 9, 2026
4b1cfde
fix(navigation): report Unchanged when a navigation rewrite was elided
claude Oct 9, 2026
a1f0463
fix(pages): carry layout-grid row/column appearance and alignment thr…
claude Oct 9, 2026
5062de0
fix(deprecation): MDL-DEPR081 names the $currentObject form it means
claude Oct 9, 2026
f116cb8
fix(visitor): hint the `if exists` order, and no false missing-`;` af…
claude Oct 9, 2026
aa86b12
test(roundtrip): strike two snippets the layout-grid appearance fix r…
claude Oct 9, 2026
58565b5
fix(visitor): hint the quoted last segment for a hyphenated icon name
claude Oct 9, 2026
edb7ef1
feat(pages): write a page variable's default as a bare expression
claude Oct 8, 2026
5debe65
test: write page variable defaults bare in the R2 header-map fixtures
claude Oct 8, 2026
3fd9c07
docs: write the ALTER PAGE add-variables example's default bare
claude Oct 9, 2026
dc4af4b
feat(lint): MPR007 flags a user role that cannot open its home page
claude Oct 9, 2026
ae8c4ba
chore(devcontainer): build with go.mod's Go toolchain (1.27.2)
ako Oct 9, 2026
b056182
Merge pull request #1086 from ako/claude/nice-einstein-a74lz4
ako Oct 9, 2026
6b3f0b1
Merge pull request #1083 from ako/claude/peaceful-clarke-0emnau
ako Oct 9, 2026
a541fc1
Merge pull request #1085 from ako/chore/devcontainer-go-1.27
ako Oct 9, 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
2 changes: 1 addition & 1 deletion .claude/commands/mendix/lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ mxcli lint -p app.mpr --exclude System --exclude Administration
| MPR004 | quality | ValidationFeedback - Validation feedback with empty message |
| MPR005 | quality | ImageSource - IMAGE widgets with no source configured |
| MPR006 | quality | EmptyContainer - Empty layout containers |
| MPR007 | security | PageNavigationSecurity - Navigation pages need allowed roles (CE0557) |
| MPR007 | security | PageNavigationSecurity - Navigation pages need allowed roles (CE0557); every user role must be able to open its home page (else CE2729 per widget) |
| MPR012 | correctness | LegacyImageWidget - staticimage/dynamicimage are unsupported by the React client (CE0582) |
| SEC001 | security | NoEntityAccessRules - Persistent entities need access rules |
| SEC002 | security | WeakPasswordPolicy - Password minimum length should be 8+ |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-10-09", "symptom": "`mxcli check` with no project warns MDL-WIDGET07 \"property `p` is not recognized and will be silently dropped on write\" for every property of a project's own pluggable widget (`pluggablewidget 'ledger.widget.web.vegachart.VegaChart' chartSpark (spec: …, chartData: …)` drew 6), yet with `-p` it is clean and exec writes them all. 58 false warnings across the 41 mxcli-ledger scripts", "cause": "With no project `LoadWidgetRegistry(\"\")` holds only embedded definitions, so the explicit id resolves to nothing (def == nil); TypeIsGeneric is only set for a bare-identifier type, so the `pluggablewidget '<id>'` form fell through to the BUILT-IN static allow-list (`validateStaticWidgetUnknownProps`). MDL-WIDGET25 already returned early for exactly this case", "file": "`mdl/executor/validate_widgets.go` (WIDGET07 gate), helper `explicitWidgetID` in `mdl/executor/validate_widget_kind.go`", "insight": "Three ways to name a widget (keyword, bare generic identifier, explicit id string) and the 'is this a built-in?' gate only excluded two. #1036 fixed the generic-identifier leg; the explicit-id leg had the same fall-through. Every rule that branches on 'no definition found' must say which of the three forms it means — and the no-project case is the one CI exercises, so it is where the gap shows. CONTROL: built-in `container c (bogus: 1)` still raises MDL-WIDGET07", "refs": ["#1036"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"date": "2026-10-09", "area": "mdl/executor", "symptom": "Re-running `create or modify navigation Responsive ...` (chipcov6 04-security-navigation.mdl) printed \"Navigation profile 'Responsive' updated.\" on every run although canon.Reconcile elided the write and every .mxunit md5 was identical; every other statement in the script already reported Unchanged, so the navigation line was the only thing stopping the rerun output from serving as the idempotency gate.", "cause": "execAlterNavigation printed its 'updated' sentence with fmt.Fprintf after UpdateNavigationProfile returned, instead of going through ctx.reportWrite's write-stats evidence (offered vs written) - the #890 sweep converted security and settings but missed navigation. The kept-menu-action note was likewise printed for a rewrite that never happened.", "file": "mdl/executor/cmd_navigation.go", "fix": "The update branch reports through ctx.reportWrite(\"navigation profile '<name>'\", ...), which prints Unchanged (through the run tally) when the write was elided; reportKeptMenuActions now runs only when the write landed (the created-profile branch is untouched: AddNavigationProfile always writes).", "test": "mdl/executor/navigation_report_mutation_test.go TestCreateOrModifyNavigation_ReportsWhatHappened (countingBackend: written=1 reports 'updated' + kept note = control; written=0 reports 'Unchanged navigation profile' and no kept note). Revert check: the elided case fails with \"Navigation profile 'Responsive' updated.\". E2E on a chipcov6 copy: base binary rerun printed 'updated' + 22 in sync, fixed binary 23 in sync; a renamed menu item prints 'updated' then 'Unchanged' on rerun.", "insight": "A statement-specific sentence printed after a backend write is the #890 class again; grep the executor for fmt.Fprintf lines containing 'updated'/'set'/'added' that follow a ctx.Backend.Update* call rather than auditing statement by statement."}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-10-09","symptom":"`grant write (Country) on entity FieldService.Customer to FieldService.Coordinator` prints `Result: read *` — the rights of the shared FabUser/Coordinator/Engineer rule — while the grant actually wrote a separate rule for Coordinator alone (`show access` rule 3: read, write Country)","cause":"`formatAccessRuleResult` echoed the first rule naming ANY of the granted roles with the same XPath, but `AddEntityAccessRule` upserts by the EXACT role set plus XPath (`sameStringSet` in the backend), so whenever a role also sits in a broader rule the echo reported that rule instead","file":"`mdl/executor/cmd_entities_access.go` (`formatAccessRuleResult`, `sameRoleSet`)","insight":"**A post-write echo must select by the key the write used, not a looser one.** #936 already made the XPath part of the echo key; the role-set half stayed an any-overlap match, so the report and the write disagreed exactly when rules are additive — the case #936 made common. REVOKE keeps the any-overlap match deliberately (it narrows every rule a role appears in). Guard `TestGrantEntityAccess_ResultDescribesExactRoleSetRule` (control: a grant to the shared rule's exact role set still echoes it); stubbing the role-set check makes it print `Result: read *` again. Measured on chipcov6 (11.15.0): old binary `Result: read *`, fixed `Result: read (Country), write (Country)`","refs":["mendixlabs/mxcli#936"],"ce":[]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-10-09", "symptom": "`describe page` -> `exec` of a Studio Pro page loses its layout-grid row/column appearance: ledger MyFirstModule.Home_Web went from 46 Forms$DesignPropertyValue entries to 33 ('Flex container' = 'Vertical (column)' x7 on LayoutGridColumn, 'Column gap' = 'Large' x3 and 'Cards style' toggle x3 on LayoutGridRow), and LayoutGridRow.VerticalAlignment Center x6 came back None. check, exec and mx check all green; check even warned MDL-WIDGET07 'silently dropped' for design properties written on a row.", "cause": "Three layers, none carried it: parseLayoutGridRows read only Columns/weights/Widgets (never Appearance or the alignment enums); buildLayoutGridRowV3/ColumnV3 ignored every property but widths (validate_design_properties classified row/column as slotDropped); layoutGridRowToGen/ColumnToGen hardcoded newAppearance(\"\",\"\",\"\",nil), alignment \"None\" and SpacingBetweenColumns true. sdk/pages.LayoutGridRow/Column had no field to hold any of it.", "file": "sdk/pages/pages_widgets_container.go, mdl/executor/cmd_pages_describe_parse.go (extractAppearance), cmd_pages_describe_output.go (row (...) / column (...) via formatWidgetProps), cmd_pages_builder_v3_layout.go (designPropertyValuesV3 with theme key LayoutGridRow/LayoutGridColumn, layoutGridAlignment), cmd_pages_builder_v3.go, validate_design_properties.go (slotLayoutGridRow/Column), validate_widgets.go (known props), mdl/backend/modelsdk/widget_write.go, mdl/backend/mcp/page_widgets.go", "insight": "A hardcoded literal in a *ToGen helper (newAppearance(\"\", \"\", \"\", nil), \"None\") is the writer-side tell of a describe round-trip gap, the same way a zero-arg constructor is. Count design-property values over the whole page dump before/after rather than diffing: the loss is invisible in mx check. The theme key matters: a row/column's design properties live under the Atlas classes LayoutGridRow / LayoutGridColumn, not DivContainer (what resolveDesignPropsKey('row') gives, since a top-level `row` builds a container), so ToggleButtonGroup typing and MDL-WIDGET11/12 validation need the explicit key. `VerticalAlignment: End` parses although END is a keyword (qualifiedName admits it).", "refs": "ledger Home_Web round trip; prior: 2026-09-29-re-running-describe-page-output-resets-every-layout-grid", "test": "mdl/executor/cmd_pages_layoutgrid_appearance_test.go, mdl/backend/modelsdk/widget_write_layoutgrid_appearance_test.go; control: both fail with the parse/build/writer changes stashed; e2e Home_Web 46 -> 46 entries, VerticalAlignment Center x6 kept, second exec 'Unchanged page'"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/deprecation","date":"2026-10-09","symptom":"MDL-DEPR081 said `write Visible: <expression> / Editable: <expression> — same meaning`; following it by hand turns `Visible: [\"Value\" != empty]` into `Visible: Value != empty` (unbound) and `Visible: [\"N1\"]` into a non-attribute — every notes-mode cell in sudoku rebound, with check, exec, mx check and tests all green (sudoku FINDINGS #63)","cause":"the registry entry's `Canonical` was the bare form, but the brackets root a bare attribute in $currentObject; the Structural rewrite and `fmt --upgrade` already wrote `$currentObject/Attr`, so the one-line message contradicted its own suggestion","file":"`mdl/deprecation/deprecation.go` (BracketedWidgetCondition entry)","insight":"**`Canonical` is shown verbatim in six places (check/exec warning, mdl-2 refusal, LSP, fmt notes, help, migration table) behind a universal \"same meaning\" — so it must be the form that stores the same thing, not the form that merely parses.** Guard `TestBracketedWidgetConditionMessageNamesCurrentObject` reads the text between `write` and `same meaning`; on the old entry it fails with the bare form. `make gen-migration-reference` regenerates versions.md","refs":[],"ce":[]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/linter", "date": "2026-10-09", "symptom": "`mxcli docker check` fails with ~10× CE2729 \"No read access to attribute … for user role 'X' (with no roles defined in module 'M')\" on the widgets of the navigation home page, while `mxcli lint` and `check --references` are clean — a template user role (e.g. `User`) left in place after the home page moved to a page in a module it has no role in (ChipCoV6 FINDINGS)", "cause": "MPR007 only checked that a navigation page has *some* allowed role (CE0557). mxbuild validates the home page per user role, but nothing checked that each user role can open the home page it lands on, so the CE2729s name widgets and attributes and never the cause", "file": "`mdl/linter/rules/page_navigation_security.go` (`checkHomePagePerUserRole`)", "insight": "MPR007 now resolves each user role's effective home page per profile (role-based entry, else default; microflow home pages against the microflow's allowed roles) and warns when none of the role's module roles is allowed. Skipped at security level Off; a page with no roles at all stays CE0557's report. Repro: `create user role TmpUser (ModuleRoles: (System.User, Administration.User))` on a copy of ChipCoV6 — base lint is silent, mxbuild 11.15.0 reports 10× CE2729. A per-widget CE flood usually has one per-role cause upstream; lint it there"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/visitor","date":"2026-10-09","symptom":"`drop microflow M.F if exists;` (SQL order) gave `extraneous input 'if'` with no hint, and under `mdl 1;` a second error, `the statement ending at \"F\" has no terminating ;`, blaming a `;` that is present (ledger FINDINGS #166)","cause":"the grammar takes `if exists` before the name; ANTLR's single-token deletion ends the drop at the name and discards `if exists;`, and ExitStatement's mdl-1 terminator check then read the recovery-truncated statement as unterminated","file":"`mdl/visitor/visitor.go` (`dropIfExistsAfterNameRe` in enhanceErrorMessage; errorListener.lines -> Builder.syntaxErrorLines), `mdl/visitor/visitor_strict_terminators.go` (ExitStatement)","insight":"**A builder check that reads a statement's last tokens is reading the error-recovered tree on a line with a syntax error, so it must stand down there or it reports the recovery as a second mistake.** The parser finishes before the walk, so the listener's error lines are available to every Exit*. Guard `TestDropIfExistsAfterNameHint` (control `TestDropIfExistsControls`: canonical order parses; a genuinely missing `;` on a clean line is still refused); reverting only the terminator half brings back both `no terminating ;` errors","refs":[],"ce":[]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/visitor","date":"2026-10-09","symptom":"`Icon: Atlas_Core.Atlas.add-circle` failed with `extraneous input '.' expecting {',', ')'}` (pointing at a dot) or `no viable alternative` on a widget, and `Icon: 'Atlas_Core.Atlas.add-circle'` with `mismatched input ... expecting the start of a statement`; neither said that the fix is `Atlas_Core.Atlas.\"add-circle\"` — two retries in ChipCoV6's build","cause":"an icon is a qualified name and `add-circle` is not an identifier; the grammar is right to refuse both forms, but the error carried no hint","file":"`mdl/visitor/visitor.go` (`hyphenatedIconRe` in enhanceErrorMessage)","insight":"**When the grammar is right and the mistake is predictable from the source line, the fix is a hint keyed on the line, not a grammar change** — accepting the whole-name string would break \"references are qualified names\" and need its own resolution path. Guard `TestHyphenatedIconNameHint` covers menu item, quoted-whole and widget icon; `TestHyphenatedIconNameHintControls` keeps the quoted segment parsing and keeps the hint off unrelated errors on an icon line","refs":[],"ce":[]}
4 changes: 2 additions & 2 deletions .claude/skills/mendix/alter-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ The older dotted form `gridName.columnName` still works; it matches a name mxcli
### ADD Variables - Add a Page Variable

```sql
add variables $showStockColumn: boolean = 'true'
add variables $showStockColumn: boolean = true
```

Adds a new page variable (`Forms$LocalVariable`) to the page/snippet. DataType can be `boolean`, `string`, `integer`, `decimal`, `datetime`, or an entity type. Default value is a Mendix expression in single quotes.
Expand Down Expand Up @@ -451,7 +451,7 @@ alter page MyModule.Customer_Edit {
```sql
mdl 1;
alter page MyModule.ProductOverview {
add variables $showStockColumn: boolean = 'if (3 < 4) then true else false'
add variables $showStockColumn: boolean = if (3 < 4) then true else false
};
```

Expand Down
1 change: 1 addition & 0 deletions .claude/skills/mendix/assess-quality/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ After reviewing automated results, assess the following areas manually. The guid
|-----------|------|----------|
| 1:1 mapping between module roles and user roles | CONV008 | High |
| Pages in navigation must have allowed roles | MPR007 | High |
| Every user role can open its home page (else CE2729) | MPR007 | High |
| No guest/anonymous access to sensitive data | SEC004 | Critical |
| Strict security mode enabled | SEC005 | High |
| No demo users in production | SEC003 | Critical |
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/mendix/create-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Guide for writing CREATE PAGE statements in Mendix Definition Language (MDL).
create [or replace] page Module.PageName
(
[params: ( $ParamName: Module.EntityType | PrimitiveType, ... ),]
[variables: ( $varName: DataType = 'defaultExpression', ... ),]
[variables: ( $varName: DataType = <default expression>, ... ),] -- bare: `= true`, not `= 'true'`
title: 'Page Title',
layout: Module.LayoutName,
[url: 'page-url',]
Expand Down
8 changes: 7 additions & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Mendix Model SDK Go - Development Container
FROM mcr.microsoft.com/devcontainers/go:dev-1.26-bookworm
FROM mcr.microsoft.com/devcontainers/go:dev-1.27-bookworm

# The base image sets GOTOOLCHAIN=local, which ignores go.mod's `toolchain`
# line and builds with whatever Go the image ships. Its tags track minor
# versions only (dev-1.27 was 1.27.1 when go.mod moved to 1.27.2 for security
# fixes), so `auto` lets go.mod — the same pin CI uses — choose the toolchain.
ENV GOTOOLCHAIN=auto

RUN rm -f /etc/apt/sources.list.d/yarn.list

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- **A page or snippet variable's default is written as a bare expression** — `Variables: ( $show: Boolean = true )`, `= if (3 < 4) then true else false`, `= 'Price' + ' list'`, `= Module.Enum.Value`, and the same in `alter page … add variables`. The old spelling, a string whose content is the expression (`= 'true'`), keeps its meaning under every language version and warns as **MDL-DEPR086** (refused from `mdl 2`). `fmt --upgrade` rewrites it, and `describe` prints the bare form. A default that is itself a string (`$s: String = '''abc'''`) or is empty keeps its current spelling and is not reported, since a bare `'abc'` is the old spelling.
- **A published OData service's authentication is a property** — `create published odata service M.S ( …, Authentication: (basic, session, microflow M.Authenticate) )`, in the order written, replaces the trailing `authentication basic, session` clause (R9). The property can also say `Authentication: none`, which the clause could not, and `alter published odata service M.S set ( Authentication: … )` now changes authentication on an existing service — before, it could only be restated with the whole service. Left out, `create or modify` and `alter` keep the stored setting, as before. `describe` prints the property. **Migrating a script:** nothing breaks — the clause (**MDL-DEPR139**) still parses and builds the same service, `check` / `exec` warn, and `mxcli fmt --upgrade` moves it into the list (a clause naming a method MDL has no keyword for, e.g. `authentication Custom`, is reported and left alone). A stored setting MDL cannot state — a microflow stored without the Microflow method, or the reverse — is now a comment in `describe` rather than printed as `Microflow M.F`, which used to add the method when the output was executed. Executing the `describe` output of each of ako/TestApp's three Studio Pro OData services writes nothing.
- **`call rest service` takes its settings as one property list** (ADR-0013) — the activity's dialog settings go in one `( Key: value, … )` list after the URL, keyed as the consumed REST service names the same concepts: `$Html = call rest service get 'https://example.com' (Headers: ('Accept': 'text/html'), Authentication: basic (Username: $User, Password: $Password), Timeout: 300) returns String;`. `Body:` is `template '…' [with ({1} = …)]`, `mapping M.EMM from $Var`, `binary <expr>` or an expression. The method, URL, `returns …` and `on error …` stay words. An unknown or repeated key, or a value of the wrong shape, is an error. `describe` writes this form. **Migrating a script:** nothing breaks — the clauses `header 'N' = v`, `auth basic $u password $p`, `body …` and `timeout n` (**MDL-DEPR720**) still parse and store the same activity, `check` / `exec` warn, and `mxcli fmt --upgrade` rewrites them; a statement cannot mix the two forms. ADR-0013 makes this the rule for every new microflow activity and every activity with several settings.
- **`run --local --page-check` signs in with `--screenshot-user` without `--screenshot`, and checks all pages in one browser** — the sign-in only ran when `--screenshot` was also given, so `--page-check --screenshot-user U` reported every secured page as the login page. The verdict also no longer counts a list view's "No items found" placeholder or a data grid's header as rows, ignores the demo-user switcher's "Select user" heading, and reports a failed same-origin request (`HTTP 560 POST /xas/`) instead of the duplicate "Failed to load resource" console line. The login script used by `--screenshot-user` falls back to `/login.html` when the app root does not show the sign-in form, and finds Playwright the way the page check does (no `playwright` CLI on `PATH` needed).
Expand Down
2 changes: 1 addition & 1 deletion cmd/mxcli/cmd_lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Built-in rules check for:
- Empty validation feedback (MPR004) - validation feedback with empty message
- Unconfigured images (MPR005) - IMAGE widgets with no source configured
- Empty containers (MPR006) - layout containers with no children
- Navigation page security (MPR007) - pages in navigation need allowed roles
- Navigation page security (MPR007) - pages in navigation need allowed roles, and every user role must be able to open its home page
- Gallery selection listener (MPR009) - DataView 'DataSource: selection X' needs gallery 'ItemSelectionMode: toggle' (Studio Pro CE3637)
- Entity access rules (SEC001) - persistent entities need access rules
- Password policy (SEC002) - password minimum length should be 8+
Expand Down
Loading
Loading