Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
01c3a84
fix: a restated if outweighs a new one whose condition differs
ako Oct 21, 2026
a4ed9b6
fix: create or modify does not move a node the script places (mdl 1)
ako Oct 21, 2026
c4f43e8
docs: list the stated-position refusal among mdl 1's splice refusals
ako Oct 21, 2026
9442043
fix: a splice making room leaves the flow's parameters where they are
ako Oct 21, 2026
27b064b
Merge pull request #1548 from ako/fix/1547-splice-shift-leaves-parame…
ako Oct 22, 2026
756bcfb
refactor: move the object-list body-slot rule into mdl/types
claude Oct 22, 2026
66a4a1b
feat(grammar): an item address takes a slot, `acc1 item@1.headercontent`
claude Oct 22, 2026
3002233
feat(alter page): address a pluggable widget's object-list items
claude Oct 22, 2026
a50ac0a
fix(executor): refuse a page following a view entity's association fr…
claude Oct 22, 2026
b39bd6b
fix(executor): refuse a String-returning widget expression given a no…
claude Oct 22, 2026
f1ccb59
fix(alter page): keep an item's hidden slots and texts as CREATE stor…
claude Oct 22, 2026
faa20ca
docs: document object-list item addressing; regression script; finding
claude Oct 22, 2026
ba67202
docs(skill): regenerate the alter-page skill index after the new section
claude Oct 22, 2026
17b8967
chore: gofmt the test stubs; keep the alter-page skill under its 700-…
claude Oct 22, 2026
676ad81
Merge pull request #1569 from ako/fix/1508-splice-shift-stated-positions
ako Oct 22, 2026
0d79e62
Merge branch 'mendixlabs:main' into main
ako Oct 22, 2026
3195fe5
Merge pull request #1613 from ako/feat/1453-object-list-item-address
ako Oct 22, 2026
aa10daf
Merge pull request #1611 from ako/fix/1611-view-association-reverse
ako Oct 22, 2026
6b82305
Merge pull request #1612 from ako/fix/1611-widget-expression-return-type
ako Oct 22, 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/mxcli-dev/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ proactively. Add a row after every review that surfaces something new.
| 21 | A registry that reads only one source (an `embed.FS`) has no extension point, so the only way to customise its output is to hand-edit the generated block — which a digest fence then refuses to touch on the next run. The feature ends up hostile to the exact case it was built for | API design | When a guard refuses a user's edit, ask whether the thing being edited should have been authorable. Introduce a source abstraction (`fs.FS` + root) and let a project-local directory shadow the embedded set — the walk functions usually already take a root, so the change is contained |
| 22 | A test helper that expands a template cross-products every list against every reference (e.g. every `@each $weight` list against every `url()`), inventing artefacts no asset ever shipped and failing on correct input | Test coverage | Scan positionally: a reference must be expanded against the loop it actually sits under. Prove the helper both fires on a real break (delete one shipped file) and stays quiet on correct input — a helper only checked against green code has not been shown to detect anything |
| 23 | A copy-to-scaffold path renames files but not the identifiers built from the name (`@mixin mxcli-<name>-<alt>`, `@import "mxcli-<name>"`), so two artefacts collide the moment both exist — and the symptom is a rule that silently compiles to nothing | Code correctness | Assert the structural contract on the *generated* artefact, not just the shipped ones: factor the built-in's contract test into a helper and run the scaffold through it. Verify once end to end against the real toolchain and record it |
| 24 | A new splice refusal routed through the shared `cannotSplice` path changes **mdl 0** too: there a refusal falls back to the whole-flow rebuild, so a headerless script that used to splice now loses its merges, curves and `$ID`s — a silent change of meaning ADR-0011 rules out (#1508 case 6, caught by `TestFlowModify_InsertIsSpliced`) | Language versioning | Gate the new refusal on `flowRebuildRefused.Applies(ctx.LanguageVersion)` so mdl 0 keeps its old splice; add an mdl 0 case asserting "spliced, no MDL-V1-REBUILD" beside the mdl 1 refusal test |
| 24 | A stacked PR's base merged at an older head: commits pushed to the base branch after its merge never reach `main`, while both PRs read as fine (ako/mxcli#1108 lost its last two commits this way) | Merge hygiene | Before merging any stacked PR, and after pushing to a branch whose PR may already be merged, check each commit with `git merge-base --is-ancestor <sha> origin/main`; retarget the next PR to `main` so it carries the stranded commits, and say so in its description |
| 24 | A writer leaves out a property Studio Pro always writes (an empty `Documentation`, the empty half of an attribute/association pair) or writes one Mendix does not declare. `mx check`, the build and the runtime stay green, but Mendix's merge/diff engine compares property NAMES and throws, so the document becomes unmergeable once Studio Pro saves it (mendixlabs/mxcli#1373) | BSON writing | The doctype gate's `checkPropertySets` catches it per script: `mx convert -p` re-serializes what is not canonical. Fix the writer, or record Mendix's value with `MXCLI_PROPERTY_SETS_RECORD` + `internal/propertysets/cmd/propdefaults`; never guess a default |
| 24 | A skill or command still instructs work into a package the repo has deleted (`sdk/mpr` test locations, a symptom table moved to `findings/*.jsonl` years prior) — the doc reads as authoritative and every instruction in it is a compile error or a no-op | Docs quality | When a package is deleted or a doc is restructured, grep `.claude/` for its name in the same PR. A deletion that leaves the guidance behind is worse than no guidance, because the reader trusts it |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/backend", "date": "2026-10-21", "symptom": "create or modify of a nanoflow drawn with @position on every statement, adding an if/else after the declare without @position and editing the two calls in the next if's branch: `spliced: 1 inserted, 1 replaced, 1 dropped`, with `if $Flag` -299 -> 40, its merge 186 -> 525 and the return 530 -> 869, while the replaced calls stayed at (-124, 100)/(60, 100), left of their own decision. Re-running the same script was refused (`the connectors of the If at (40, 200) are redrawn`). With a plain if (no else) the new if took over the stored decision and the restated `if $Flag` was drawn on top of it at (-299, 200), both merges at (186, 200).", "cause": "Three rules that each held alone. makeRoom shifts every top-level node past the cut by the deficit (339); a Replace whose first statement states @position is Placed and written at its stated coordinates, never shifted; movedNode records a move only when the stated position differs from the stored one, so the shifted nodes were never moved back, and the merge is not a move candidate at all. On re-run @merge is geometry but not a position field, so the pair fell to `redrawn`. No-else: lcsStatements weighed ifConditionChanged the same as sameIfShell and the tie went to the earlier pair, so the new if became a condition edit of the stored decision and the restated if a Placed insert, which never runs checkRoom.", "file": "mdl/backend/mfmutator/splice.go (Pin, pinnedPast, makeRoom, Replace), mdl/backend/microflow_mutation.go (MicroflowMutator.Pin), mdl/executor/cmd_flow_modify_patch.go (statedPoints, patch), mdl/executor/cmd_flow_modify.go (lcsStatements weights, mdl 1 gate in planFlowModify)", "insight": "Shift-consistently cannot satisfy idempotence: once the script states -299 and the store says 40, the re-run must move it back or refuse — measured even for a plain one-activity insert into a described flow (+104, then the re-run refused on @merge). So a node at a stated point is pinned and a shift that would move one is refused, naming the room and the node; the mutator pins by point rather than $ID because the node at a stated point may be one a later Placed fragment puts there. Collect stated points from @position, @merge, @start, note and parameter positions, skipping loop bodies (relative coordinates). The control is the same script with the positions after the insert left out: spliced, everything moved by one delta, re-run Unchanged. Gate the pins on mdl 1 (flowRebuildRefused.Applies): under mdl 0 the refusal falls back to the whole-flow rebuild, which deletes the Studio Pro merges and $IDs the shift kept — TestFlowModify_InsertIsSpliced (ADR-0012's describe+insert acceptance, headerless) caught exactly that, and ADR-0011 forbids the change of meaning. Not touched: the shift still moves parameters (y 53) along with the row.", "refs": ["ako/mxcli#1508", "mendixlabs/mxcli#1598"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/backend", "date": "2026-10-21", "symptom": "A splice that makes room for an inserted fragment (create or modify / alter … insert) moved the flow's parameters along with the row: one if/else inserted into NF_Do moved $Flag from its derived (200, 53) to (539, 53), a longer replace then to (699, 53), and from then on describe printed a parameter @position no script stated. Inserting before the first statement of a two-parameter microflow moved $Count from (300, 53) to (410, 53).", "cause": "mfmutator shift (splice.go) moves every top-level node past the cut, and a Microflows$MicroflowParameter is a top-level object of the unit's object collection (the decoded model keeps parameters apart, so nothing in the executor sees them as nodes). mxcli's derived parameter row is absolute (200 + idx*pitch, 53), right of the start event, so almost every insert's cut falls left of it.", "file": "mdl/backend/mfmutator/splice.go (shifted)", "insight": "Measure the authored convention before deciding what 'the row' is: 63 of 71 Studio Pro-authored parameters (PedApp 19/20, JTSBootLogboek 44/51) sit exactly at the start event's x, all above every other node, none within 60px of the flow line — a parameter belongs to the flow, not to the stretch of line past an insert, and Studio Pro moves nothing on insert. The decoded model hides this: microflows.Microflow carries Parameters apart from ObjectCollection, so a probe that type-switches the collection for *MicroflowParameter does not compile, while the BSON the mutator walks has them inline. Excluding them in shifted also keeps them out of pinnedPast, so a stated parameter position can no longer block a splice it is not in the way of.", "refs": ["ako/mxcli#1547", "ako/mxcli#1508"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/backend/pagemutator", "date": "2026-10-22", "symptom": "`insert into grp1 { … }` for an Accordion group: widget \"grp1\" not found — the group stores no Name; `insert into acc1 item@2 { … }` was refused as \"not a navigation list\"; an empty group could not be filled at all", "cause": "object-list items (CustomWidgets$WidgetObject) had no address; resolveNavItem accepted only navigation lists", "file": "`mdl/backend/pagemutator/objectlist_items.go` (resolveObjectListItem, item slots, ObjectListItemValues), `column_hidden_reset.go` (ResetHiddenItemProperties, template nulling/creation), `mdl/executor/alter_hidden_item_slot.go`; grammar `alterTarget` itemSlot", "insight": "The default `docker check` normalises widgets on a temp copy and HID every defect here — run `--no-update-widgets` (the project as stored, what MxBuild and run --local see) or a hidden-slot write and a mode-switching SET both look clean while each is CE0463. Diffing a SET-switched group against a CREATE-made one (TypePointer -> PropertyKey in the JSON dump) found the cause in one step: headerText template filled vs null. Two plausible wrong turns: assuming the schema DefaultValue holds the visible default (it is \"\" — 'Header' lives in the widget package, so a revealed text with no value is refused, not invented), and a check-time hidden-slot refusal that blocked `set (headerRenderMode: 'custom'); insert into …headercontent` — the very fix its own message recommended — because check reads the stored document; it stands down when the statement sets the item first, and exec decides on live values. The item result carries the item schema like a DataGrid2 column, so SET and `.slot` reused existing paths rather than growing new ones", "refs": ["mendixlabs/mxcli#1453", "ako/mxcli#1326"], "ce": ["CE0463", "CE4899"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-10-22","symptom":"`column (Attribute: IsActive) { dropdownfilter ddf (defaultValue: 'true') }` passed check and exec, then mx check failed CE0117 \"Error(s) in expression.\" at the drop-down filter (mendixlabs/mxcli#1611 B).","cause":"defaultValue is a pluggable Expression property whose template declares ReturnType String, and a quoted MDL value on an Expression property is the expression's text, so 'true' stored the Boolean expression `true`. No check typed an authored expression against the property's declared return type; the embedded schema read each property's ValueType.Type but not its ReturnType.","fix":"MDL-WIDGET49 (validate_widget_expression_return.go), beside MDL-WIDGET42 in validateWidgetProperties: for a String-returning Expression property, refuse a value that infers as a literal Boolean, Integer or Decimal (exprcheck.InferSourceKind) or is one bare name, with the String-literal spelling as the hint (`defaultValue: '''true'''`). embeddedWidgetSchema.returns records ReturnType.Type per Expression key. Runs with and without a project (the DROPDOWNFILTER definition is built in).","file":"mdl/executor/validate_widget_expression_return.go","insight":"The writer was right and the check was missing: the MDL convention (a quoted value IS the expression text) is what describe prints and what every other expression property relies on, so 'wrap the string for a String-returning property' would have silently changed the meaning of every existing script. Measure the column type separately from the property's return type: '1' failed in an Integer column too — the return type is the widget's, fixed at String, not the attribute's. Judge only what InferSourceKind is sure of: it calls [%CurrentDateTime%] a String, so DateTime is not trusted; Unknown (paths, variables, untyped functions) is never refused.","refs":["mendixlabs/mxcli#1611","ako/mxcli#1539"],"ce":["CE0117"],"rules":["MDL-WIDGET49"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-10-22","symptom":"A page data source following a view entity's association from the entity it points at — `listview (DataSource: DATABASE FROM $currentObject/M.Stats_Owner/M.Stats)` on a page over M.Owner, where M.Stats selects `o.ID as Stats_Owner` — passed check --references and exec, then mx check failed CE8812 \"Entity 'M.Owner' has no association 'M.Stats_Owner'\" (mendixlabs/mxcli#1611 A). Separately, its remedy `DATABASE M.Stats WHERE [M.Stats_Owner = $currentObject]` (and the microflow `retrieve … where M.Stats_Owner = $x`) was refused by check as naming 'neither an attribute nor an association' when the view entity came from the same script.","cause":"Nothing modelled that a view entity's OQL-derived association (OqlViewAssociationSource) is one-way for a page; MDL-ASSOCDS01's association index had no view flag. And scriptContext recorded only CREATE ASSOCIATION statements, so a script's view associations (its `<alias>.ID as <Name>` columns) were invisible to every name-resolving checker.","fix":"MDL-ASSOCDS02 (validate_view_assoc_reverse.go), hooked into attributeScopeValidator.walk beside MDL-ASSOCDS01: follow the association or `database from` path hop by hop from the widget's context entity and report the first hop that follows a view association from its TO end, with the constrained query as the fix. assocShape.view from Association/CrossAssociation Source == OqlViewAssociationSource; scriptContext.recordViewAssociations registers each view association column through recordAssociation (ends, name index, shape) and marks it view.","file":"mdl/executor/validate_view_assoc_reverse.go","insight":"Measure the remedy you are about to recommend in the same shape the user works in: the constrained query built at 0 errors against a stored view entity, but the check refused it when the view was created in the same script — the reporter's flow — so the new hint would have sent users into a second false refusal. Measured on 11.14.0, one page per shape with uniquely named widgets so mx check's 'at <widget>' attributes each CE8812: refused in list view, data grid, gallery and data view, database and in-memory form, at any hop; accepted from the view side, constrained, and in a microflow association retrieve (so the rule is page data sources only). An entity named Owner is itself CE7247 — rename repro entities before reading the result.","refs":["mendixlabs/mxcli#1611","ako/mxcli#1539"],"ce":["CE8812"],"rules":["MDL-ASSOCDS02"]}
12 changes: 8 additions & 4 deletions .claude/skills/mendix/alter-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ description: "Modify an existing page or snippet's widget tree in place with ALT
| 482-560 | Examples |
| 561-651 | DataGrid 2 columns: how to address them, and what you can set |
| 652-663 | Common Mistakes |
| 664-692 | Limitations — prefer binding at page creation (ledger finding #45) |
| 693-700 | Validation Checklist |
| 701-710 | Related Commands |
| 711-715 | Related Skills |
| 664-696 | Limitations — prefer binding at page creation (ledger finding #45) |
| 697-704 | Validation Checklist |
| 705-714 | Related Commands |
| 715-719 | Related Skills |

<!-- /skill-index -->
# ALTER PAGE / ALTER SNIPPET - Modify Existing Pages and Snippets
Expand Down Expand Up @@ -678,6 +678,10 @@ page and bind the buttons at creation time instead of rewiring afterwards:
view: `replace dvMain.footer with { footer { … } }`, `insert into
dvMain.footer { … }`, `drop dvMain.footer`.

**An Accordion group has no name either** (`group g1` keeps no `g1`): use
`acc1 item@2`, `acc1 item('One')` or `acc1 item(btnIn)`; `.headercontent` is its
header slot, refused until `set (headerRenderMode: 'custom') on acc1 item@1`.

**A pluggable widget's slot has no name either.** A Data grid 2's control bar
(`controlbar { … }` in describe) is a property of the grid, so address it by
the grid: `insert into dataGrid1.controlbar { actionbutton … }`, `replace
Expand Down
14 changes: 14 additions & 0 deletions .claude/skills/mendix/create-page/reference/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,8 @@ column (caption: 'Actions') {

> **A list widget needs an association that yields a list.** A list view, data grid or gallery over `$currentObject/M.Assoc` is CE8812 ("A grid association path must result in a list") when the association is a Reference followed from its FROM entity, or a Reference with owner Both from either end (one-to-one). Use a data view for the single object, or a ReferenceSet. `check` reports it as MDL-ASSOCDS01. The reverse of a default Reference (from the TO entity) is a list and fine.

> **A view entity's association is one-way for a page.** The association a view entity derives from its OQL (`select c.ID as Stats_Customer`) can be followed from the view entity only. A data source that follows it from the entity it points at, in any widget and at any hop (`database from $currentObject/M.Stats_Customer/M.Stats`, `$currentObject/M.Stats_Customer`), is CE8812 "Entity 'M.Customer' has no association 'M.Stats_Customer'". Query the view entity instead: `datasource: database M.Stats where [M.Stats_Customer = $currentObject]`. `check` reports it as MDL-ASSOCDS02. A microflow `retrieve … from $Cust/M.Stats_Customer` is accepted.

**With WHERE and SORT BY (inline in DataSource):**
```sql
datagrid dgActive (
Expand Down Expand Up @@ -710,6 +712,18 @@ custom attribute (Studio Pro's "Attribute config: Custom"); listing two is refus
attribute of the Data grid 2 column it sits in ("Auto"), so in a **gallery's**
`filter { … }` bar, which has no column, the attribute must be named.

`defaultValue:` preselects an option. It is an **expression returning a String**, and a
quoted MDL value is the expression's text, so the option goes in as a String literal
inside the quotes, even for a Boolean column:

```sql
column (Attribute: IsActive) { dropdownfilter ddfActive (defaultValue: '''true''') }
```

`defaultValue: 'true'` stores the Boolean expression `true`, and `'1'` or a bare name
like `'Open'` fail the same way: mxbuild reports CE0117 "Error(s) in expression." `check`
reports it as MDL-WIDGET49. `'empty'` (no preselection) is fine.

Filter by an **association** instead of an attribute — the options are the
associated objects. Giving the filter a `datasource:` (the OPTION list) selects
this mode; all three parts are required:
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 @@ -69,7 +69,7 @@ Choose the mode by who owns the microflow ([choose-edit-mode](../choose-edit-mod
(targets from `describe microflow X with handles`). `create or modify` of `describe`
output patches too: unchanged writes nothing; a statement (even one whose handler returns), guard
clause, `return` value, `if` condition, header clause, parameter (added/retyped; removed only if unused) or stated `@position`/`@start` change is
patched in place (a move keeps the node's flows), as is a loop-body activity with new values. A redrawn
patched in place (a move keeps the node's flows), as is a loop-body activity with new values ([an insert needing room past a stated `@position`](reference/pitfalls.md#13-inserting-where-the-script-states-every-position)). A redrawn
`@anchor`/`@curve`, other loop-body change, error handler or other `return` added/taken away rebuilds under
mdl 0 (`MDL-V1-REBUILD`: IDs renumbered, merges and curves lost) and is refused under `mdl 1;`. **To add,
remove or reorder in a loop body, `alter … replace` the whole loop** ([pitfalls](reference/pitfalls.md#11-changing-something-inside-a-loop-body)). **To rebuild deliberately, `drop` and `create` in ONE script** — the grants carry only within it ([pitfalls](reference/pitfalls.md#drop--create-is-still-a-new-document)).
Expand Down
Loading
Loading