Skip to content

Commit 2be9331

Browse files
authored
Merge pull request #1660 from ako/main
Sync ako/mxcli: loop-body splices and #1517 decisions, workflow texts and timers, Java action round trips, input widget drops
2 parents 9eeb6e0 + 9bdf723 commit 2be9331

283 files changed

Lines changed: 16215 additions & 776 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area": "mdl/backend", "date": "2026-10-26", "symptom": "A user task created with `targeting groups xpath [Name = 'Reviewers']` was stored correctly (`UserTargeting: Workflows$XPathGroupTargeting`), but `describe workflow` printed it with no targeting clause, so describe -> create or modify silently removed the targeting. Users xpath, users microflow and groups microflow on the same workflow described fine", "cause": "userSourceFromGen, the reader's switch from the polymorphic UserSource/UserTargeting part to sdk/workflows, had a case for every targeting $Type the writer emits except Workflows$XPathGroupTargeting, which fell through to NoUserSource. The writer (userTargetingToGen) and the describe formatter (the XPathGroupSource case in cmd_workflows.go) both handled it, so reading the formatter and the writer showed nothing wrong", "file": "`mdl/backend/modelsdk/workflow_read.go` (userSourceFromGen)", "insight": "When a describe clause is missing but the formatter has a case for it, the reader is the suspect: a missing case in a type switch on decoded gen types defaults silently. A table test that round-trips EVERY value the writer can emit through encode -> decode -> read catches the whole class, not just the reported kind; the per-kind tests that existed covered only the kinds someone had happened to describe. The executor's `TestRoundtripWorkflow_XPathTargeting` (integration) replays the describe output and compares, with the users-xpath task as the control", "fix": "Add the `*genWf.XPathGroupTargeting` -> `&workflows.XPathGroupSource{XPath: s.XPathConstraint()}` case; table test over all five targeting kinds, single and multi user task", "refs": ["ako/mxcli#1722"], "ce": []}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area": "mdl/executor", "date": "2026-10-25", "symptom": "`alter microflow … insert` with `@annotation(id: n1)` meant to attach the new activity to a note already in the flow was refused at exec — '@annotation(id: n1) refers to an annotation that has not been declared — the first mention must carry the text' — while check passed it as 'not predicted'; there was no other way to attach a fragment statement to a stored note", "cause": "A fragment is built by a fresh flowBuilder whose annotationsByLabel starts empty, and the `id:` labels describe prints (n1, n2, …) are re-derived on every describe and stored nowhere, so there is nothing for a fragment to resolve them against. check only learned of the builder's error in alterFlowViolations' pre-build loop, which files builder errors as dependencies ('not predicted') rather than refusals", "file": "mdl/executor/cmd_alter_flow_stored_notes.go (resolveStoredNotes, attachStoredNote, takeStoredNoteLines); mdl/backend/mfmutator/note_link.go (LinkNote); mdl/visitor/visitor_microflow_statements.go (parseNoteAnnotation `existing:`)", "fix": "New key `@annotation(existing: 'text')`: resolveStoredNotes (called by plan for exec and before the pre-build loop for check) resolves each text to exactly one stored note or refuses, listing the candidates with position and attachments; the builder links the stored note's ID; the lines are taken out of the fragment and drawn by MicroflowMutator.LinkNote after the splice, oriented with microflows.FacingSides (moved there from the executor so builder and splice share one formula) on the post-splice geometry. ValidateAlterFlow now also runs checkAnnotationLabels per fragment, so an undeclared `id:` is refused by check (MDL079) with a hint at `existing:`", "insight": "Two traps beyond the obvious. (1) Any refusal that lives in the fragment BUILDER is downgraded by check to 'not predicted — exec decides' (alterFlowViolations treats builder errors as dependencies on earlier statements), so a deterministic refusal must be decided before the build, by a function both plan and alterFlowViolations call. (2) A line from a stored note cannot ride in the fragment: the builder never knew where the stored note is relative to what it builds (sides come out as the bottom→top default), withoutNotes/singleActivity treat a line to a non-fragment object as a non-note so the error-handler and loop-body in-place replaces refuse, and Replace carries the target's own line to the entry so the same note gets two lines. Drawing it after the splice via a mutator op that dedupes and orients on current geometry fixes all three; each was shown by a revert (lines left in the fragment: origin 2/destination 0 instead of 0/2, and 2 lines on replace). Address by text, not by the describe-time id: the id is a derived name that moves (docs-wiki/bug-patterns/mutator-addressing.md); text is what the canvas shows, and the miss/ambiguity message lists the notes so a mismatch is visible", "refs": ["ako/mxcli#1695", "mendixlabs/mxcli#1517", "#1077", "mendixlabs/mxcli#1598", "mendixlabs/mxcli#1505"], "test": "TestAlterMicroflow_AttachesAStoredNoteByItsText, TestAlterMicroflow_StatedNoteTextIsANewNote (control), TestAlterMicroflow_StoredNoteLineFacesTheNote, TestAlterMicroflow_ReplaceAttachedToTheReplacedNote, TestAlterMicroflow_StoredNoteTextMustNameOneNote, TestAlterMicroflow_ExistingNoteMisuseRefused, TestAlterMicroflow_FragmentNoteLabelsUnchanged (mdl/executor/cmd_alter_flow_stored_notes_1695_pedapp_test.go); TestLinkNote_* (mdl/backend/mfmutator/note_link_test.go); TestExistingNoteAnnotation (mdl/visitor/visitor_existing_note_test.go)"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area": "mdl/executor", "date": "2026-10-25", "symptom": "After `alter microflow … insert after $X begin if … end if; end` spliced a decision in, re-running the original `create or modify` (mdl 1) was refused — 'cannot drop the ExclusiveSplit … only an activity or a loop can be, because a decision or an end event changes the shape of the flow' — and so were `alter … drop '<caption>'` and `alter … replace '<caption>' with …`, so the script that created the flow could not be re-run", "cause": "The mutator's Drop/Replace went through removable(), which accepts an activity or a loop only: a decision is several nodes (split, path activities, merge) and the splice had no notion of taking a subgraph out; the statement diff already produced the right Drop/Replace op for the stored `if`", "file": "mdl/backend/mfmutator/splice_decision.go (insertedDecision, dropDecision, replaceDecision, pathNotes, DecisionNodes); mdl/executor/cmd_alter_flow.go (takenOut: path activities' outputs judged like the target's)", "fix": "Drop/Replace of a split validate it has exactly the shape `alter … insert` produces — two paths of plain activities (or none), each entered only by its predecessor, no error handler, rejoining at one merge entered by those two paths alone and leading on to one node — then rewire the split's incoming flows to the merge's successor (drop) or the fragment (replace) and take out the split, path activities, merge and the flows between them; notes attached only to path nodes go too. The shape check is the one verdict exec and check -p share (both run the mutator), and every out-of-shape decision is refused with the part that is out of reach ('its true path ends in a return', '… holds a decision', '… has an error handler', '3 flows join at its merge', 'it has 3 paths')", "insight": "No executor diff change was needed: the LCS already emitted Drop of the stored `if` (and Replace for a run starting at it), so the whole refusal lived in one mutator guard — check the verb the diff emits before designing a new op. Two traps for tests: the path activities' outputs must be judged like the target's (a variable a path declares and the flow reads after the merge), or the drop leaves a dangling reader; and a 'rebuild re-mints every $ID' control does not work inside one executor session — a flow dropped and created again there gets its stored identities back — so the churn control is a replaced activity (1 gone, 1 added)", "refs": ["ako/mxcli#1694", "ako/mxcli#1399", "mendixlabs/mxcli#1517"], "test": "TestSpliceDecision1694_* (mdl/executor/flow_splice_decision_1694_pedapp_test.go); TestSpliceDecision_* (mdl/backend/mfmutator/splice_decision_test.go)"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area": "mdl/executor", "date": "2026-10-25", "symptom": "`create or modify microflow` (mdl 1) and `alter microflow` refused any change of a loop body's shape — an activity inserted, dropped or moved inside `loop … end loop` — with 'the Loop at (x, y) changes inside its body; the splice does not edit inside a loop', and named `alter … replace loop`, which re-minted every element ID in the loop", "cause": "A loop body's nodes are drawn in the loop box's coordinates, so the splice had no layout for them: graph.node() refused every body node and only an in-place one-for-one replace (#1106) was allowed; the diff paired a loop only when its body had the same shape (editableLoopBody)", "file": "mdl/backend/mfmutator/splice.go (spliceOnFlow/makeRoom scoped to a loop, appendInLoop, prependInLoop, dropInLoop, replaceInLoop, fitLoopTo, growLoop, ReplaceLoopBody); mdl/executor/cmd_flow_modify.go (inLoopBody, bodyGap, bodyRefusal); mdl/executor/cmd_alter_flow.go (inLoopRefusal, inBodyTarget, notPlainFragment); mdl/executor/flow_splice_loop_body.go (replaceBody)", "fix": "Shift/room/pin are scoped to a collection (the top level or one loop's body); an edited body is fitted back into its box, which grows on its far side along the line the loop sits on (from the side its incoming flow enters) with what is past it moving along, and symmetrically across it so the flows in and out stay level, recursing into an enclosing loop. The diff runs inLoopBody as an LCS over the body, plain activities only; a decision/loop/return/break/continue/raise/handler is refused naming the new `alter … replace body of <loop>`, which keeps the loop's $ID, position, iterator and list and rebuilds only its contents (the loop restated with its stored header is built, and its body taken)", "insight": "Three facts settled the design and are cheap to re-measure: body flows live in the unit's top-level Flows list, not the loop; an empty loop body passes mx check (0 errors on 11.13), so dropping a body's only activity needs no refusal — but mxcli stores an empty body without its Objects key, so a writer into it must add the list (marker 3); and `create` itself draws a tall nested loop over the parameter row, so a room check that counts parameters refuses layouts the builder makes. A body restated from describe states every @position, so body positions have to be pinned (PinInLoop) like top-level ones or the second run of a spliced script reads its own shifted nodes as 'moved'", "refs": ["ako/mxcli#1693", "ako/mxcli#1399", "mendixlabs/mxcli#1517", "ako/mxcli#1106", "mendixlabs/mxcli#1598"], "test": "TestSpliceLoopBody1693_* , TestAlterReplaceBody1693 (mdl/executor/flow_splice_loop_body_1693_pedapp_test.go); TestSpliceLoop_* (mdl/backend/mfmutator/splice_loop_test.go)"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area": "mdl/executor", "date": "2026-10-26", "symptom": "`alter page … set (CustomDateFormat: '') on dp` on a date picker already DateFormat Custom reported Altered page and wrote Custom with no pattern; mx check: CE0493 \"Date format is custom but no format string is specified.\" (measured on 11.13.0 with the pre-fix binary)", "cause": "The only guard was inside the DateFormat setter (refuse Custom when the stored pattern is blank), relying on properties being applied in name order. A statement that only cleared the pattern never ran it, and a per-property check on CustomDateFormat would wrongly refuse `set (CustomDateFormat: '', DateFormat: Date)`, whose result is valid", "file": "mdl/executor/cmd_alter_page.go (checkDateFormatAfterSet), mdl/backend/pagemutator/mutator.go (CheckDateFormat)", "insight": "A rule that relates two properties is a rule about the widget the statement leaves behind, so check it after the whole SET, not inside either setter — the per-property guard is order-dependent in one direction and over-strict in the other. applySetPropertyMutator already had the post-statement seam (resetHiddenColumnProperties); an optional mutator capability keeps the MCP backend untouched. Scope the check to statements that name one of the two keys, so an unrelated edit to a widget already in the bad state is not blocked. The control that matters is a valid two-property statement in the same test", "refs": ["ako/mxcli#977"], "ce": ["CE0493"]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area":"mdl/executor","date":"2026-10-26","symptom":"`check -p --references` passed `$r = call microflow AddOn.Request_GetFirst(); declare $s String = $r/Secret;` in an app module while Secret was a Hidden attribute of a Protected (add-on) module; exec wrote it and mxbuild refused CE1406 \"Create variable activity 'Create String variable' is not allowed to use attribute 'AddOn.Request.Secret' because app modules cannot reference hidden elements in add-on modules\". The same read through a parameter was reported (ako/mxcli#1701, mendixlabs/mxcli#1654).","cause":"walkTypedFlow — the one variable-typing walk the --references member checks share — typed parameters, create, retrieve and loop iterators but not the output variable of `call microflow`, so `$r/Secret` had no entity and pathMembers(\"\") returned nothing; the member-existence check (checkFlowMembers in validate_member_refs.go) was blind on call results the same way.","fix":"walkTypedFlow takes the scriptContext and types a call's output from the callee's object/list return type via scriptContext.microflowReturnEntity: the script's own definition first (sc.microflows + sc.flowParams), else the stored microflow read by name (microflowsNamed + hierarchy module match), cached per validation pass. validateProtectedModuleRefs now receives the sc validateProgram already builds.","file":"mdl/executor/validate_member_refs.go","insight":"Typing a variable is where a member check's reach is decided — a member check is only as wide as the walk that types its variables, and it fails silent, so widening the walk is the fix, not another check. But a type is a claim: typing from the STORED callee when the script rewrites it with another return type would judge members against the old entity and refuse a correct script (proved by stubbing the script-first branch: the existence check then reported 'Shop.Order has no member Name' for a callee rewritten to return Shop.Customer). So the script's signature must win, and an unresolvable callee must leave the variable untyped. Measured on 11.13.0 (module set Protected via UpdateModuleSettings, one microflow per shape): declare, end event, change on a call result and change on a loop over a returned list all CE1406; the Usable attribute clean.","refs":["ako/mxcli#1701","mendixlabs/mxcli#1654","mendixlabs/mxcli#1498"],"ce":["CE1406","CE1613"]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area": "mdl/executor", "date": "2026-10-26", "symptom": "A data view's microflow data source stored with FormValidations None (ako/TestApp Pages.OutputMappingButton dataView1, Forms$MicroflowSource.MicroflowSettings) was rewritten as All on describe → exec: Replaced page, the only difference on the page", "cause": "The data-source writer called microflowSettingsToGen with nil settings, which writes Mendix's defaults, and neither the grammar nor describe had a place for a data source's call settings", "file": "mdl/grammar/domains/MDLPage.g4 (dataSourceExprV3 MICROFLOW … actionSettingsV3?), mdl/visitor/visitor_page_v3.go + visitor_action_settings.go (data source takes FormValidations only), sdk/pages/pages_datasources.go (MicroflowSource.FormValidations), mdl/executor/cmd_pages_builder_v3.go + cmd_alter_page.go, mdl/backend/modelsdk/widget_write.go (3 microflow-source sites), mdl/executor/cmd_pages_describe_datasource.go, cmd_fragments.go; test mdl/executor/datasource_formvalidations_1729_test.go", "insight": "Tally every field of the shared element under each parent before choosing a syntax: Forms$MicroflowSettings under a data source held non-default values in FormValidations only (44 TestApp + 4 PedApp sources, every other setting default), so the data source reuses the action's `with ( … )` rule with one key allowed rather than a new property. Before the grammar change `DataSource: microflow M.F with (…)` parsed without error as the deprecated `microflow M.F` ACTION form and left the widget with no source — an alternative that fails in one rule can succeed in a fallback rule, so a parse test with no errors proves nothing about which rule took it", "refs": ["ako/mxcli#1729"]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"area": "mdl/executor", "date": "2026-10-26", "symptom": "describe fragment printed `call microflow M.F` for `call microflow M.F(Order = $currentObject)` — flow and page arguments dropped, as were a microflow/nanoflow data source's arguments, `create object E then show page P` (printed `create`), `open link …` (printed `openLink`), a fragment action parameter (printed `param`), the fragment's own `($p: datasource, $q: action)` header, and a doubled quote in a string property; the output was not the fragment `use fragment` expands, and with a quote in it did not parse", "cause": "formatActionV3 / formatDataSourceV3 / formatASTPropertyValue in mdl/executor/cmd_fragments.go were written for the bare action kinds and fell through to the AST type name for the rest; describeFragment never printed Params", "file": "mdl/executor/cmd_fragments.go (describeFragment header, formatFlowArgsV3, formatActionV3, formatDataSourceV3, string quoting via quoteExpressionLiteral); test mdl/executor/cmd_fragments_describe_args_test.go", "insight": "For an AST printer the test that finds every gap at once is 'parse the output and compare the AST with what was parsed in', not a list of expected substrings: one round-trip test over a fragment using each action kind exposed seven drops in one run, where the original report named only the arguments. A `default: return a.Type` in a printer is a silent drop waiting for the next kind", "refs": ["ako/mxcli#1731"]}

0 commit comments

Comments
 (0)