Skip to content

Sync ako/mxcli: splices keep stated positions (#1598), ALTER PAGE object-list items (#1453), view-association check (#1611) - #1637

Merged
ako merged 19 commits into
mendixlabs:mainfrom
ako:main
Oct 22, 2026
Merged

ako merged 19 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Syncs ako/mxcli:main into upstream: 12 commits from fork PRs ako#1569 (which carries ako#1548), ako#1613 and ako#1611.

Fixes #1598: cases 1–5 landed in the previous sync, and this adds case 6. Refs #1453 (most of the proposal) and #1611 (part A).

Microflow splices keep what the script places (#1598 case 6)

  • create or modify no longer moves a node the script places (mdl 1). Inserting statements without @position used to make room by shifting everything past the gap.
    • In the reported case, if $Flag went from -299 to 40, its merge from 186 to 525 and the return from 530 to 869. The calls in its branch stayed at their stated positions, left of their own decision, so a re-run was refused.
    • A stated position is now kept, and a shift that would have to pass one is refused. The refusal is listed among mdl 1's splice refusals.
  • A restated if outweighs a new one whose condition differs. Pairing used to tie "the same if but for its condition" with "the same if with a branch edit", and broke the tie toward the earlier pair.
  • A splice making room leaves the flow's parameters where they are. One inserted if/else moved $Flag from (200, 53) to (539, 53), and describe then printed a parameter @position no script had stated.

ALTER PAGE: object-list items (#1453)

  • New: address a pluggable widget's object-list items, such as Accordion groups. This extends the navigation-list item address with a .slot suffix:
    insert into acc1 item@2 { … }                   -- the group's body slot
    insert into acc1 item('One') { … }              -- by its text
    insert into acc1 item(btnIn) { … }              -- the group holding a widget
    insert into acc1 item@1.headercontent { … }     -- another slot
    replace acc1 item@1.content with { … }
    drop acc1 item@1.headercontent
    drop acc1 item@2
    set (headerRenderMode: 'custom') on acc1 item@1
    An Accordion group stores no Name, so insert into grp1 { … } never resolved, and an empty group could not be filled at all.
  • An item's hidden slots and texts are kept as CREATE stores them. Measured with docker check --no-update-widgets, because the default check normalises widgets first and hid all of these:
    • Inserting into a slot the item's mode hides is refused at check and exec. It used to give CE0463.
    • Switching to custom nulls the hidden header template. Before, the template stayed filled and gave CE0463.
    • Switching back to text is refused unless the statement also sets headerText, in which case the template is created.
  • Shared rule: the body-slot rule moves to mdl/types.BodyItemSlotKey, which CREATE, describe and ALTER now share. This is a refactor and is its own commit.
  • Docs: the alter-page reference and skill, a regression script, a finding, and the proposal status set to partial.

check: view entity associations (#1611, part A)

  • New MDL-ASSOCDS02 refuses a page that follows a view entity's association from its target.
    • That path passed check --references and exec, then failed mx check with CE8812 "Entity 'M.Owner' has no association 'M.Stats_Owner'".
    • It was measured on 11.14 across list view, data grid, gallery and data view, and second in a path. Controls (view side, constrained query, persistent association reversed, microflow retrieve) give 0 errors.
  • A view entity created in the same script now registers its associations, so the working form DATABASE M.Stats WHERE [M.Stats_Owner = $currentObject] is no longer falsely refused.
  • Part B, the drop-down filter's defaultValue, is a separate PR.

Validation

  • make build, make test, make lint, make check-mdl and make check-findings pass.
  • Each fix has a control: the test fails with the reported symptom when the fix is reverted

ako and others added 19 commits October 21, 2026 10:38
`create or modify` pairing a new `if` and a restated one against the same
stored decision weighed "the same if but for its condition" exactly like
"the same if with a change in a branch", and lcsStatements breaks a tie
toward the earlier pair. So in

    if $Out then …; end if;          -- new, no @position
    @position(-299, 200)
    if $Flag then …; end if;         -- restated, branch edited

the new `if $Out` took over the stored decision (its condition set in
place) and the restated `if $Flag` was inserted as a new node at its stated
position, on top of it: two decisions at (-299, 200), both merges at
(186, 200).

The weights are doubled and a changed condition now weighs 1, below every
other partial match, so the restated if pairs with its stored decision and
the new one is an insert. Every other ratio is unchanged.

Part of #1508 case 6 (mendixlabs#1598).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `create or modify` that inserts statements without @position into a
flow whose later statements state theirs made room by moving everything
past the gap along. The stored flow then said something other than the
script: in #1508 case 6 `if $Flag` went -299 -> 40, its merge
186 -> 525 and the return 530 -> 869, while the calls replaced in its
branch — placed at their own stated positions, never shifted — stayed at
(-124, 100) and (60, 100), left of their decision. Re-running the script
was refused (the @merge no longer matched), and shifting "consistently"
cannot fix that: the next run must move the nodes back.

Under mdl 1 every point the script states a node at — @position,
@merge, @start, a note's position:, a parameter's @position; loop bodies
skipped, their coordinates are relative — is pinned on the mutator
(MicroflowMutator.Pin), and a make-room shift (an insert, or a replace
longer than what it replaces) that would move a pinned node is refused,
naming the room it needs and the node in the way, with the two ways out:
state @position on the new statements, or leave the positions after them
unstated so they can move along. Pinning is by point, so a node a placed
fragment puts at its stated position is pinned too.

mdl 0 is unchanged (ADR-0011): there the refusal would fall back to the
whole-flow rebuild, losing the merges, curves and $IDs the shift keeps —
TestFlowModify_InsertIsSpliced, ADR-0012's headerless describe+insert
acceptance test, caught exactly that.

Fixes #1508 case 6 (mendixlabs#1598).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
versions.md enumerates what `create or modify` refuses under mdl 1 where
mdl 0 rebuilds; add the insert that needs room past a node the script
places. The review command gains the pattern that #1508 case 6 hit: a new
splice refusal changes mdl 0 into a rebuild unless it is gated on the
language version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shift moved every top-level node past the cut, and a parameter is a
top-level object of the stored object collection: 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. mxcli's derived parameter row is absolute and
right of the start event, so almost every insert's cut falls left of it.

A parameter is not on the line the room is made on. Studio Pro authors
draw it above the start event — 63 of 71 in PedApp and JTSBootLogboek sit
at the start's x, none within 60px of the flow line — and an insert in
Studio Pro moves nothing. shifted() now leaves parameters out, and with it
pinnedPast, so a stated parameter position no longer blocks a splice it is
not in the way of.

Fixes #1547.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ters

fix: a splice making room leaves the flow's parameters where they are (#1547)
bodyItemSlotKey (content, else the first widget slot) decides where an
item's loose children go for CREATE and DESCRIBE. ALTER PAGE is about to
need the same answer for a bare item address (`acc1 item@1`), and the page
mutator cannot import the executor, so the rule moves to mdl/types beside
WidgetSlotKeyword. No behaviour change; the executor name stays as a thin
alias.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019httGxDh8UXt1Zoyh3Gh52
alterTarget's two `item` alternatives gain `(DOT itemSlot=identifierOrKeyword)?`.
The visitor carries the slot as WidgetRef.Column, the way `grid.controlbar`
carries a widget's slot, so every operation and the alter-target resolver
receive it as the dotted member they already understand.

The slot is taken off the identifiers before the selector is read:
`item@1.headercontent` has two identifiers, the shape `item(t1)` was
recognised by, and the first version of the test read it as
`item(headercontent)@1`.

PROPOSAL_alter_object_list_item_address.md step 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019httGxDh8UXt1Zoyh3Gh52
An Accordion group (any CustomWidgets$WidgetObject item) stores no Name,
so `insert into grp1 { … }` never resolved, and an empty group could not
be filled at all (#1326, mendixlabs#1453). Before:

  insert into acc1 item@2 { … }
  -> acc1 item@2: "acc1" is a CustomWidgets$CustomWidget, not a navigation list

The navigation-list item address now reaches object-list items too:

  insert into acc1 item@2 { … }               -- the item's body slot
  insert into acc1 item('One') { … }          -- by its text (headerText)
  insert into acc1 item(btnIn) { … }          -- the item holding a widget
  insert into acc1 item@2.headercontent { … } -- another slot
  replace / drop acc1 item@1.content          -- swap / empty a slot
  drop acc1 item@2                            -- remove the item
  set (headerText: 'First') on acc1 item@1    -- an item property

- resolveObjectListItem finds the widget's one object list (refusing,
  by name, a widget with several), matches by held widget, by
  TextTemplate/String text, or by position, and shares @n, the
  ambiguity and the miss messages with navigation lists (pickItem).
- The result carries the item schema, so SET reuses the column setter
  and `item.slot` reuses the pluggable-slot code from #1462.
- The body slot is types.BodyItemSlotKey, the slot CREATE and DESCRIBE
  use.
- BEFORE/AFTER an item, and REPLACE of a whole item with widgets, are
  refused with the form that works: adding a new item is not part of
  this change, and widgets written into an item list are the #935
  unloadable shape.

Control: the new tests fail on main with the refusal above. Fixed, on a
blank 11.13.0 app (Accordion inside a data view) the four statements
apply, describe shows each widget in its slot, the text box binds to
the data view's attribute, and mx check reports 0 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019httGxDh8UXt1Zoyh3Gh52
…om its target (CE8812, mendixlabs#1611)

A view entity's association, derived from an `<alias>.ID as <Name>` OQL
column, is one-way for a page. A data source that follows it from the
entity it points at passed check --references and exec. One example:
`DATABASE FROM $currentObject/M.Stats_Owner/M.Stats` on a page over
M.Owner. mx check then failed with CE8812 "Entity 'M.Owner' has no
association 'M.Stats_Owner'".

MDL-ASSOCDS02 (validate_view_assoc_reverse.go) runs in the
attribute-scope walk beside MDL-ASSOCDS01. It follows the association or
`database from` path hop by hop from the widget's context entity, and
reports the first hop that follows a view association from its TO end.
The hint is the query that builds: DATABASE <View> WHERE [<Assoc> =
$currentObject]. The association index now carries a view flag, from
Source == OqlViewAssociationSource.

A script's view associations were known to no checker, so that same
remedy was refused as naming "neither an attribute nor an association"
when the view entity came from the same script. This was true on a
page and in a microflow retrieve, both of which build at 0 errors.
scriptContext.recordViewAssociations now registers each view
association column the way a CREATE ASSOCIATION is registered.

Measured on 11.14.0, one page per shape, with uniquely named widgets:
- CE8812 for a list view, data grid, gallery and data view, in the
  database and in-memory forms, and as a second hop.
- 0 errors from the view side, for the constrained query, for a
  persistent association reversed, and for a microflow association
  retrieve.
- check now reports exactly the six refused shapes, and nothing else.
- The bug-test file gives 0 errors from check and from mxbuild. With
  its data source switched to the reverse: MDL-ASSOCDS02 from check,
  CE8812 from mxbuild.

Control: with checkViewAssocReverse unhooked, the 7 refusal cases fail
with no error reported. Before the registration fix, the remedy test
failed on both the page and the microflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxAn3VqxUnZ7ZGUnzAYpbT
…n-String literal (CE0117, mendixlabs#1611)

`column (Attribute: IsActive) { dropdownfilter ddf (defaultValue:
'true') }` passed check and exec, then mx check failed with CE0117
"Error(s) in expression.". The filter's defaultValue is an Expression
property that returns a String. A quoted MDL value on an expression
property is the expression's text, so 'true' stored the Boolean
expression `true`. The writer follows the convention that describe
prints and every expression property relies on. What was missing is a
check of the expression against the property's declared return type.

MDL-WIDGET49 (validate_widget_expression_return.go) runs beside
MDL-WIDGET42. On a String-returning Expression property it refuses a
value that is a literal Boolean, Integer or Decimal
(exprcheck.InferSourceKind), or a single bare name. The hint gives the
String literal: `defaultValue: '''true'''`. The embedded schema now
records each Expression property's ReturnType. A value that infers no
kind is left alone (an attribute path, a variable, an untyped
function), and so is DateTime, which the inference gets wrong. It runs
with and without a project.

Measured on 11.14.0, one filter per value. CE0117: 'true', 'false',
'1' (in a String and in an Integer column), '1.5' and 'abc'. Builds at
0 errors: '''true''', '''abc''', '''tru''', 'empty' and
'''a'' + ''b'''. check now reports exactly the six refused values. The
bug-test file gives 0 errors from check and from mxbuild as written;
with defaultValue 'true', MDL-WIDGET49 from check and CE0117 from
mxbuild.

Control: with the check unhooked, the 6 refusal cases fail with no
error reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxAn3VqxUnZ7ZGUnzAYpbT
…es them

Measured on a blank 11.13.0 app, project checked as stored
(`docker check --no-update-widgets`; the normalising default hides all
of these):

1. `insert into acc1 item@1.headercontent { … }` on a group whose
   headerRenderMode is 'text' (the default) stored widgets the Accordion
   hides: CE0463 at the Accordion. It is now refused, at check and at
   exec, naming the property that shows the slot. A SET earlier in the
   same statement can be what shows it, so check stands down then and
   exec decides on the values after the SET.
2. `set (headerRenderMode: 'custom') on acc1 item@1` hid headerText and
   left its template filled: CE0463. CREATE stores a hidden template
   null (#1198), so the post-SET reset now nulls it.
3. `set (headerRenderMode: 'text')` on a group made in custom mode
   revealed a null template: CE0463, and an empty one is CE4899. The
   'Header' text CREATE writes lives in the widget package, not the
   stored schema, so such a SET is refused unless it also sets
   headerText; `set (headerRenderMode: 'text', headerText: 'Two')`
   creates the template (Deps.SerializeClientTemplate, codec-built) and
   writes the text.

The reset is ResetHiddenColumnProperties' pass, generalised to items;
templates are handled for items only, since the column case is not
measured. check's dry run now applies an item SET as a whole, as it does
for a column's.

Control: each new test fails before its change with the measured symptom;
on the app, scenarios 1-3 give 1 error before and 0 after (3 by refusal,
the project left unchanged), and set-then-insert gives 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019httGxDh8UXt1Zoyh3Gh52
- alter-page reference and skill: `acc1 item@n` / `item('text')` /
  `item(widget)` / `.headercontent`, the hidden-slot refusal and its fix,
  and what is not supported (adding a new item).
- mdl-examples/bug-tests/1453-alter-accordion-group-item-address.mdl:
  exec on a blank 11.13.0 app puts each widget in its slot and
  `docker check --no-update-widgets` reports 0 errors.
- Proposal status -> partial, with what is done and what is not;
  proposals index regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019httGxDh8UXt1Zoyh3Gh52
…line budget

TestLargeSkillsWereSplit refused the skill at 703 body lines after the
item-address paragraph; it is now three lines, with the detail in the
alter-page reference. The test-stub methods are gofmt-clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019httGxDh8UXt1Zoyh3Gh52
fix: create or modify does not move a node the script places (#1508 case 6) — re-land of #1546
feat(alter page): address Accordion groups and other object-list items
fix(executor): refuse a page following a view entity's association from its target (CE8812, mendixlabs#1611)
fix(executor): refuse a String-returning widget expression given a non-String literal (CE0117, mendixlabs#1611)
@ako
ako merged commit 3812b35 into mendixlabs:main Oct 22, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Microflow canvas layout: six cases where mxcli writes positions, sides or splices that Studio Pro would draw differently or overlap

2 participants