Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7887777
build(deps-dev): bump serialize-javascript
dependabot[bot] Oct 29, 2026
11f1a46
docs: propose configurable microflow/nanoflow layout
claude Oct 31, 2026
95cba07
fix(lint): MPR012 follows the app's Web UI client (#1287)
ako Oct 31, 2026
942e9c9
docs: resolve open questions in configurable flow layout proposal
claude Oct 31, 2026
07b9777
Merge remote-tracking branch 'origin/main' into fix/1287-mpr012-class…
ako Oct 31, 2026
3e26964
feat(layout): ELSE body as a column under its decision (if-else: true…
claude Oct 31, 2026
b3a7f4f
feat(layout): layout-config.yaml, per-flow @mxcli-layout marker, layo…
claude Oct 31, 2026
fa337e2
test(layout): classify flowBuilder.layout as inherited by nested bodies
claude Oct 31, 2026
1e112a5
docs(skills): keep the layout-style note within write-microflows' lin…
claude Oct 31, 2026
a9972ba
feat(layout): guard: true-right — the early return on the line, the h…
claude Oct 31, 2026
b02f984
feat(layout): case-split: first-right-rest-columns for CASE and type …
claude Oct 31, 2026
03489a0
Merge pull request #1828 from ako/fix/1287-mpr012-classic-client
ako Oct 31, 2026
d325693
feat(layout): wrap: off and max-row-width
claude Oct 31, 2026
00a47d3
Merge pull request #1783 from ako/dependabot/npm_and_yarn/dot-claude/…
ako Oct 31, 2026
2220a56
Merge branch 'mendixlabs:main' into main
ako Oct 31, 2026
0ead995
Merge remote-tracking branch 'origin/main' into feature/configurable-…
claude Oct 31, 2026
f17e9c7
Merge pull request #1842 from ako/feature/configurable-flow-layout
ako Oct 31, 2026
4f4ffd1
docs(layout): make flow layout styles discoverable
claude Oct 31, 2026
e9bdd85
fix(codec): every write gives a stored list the marker Studio Pro wri…
claude Oct 31, 2026
4909f26
Merge pull request #1846 from ako/docs/flow-layout-discoverability
ako Oct 31, 2026
b05c9bd
Merge remote-tracking branch 'origin/main' into claude/cool-keller-jt…
claude Oct 31, 2026
ff6ed9b
test(executor): rename the #1640 test helper that collides with main'…
claude Oct 31, 2026
c33102d
test(roundtrip): strike five TestApp snippets that now round-trip (#1…
claude Oct 31, 2026
0c6c608
Merge pull request #1847 from ako/claude/cool-keller-jtch0r
ako Nov 1, 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 @@ -40,7 +40,7 @@ mxcli lint -p app.mpr --exclude System --exclude Administration
| MPR005 | quality | ImageSource - IMAGE widgets with no source configured |
| MPR006 | quality | EmptyContainer - Empty layout containers |
| MPR007 | security | PageNavigationSecurity - Navigation pages need allowed roles (CE0557); every user role must be able to open its home page (at Production security, else CE2729 per widget) |
| MPR012 | correctness | LegacyImageWidget - staticimage/dynamicimage are unsupported by the React client (CE0582) |
| MPR012 | correctness | LegacyImageWidget - staticimage/dynamicimage are unsupported by the React client (CE0582; info on the classic client) |
| MPR013 | correctness | SnippetCallAppearance - Class/Style/DynamicClasses/design properties on a snippet call never reach the page (it renders no element) |
| 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/linter", "date": "2026-10-31", "symptom": "MPR012 warned on every legacy static/dynamic image widget of a Mendix 11 app on the classic client (Web UI UseOptimizedClient = No), each message saying `mxbuild reports CE0582`, while mx check reported 0 errors: 57 false warnings on one app.", "cause": "The rule assumed the React client is the only client on Mendix 11 and never read the project's Web UI setting; the executor's check for the same CE0582 (usesReactClient) already did.", "file": "mdl/linter/rules/legacy_image_widget.go", "insight": "When a lint rule and an executor check guard the same CE code, they must share the condition: grep the CE number across mdl/ before writing either. CE0582 is the React client's error, so it is conditional on Web UI UseOptimizedClient == Yes; MigrationMode builds clean too. A rule that claims what mxbuild reports should be measured under each setting, not reasoned from the Mendix version.", "issue": "ako/mxcli#1287"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "modelsdk/codec", "date": "2026-10-31", "symptom": "A Data grid 2 an older mxcli stored with SortBar.SortItems marker 3 kept it through every `create or replace page`, including one that changed the page for real, while Studio Pro and a page created from nothing store 2. mx check, mx diff (names only, 0 changeDetails), lint and `fix property-sets` were all clean. Separately, a populated Forms$CallNanoflowClientAction / Forms$NanoflowSource ParameterMappings list was written with marker 3 where Studio Pro writes 2", "cause": "A marker registration only reaches lists the encoder builds. An unchanged pluggable widget's Type and Object pass through byte for byte (passStoredThrough), and the codec copies an existing element's clean properties as raw bytes, so a stored list keeps the marker an older build gave it. The nanoflow lists had no owner/key registration, and the child type (Forms$NanoflowParameterMapping) has no marker of its own, so a populated list took the default 3", "file": "modelsdk/canon/listmarkers.go; modelsdk/mpr/writer_core.go (normalizeForStorage); modelsdk/codec/defaults.go; mdl/backend/modelsdk/widget_write.go", "fix": "The marker tables moved to canon. Every write (normalizeForStorage, so insert, update and transaction alike) runs canon.NormalizeListMarkers, which gives each list with a registered owner/key marker that marker anywhere in the unit and leaves unregistered lists and child-type registrations alone. Registered the two nanoflow lists as 2", "insight": "`drop page` plus the same MDL giving 2 while the rewrite kept 3 points at pass-through, not at the writer: look for what the rewrite copies rather than what it builds. Applying a registration to stored bytes turns a wrong registration into a rewrite of Studio Pro's bytes, so the census (every owner/key registration against TestApp and PedApp, plus normalization as a fixed point on every fixture unit) is a test. A forced-wrong registration fails it twice: 164 disagreements and 114 changed units. Child-type markers are not applied: they are the encoder's rule for lists it builds, not a measurement of every list starting with that child. Do not fix this with `fix property-sets`: a stale marker breaks no merge or check, so the units nobody rewrites can keep it", "refs": "ako/mxcli#1640", "test": "mdl/executor/issue1640_stale_list_marker_test.go; mdl/backend/modelsdk/issue1640_list_marker_census_test.go; modelsdk/canon/listmarkers_test.go"}
6 changes: 4 additions & 2 deletions .claude/skills/mendix/create-page/reference/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -921,8 +921,10 @@ auto units and a responsive image — which `describe page` also omits, so a
round trip neither loses them nor invents them.

**CE0582** is reported for `staticimage` wherever it appears, by any app running
the React client — which Mendix added in **10.7** and which is the only client on
11, so this is not a Mendix 11 rule. The replacement is the pluggable `image`
the React client — which Mendix added in **10.7**. A Mendix 11 app can still run the
classic client (Web UI → `UseOptimizedClient` is `No` or `MigrationMode`), where these
widgets build clean, and there MPR012 is an info-level "convert before switching" note
instead of a warning. The replacement is the pluggable `image`
widget, which takes the same `Image:`; Studio Pro offers the conversion from the
CE0582 error's context menu. mxcli still writes it, because round-tripping a
model that already contains one is the point — and `mxcli lint` reports it as
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/mendix/write-microflows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ $var/Module.AssociationName/attribute -- Chained

### Annotation Pattern
```mdl
@position(200, 200) -- optional: omit it and mxcli lays the flow out; to re-arrange an existing flow run `mxcli layout flows`
@position(200, 200) -- optional: omit it and mxcli lays the flow out in the project's layout-config.yaml style (never imitate a style with @position; keep, never hand-write, a `@mxcli-layout:` doc line); to re-arrange an existing flow run `mxcli layout flows`
@caption 'Persist order'
@color Green
@annotation 'Note about the next activity'
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions 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

- **Configurable microflow and nanoflow layout** (ako/mxcli#1832) — a committed `layout-config.yaml` (in `.claude/` or next to the `.mpr`) chooses how `create microflow`/`create nanoflow` and `mxcli layout flows` place what a script leaves unpositioned: preset `happy-line` (the default, unchanged) or `true-right-false-down`, and the options `if-else: true-right-else-column` (ELSE stacked as a column under its decision), `case-split: first-right-rest-columns` (a CASE's or type split's first outcome on the line, the rest as columns), `guard: true-right`, `wrap: off` and `max-row-width`. A flow laid out in a non-default style records it as an `@mxcli-layout:` line in its documentation, which DESCRIBE and `create or modify` keep using for that flow, so its description stays free of `@position` when the project style changes. `mxcli layout config` shows the style in force; `mxcli layout flows --style <preset>` tries one. Without a config file nothing changes: measured on 137 flows, the geometry and DESCRIBE output are identical to before.
- **Optional page parameters** (mendixlabs/mxcli#1640, ako/mxcli#1649) — `Params: ( $Sale: Shop.Sale optional )` is Studio Pro's unticked *Required* (`Forms$PageParameter.IsRequired = false`, Mendix 11.5+): the page can be opened with no argument, from a menu item for example, where a required parameter is CE1571. `create page`, `alter page … add parameters` and `describe page` all spell it. `describe navigation` no longer invents `(Sale = $currentObject)` for a menu item opening such a page, which made its output one `exec` refused (MDL-PAGEARG01) and left the navigation profile unchangeable from MDL. Below 11.5 `optional` is refused (MDL-VERSION01), and a snippet parameter cannot be optional (MDL087).

### Changed
Expand All @@ -16,11 +17,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **MPR012 follows the app's Web UI client** (ako/mxcli#1287, mendixlabs/mxcli#1226) — a Mendix 11 app can still run the classic client, where the legacy static and dynamic image widgets build clean, but MPR012 warned on each of them that "mxbuild reports CE0582". On the classic client or in migration mode it is now an info-level note that the widget needs converting before the app switches; under the React client it stays a warning.
- **Every `mxcli init --tool <x>` installs the bundled lint rules, and `lint` says when it runs without them** (ako/mxcli#1292, mendixlabs/mxcli#1242) — only `claude` and `opencode` wrote `.claude/lint-rules/`, so `init --tool cursor` (or aider, continue, copilot, vibe, windsurf) left `mxcli lint` running the built-in Go rules alone, silently. `lint` and `report` now print a warning when no `.claude/lint-rules/` is found, naming how many rules run instead.
- **`show references to` an entity, attribute or association lists the message definitions that expose it** (#1781) — a message definition (the pre-11.15 collection, `MESSAGE_DEFINITION_COLLECTION`, and the 11.15+ document, `MESSAGE_DEFINITION`) was not walked for references, so impact analysis called an exposed entity or attribute unused. The entity edges are `mapping`, the attribute and association edges `member`, as for an import or export mapping.
- **A pluggable widget's hidden expression property is stored as Studio Pro stores it** (ako/mxcli#548) — a property the widget's editorConfig hides is written at its declared default, and for an Expression-typed one that default went into `PrimitiveValue` as well as `Expression` (every combo box's `customEditabilityExpression`, `PrimitiveValue: "false"`). Studio Pro leaves `PrimitiveValue` empty on all 939 Expression-typed values of ako/TestApp and PedApp; mx check accepted both, so the cost was a widget that differed from Studio Pro's.
- **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.

## [0.29.0] - 2026-10-30

Headline: **Write what the project's own Mendix version stores, and commit it the way Studio Pro does.** Every write is now checked against the element types and property shapes of the project's Mendix version, so a statement 9.x or 10.x cannot store is refused by `check -p` instead of leaving a project Studio Pro cannot open, and `mxcli git commit` / `git doctor` keep a plain-git history in the shape Studio Pro expects. Alongside it: workflows, pages and Java actions gain the settings a rewrite used to reset, `alter microflow` edits inside loop bodies, the debugger works end to end, and commands on large MPR v2 projects open only the files they need.
Expand Down
75 changes: 72 additions & 3 deletions cmd/mxcli/cmd_layout_flows.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,18 @@ The layout is the one CREATE MICROFLOW uses for a flow written without
are sized to their bodies. Name the flows to lay out, or pass --module to lay
out every microflow and nanoflow in a module.

Only positions change: activity and event positions and sizes, parameter
Positions change: activity and event positions and sizes, parameter
positions, and the anchors and curves of sequence flows. Content, element IDs
and everything else about the flow are left exactly as stored.
and everything else about the flow are left exactly as stored, apart from
the one documentation line described next.

The style is the project's layout-config.yaml (in .claude/ or next to the
.mpr; see 'mxcli layout config'), or the default when there is none. --style
lays out in a preset instead, to try one. Each flow laid out in a style other
than the default records it as an '@mxcli-layout:' line in its documentation,
which DESCRIBE and CREATE OR MODIFY then keep using for that flow; laying it out
in the default removes the line. That line is the one thing besides positions
this command writes.

A flow whose description does not rebuild into the same graph — something MDL
cannot yet express — is skipped with the reason, never laid out by guesswork.
Expand All @@ -48,7 +57,8 @@ what would move first. Marketplace modules and System are never touched unless
--include-marketplace is given.`,
Example: ` mxcli layout flows -p app.mpr MyModule.ACT_Order_Submit
mxcli layout flows -p app.mpr --module MyModule
mxcli layout flows -p app.mpr --module MyModule --dry-run`,
mxcli layout flows -p app.mpr --module MyModule --dry-run
mxcli layout flows -p app.mpr --module MyModule --style true-right-false-down --dry-run`,
RunE: func(cmd *cobra.Command, args []string) error {
return runLayoutFlows(cmd, args)
},
Expand All @@ -59,9 +69,58 @@ func init() {
"module whose flows to lay out (repeatable)")
layoutFlowsCmd.Flags().BoolVar(&layoutDryRun, "dry-run", false,
"report what would move without writing")
layoutFlowsCmd.Flags().StringVar(&layoutFlowStyle, "style", "",
"lay out in this preset instead of the project's layout-config.yaml (happy-line, true-right-false-down)")
layoutFlowsCmd.Flags().BoolVar(&layoutIncludeMarketplace, "include-marketplace", false,
"also lay out flows in Marketplace modules (a module update replaces them, so this is normally pointless)")
layoutCmd.AddCommand(layoutFlowsCmd)
layoutCmd.AddCommand(layoutConfigCmd)
}

var layoutFlowStyle string

var layoutConfigCmd = &cobra.Command{
Use: "config",
Short: "Show the layout style microflows and nanoflows are laid out with",
Long: `Show the style CREATE MICROFLOW, CREATE NANOFLOW and 'mxcli layout flows'
lay flows out with, and the file it comes from.

The style is read from layout-config.yaml in the project's .claude/ directory or
next to the .mpr — a committed file, so everyone working on the project lays
flows out the same way:

flows:
preset: true-right-false-down # happy-line (default) | true-right-false-down
if-else: true-right-else-column # lanes | true-right-else-column
case-split: lanes # lanes | first-right-rest-columns
guard: return-below # return-below | true-right
wrap: rows # rows | off
max-row-width: 2880 # px, 640..20000

An unknown key or value is an error rather than a silent fallback.`,
Example: ` mxcli layout config -p app.mpr`,
RunE: func(cmd *cobra.Command, args []string) error {
projectPath, _ := cmd.Flags().GetString("project")
if projectPath == "" {
return fmt.Errorf("no project given: pass -p <project.mpr>")
}
exec, logger := newLoggedExecutor("subcommand")
defer logger.Close()
defer exec.Close()
exec.SetQuiet(true)
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", visitor.QuoteString(projectPath)))
for _, stmt := range connectProg.Statements {
if err := exec.Execute(stmt); err != nil {
return err
}
}
style, source, err := exec.FlowLayoutStyle()
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "Flow layout: %s\nFrom: %s\n", style, source)
return nil
},
}

// flowTarget is one flow to lay out.
Expand All @@ -86,6 +145,9 @@ func runLayoutFlows(cmd *cobra.Command, args []string) error {
defer logger.Close()
defer exec.Close()
exec.SetQuiet(true)
if err := exec.SetFlowLayoutStyle(layoutFlowStyle); err != nil {
return err
}
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", visitor.QuoteString(projectPath)))
for _, stmt := range connectProg.Statements {
if err := exec.Execute(stmt); err != nil {
Expand Down Expand Up @@ -195,6 +257,13 @@ func layoutFlowTargets(out io.Writer, exec *executor.Executor, targets []flowTar
changed++
fmt.Fprintf(out, "%s: moved %d of %d objects, re-anchored %d flows\n", res.Name, res.Moved, res.Objects, res.Flows)
}
if res.Restyled && res.Refused == "" {
verb := "updates"
if layoutDryRun {
verb = "would update"
}
fmt.Fprintf(out, "%s: %s the %s line in its documentation\n", res.Name, verb, "@mxcli-layout:")
}
}

switch {
Expand Down
16 changes: 15 additions & 1 deletion cmd/mxcli/syntax/features_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,8 @@ func init() {
"position", "start", "anchor", "curve", "layout", "canvas",
"annotation", "caption", "color", "excluded", "bezier",
"disable activity", "disabled activity",
"layout style", "layout-config", "mxcli-layout", "else column",
"wrap", "row width",
},
Syntax: "@position(x, y) -- the activity's centre point\n" +
"@position(x, y) -- also on a PARAMETER, in the ( … ) list\n" +
Expand Down Expand Up @@ -977,7 +979,19 @@ func init() {
"never prints it: a whole flow declares its notes. (ako/mxcli#1695)\n\n" +
"position:/size: are the note's own geometry, omitted whenever they match\n" +
"what a rewrite re-derives (100px above the activity, stacked 60px per extra\n" +
"note, 200x50), so an ordinary note keeps the short form. (#1077)",
"note, 200x50), so an ordinary note keeps the short form. (#1077)\n\n" +
"LAYOUT STYLE — where mxcli puts what you leave unpositioned — is the\n" +
"project's layout-config.yaml (.claude/ or next to the .mpr), never something\n" +
"to imitate with @position. Leave positions out and the style applies:\n" +
" flows:\n" +
" preset: true-right-false-down # happy-line (default)\n" +
" if-else: true-right-else-column # ELSE as a column under its decision\n" +
" case-split: first-right-rest-columns\n" +
" guard: true-right # early return on the line\n" +
" wrap: off # or max-row-width: <px>\n" +
"`mxcli layout config` shows the style in force. A flow drawn in a non-default\n" +
"style carries a `@mxcli-layout: …` line in its /** … */ documentation: keep\n" +
"it when copying a described flow, never write one by hand. (ako/mxcli#1832)",
Example: "create microflow MyModule.ACT_Flow (\n @position(145, 0)\n $In: String\n)\nreturns String as $Out\nbegin\n" +
" @start(145, 100)\n @position(200, 100)\n @anchor(from: bottom)\n" +
" @curve(from: (40, -90), to: (-40, 90))\n declare $Tmp String = $In;\n" +
Expand Down
Loading
Loading