Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/skills/fix-issue/findings/mdl-executor.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -496,4 +496,5 @@
{"area": "mdl/executor", "date": "2026-08-31", "symptom": "`main` goes red on a test that passed in **both** PRs that touched it — here `TestDescribeWorkflow_NoAnnotationEmitsNoComment`: `unexpected violations [MDL-WF05] for a plain jump`", "cause": "Two PRs merged in sequence. One added a validator rule (MDL-WF05, dangling `jump to` target); the other's control test asserted `len(violations) == 0` over a fixture that was **not a valid workflow** — a lone jump whose target did not exist. Each CI run was green because neither saw the other's change", "file": "`mdl/executor/issue1007_annotation_emit_test.go` (the fixture), `mdl/executor/validate_workflow_jump.go` (the rule, which is right)", "insight": "**A test fixture that is not a valid instance of the thing under test is a landmine for the next rule.** \"No violations at all\" is only meaningful over input that *should* have none; over an invalid fixture it silently asserts \"no rule has been written yet that notices this\". Fix the **fixture**, not the rule. Generalise: when a test asserts the ABSENCE of diagnostics, make the input something you would be happy to ship. Two green PRs can still merge to red and **neither PR's CI can detect it** — the only protection is a fixture that does not depend on which rules exist today. Found by running `make test` on an unrelated docs branch cut from the merged main, which is an argument for doing that on any branch cut after a batch merge.", "refs": ["#350", "#351"], "rules": ["MDL-WF04", "MDL-WF05"]}
{"area": "mdl/executor", "date": "2026-08-31", "symptom": "`filter($L, Amount > 0)` — or any FILTER/FIND predicate with a bare attribute and an operator other than `=` — fails the build with CE0117 \"Error(s) in expression.\", while the same attribute with `=` builds fine. `mxcli check` and `check --references` both pass. Reported as a Mendix 11.13 regression.", "cause": "The unfinished half of bug #343. Mendix has two filter operations: `Microflows$Filter` takes a member NAME (filter by attribute) and `Microflows$FilterByExpression` takes an expression evaluated per item with the item bound to `$currentObject`. #343 rerouted only `attr = value` to the by-attribute form; every other predicate still fell through to the expression form, where mxcli stored the authored text verbatim — and a bare attribute is not a valid Mendix expression. So the split was on the OPERATOR and invisible to the author: `Status = 'x'` built, `Status != 'x'` did not.", "file": "`mdl/executor/cmd_microflows_builder_actions.go` (`qualifyIteratorAttributes`, `listElementEntity`, `iteratorMemberPath`, `qualifyNamesInSource`); `mdl/executor/validate_microflow_listop_iterator.go` (MDL-LISTOP01); syntax topic `cmd/mxcli/syntax/features_microflow.go`; repros `mdl-examples/bug-tests/1002-filter-find-bare-attribute.mdl` and `1002-filter-bad-iterator.fail.mdl`.", "insight": "Rewrite a bare name that PROVABLY resolves to a member of the list's element entity into `$currentObject/<member>`; refuse one that does not resolve (it used to surface as CE0117 at build time); pass through untouched when the element entity cannot be determined, which proves nothing either way. An association takes its module qualifier in the path, an attribute does not. The predicate is a frozen `SourceExpr` (`buildSourceExpression` in the visitor), so the rewrite has to patch the source TEXT, skipping single-quoted literals — `filter($L, Qty > 0 and Status != 'Amount')` must not rewrite the `'Amount'` inside the literal. Do not blame the Mendix version without running both: the issue reported \"reproduces on 11.13.0, not 11.11.0\" and the identical 7-error probe on mxbuild 11.11.0 and 11.13.0 disproves it — the fork is in mxcli, and nothing in it depends on the version. `mx check -j <file>` is the attribution tool: the console output names only the activity (\"List operation activity 'Filter by expression'\"), so two errors on one line read as one microflow; the JSON carries `document-name` per location. MDL-LISTOP01 keys on scope, not on the name — `$item` is valid in a predicate when it is the enclosing loop's iterator, which is exactly how CLAUDE.md's O(N) `find` idiom is written, so flagging the name would break the documented pattern. Control for the whole fix: stub `qualifyIteratorAttributes` to return the condition unchanged and the repro goes 0 → 7 x CE0117 with the two `=` cases staying green, which is also the #343 regression guard.", "refs": ["#1002", "#343"], "ce": ["CE0117", "CE0109"], "rules": ["MDL-LISTOP01"]}
{"area": "mdl/executor", "date": "2026-08-31", "symptom": "A copied Atlas layout (or any page mxcli authors) fails `mx check` with **CE0463** on its Image widget, and a full field-level diff against Studio Pro's own widget differs in exactly **one line of 1480** — `maxHeight`, mxcli's `0` against the installed package's declared default", "cause": "Same class as the §69 width/height fix, which did not cover it — and why is the point. The reset was applied in a loop over the definition's **property mappings**, and a mapping is what gives a property an MDL keyword: `width`/`height` have one, `maxHeight` has none, so it was never visited and the widget TEMPLATE's captured value stood. Two further halves, each independently load-bearing (stub either and the test fails): a rule whose CONDITION is an unmapped property (`maxHeight` is hidden when `maxHeightUnit` = \"none\") was always indeterminable so never fired — the declared default is the right fallback there, because nothing can have moved an unnamable property off it; and `def.PropertyVisibility` is **empty** for every widget whose rules are lifted live from the `.mpk`, which is most of them, so a lookup keyed on that field silently finds nothing (both consumers now share `visibilityRules()`).", "file": "`mdl/executor/widget_engine.go` (`unmappedHiddenResets`, `visibilityRules`, the condition fallback in `hiddenUnnamedProperties`)", "insight": "The general rule: **the set of properties that must be default-valued is the widget's editorConfig to decide, not mxcli's** — making it a subset of what MDL has words for was the mistake. The third trap was found only end-to-end; the unit test passed while the command still wrote the wrong value. Control, end-to-end on a real 11.13 project with the Image `.mpk` patched to declare `maxHeight` 250 (the 1.6.0 value the finding measured, since that version was not obtainable): pre-fix binary writes 0 and all four mxcli-authored Images fail CE0463; fixed binary writes 250 and all four are clean, while `minHeight` stays at its own declared 0 — per-property from the package, not a blanket value. The project's ~60 Studio Pro-authored Images stay stale under both, which is the package change itself and not mxcli: the Step 0 discrimination diagnose-ce0463.md asks for. Reported as mxcli-ledger FINDINGS §142."}
{"area": "mdl/executor", "date": "2026-09-01", "symptom": "A hand-placed microflow/nanoflow/rule parameter is silently moved onto a grid at 200;53, 300;53, \u2026 by any rewrite \u2014 including a describe \u2192 exec of mxcli's OWN output. Reported as a feature request for `@position` on a parameter; the missing feature and the silent loss are the same defect seen from two sides.", "cause": "Microflows$MicroflowParameter is a stored node with RelativeMiddlePoint + Size, but the semantic type had no position field, so NEITHER reader carried one and BOTH writers could only recompute `200+idx*100;53` inline. The grammar had no slot for the annotation either, so there was no way to state a position and no way to preserve one.", "file": "sdk/microflows/microflows.go (Position + DerivedParameterPosition/AuthoredParameterPosition), mdl/grammar/domains/MDLMicroflow.g4 (annotation* on microflowParameter), mdl/visitor/visitor_microflow.go, mdl/backend/modelsdk/microflow.go + microflow_write.go, sdk/mpr/parser_microflow.go + writer_microflow.go, mdl/executor/cmd_microflows_parameter_position.go", "insight": "The design was already litigated one node family over and should be COPIED, not re-derived: @start's authoredStartPosition (#884 + #951) settles that a node at the layout's own derived spot carries no intent and must be re-derived, while one anywhere else was placed by a person and must survive. Carrying stored coordinates over unconditionally is the trap \u2014 inserting a parameter would strand the existing ones on the old grid while the new one lands on top. Put the arbitration in the READER so a non-nil Position means intent everywhere downstream; Position must be a POINTER because 0;0 is a coordinate a person can choose (two flows in the reference project use it). Measurement that framed the work: 20 of 28 parameters in a real 1971-unit project sit off the derived grid, so nearly every rewrite moved one. Trap when measuring: `Unchanged` on a SECOND exec proves the round trip reaches a fixed point, NOT that the first write changed only the thing you are looking at \u2014 this document also loses ExportLevel and a DestinationControlVector, unrelated and still open. Diff the raw unit BSON sorted by path, since the rebuild reorders ObjectCollection.Objects and a line-diff is then all noise.", "refs": ["ako/mxcli#993", "#951", "#884"], "rules": ["MDL059"]}
{"area": "mdl/executor", "date": "2026-08-29", "symptom": "A mapping sourced from an imported web service (SOAP) loses its binding on `create or replace|modify`: ImportedWebService and RootElementName are removed, ServiceName and OperationName blanked, and mxbuild reports CE6896 \"A mapping must have exactly one schema source\" + CE0270 \"No root element could be found in the schema\". `describe` emits the mapping with NO source clause, so describe -> exec — how a document is copied — is what destroys it.", "cause": "A mapping has FOUR source kinds, not three: JSON structure, XML schema, message definition, and ImportedWebService (stored key ImportedWebService, SDK name wsdlFile) with ServiceName/OperationName/RootElementName qualifying it — plus ParameterName/IsHeader on export mappings. model.ImportMapping carried three and a comment saying \"Schema source (at most one is set)\", so neither engine read the fourth and every rebuild dropped it. modelsdk/gen already exposed all the accessors; nothing called them.", "file": "model/types.go (WebServiceMappingSource), sdk/mpr/parser_import_mapping.go + parser_export_mapping.go (parseWebServiceSource), mdl/backend/modelsdk/mapping_read.go, mdl/executor/validate_webservice_mapping.go", "insight": "**Guard-don't-drop, and REFUSE rather than preserve** (ADR-0005, same class as the queued-call guard). Carrying the binding through a rebuild would imply the rest of the document survives too, and mxcli cannot check that: a SOAP mapping's elements resolve against the WSDL's schema entries, which live INLINE on WebServices$ImportedWebService.WsdlDescription.SchemaEntries and are never standalone XmlSchemas$XmlSchema documents. That inline detail also means improving `with xml schema` support does nothing for SOAP — a natural but wrong assumption. **The corpus cannot tell you SOAP is rare**: 0 web services across the 9 demo apps, but they are modern AI/factory demos, so that is evidence about the sample, not the population; legacy estates are exactly where this bites. To get a reference document, PLANT one (set the four keys on a real unit via UpdateRawUnit) — and replace keys IN PLACE, because appending duplicates makes the two engines disagree (bson.Raw.LookupErr takes the FIRST occurrence, bson.Unmarshal into a map takes the LAST), which looks exactly like an engine bug and is not. `describe` marks the source as NOT REPRESENTABLE in a comment rather than emitting nothing: the silent output parses, and re-executing it is the deletion.", "refs": ["ako/mxcli#365"], "ce": ["CE6896", "CE0270"]}
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,14 @@ commit $Product;
- `@position` always appears in DESCRIBE output; `@caption` only when custom; `@color` only when not Default
- DESCRIBE MICROFLOW shows `@` annotations before their activities
- `@start(x, y)` positions the **start event** and goes on the first statement, because the start has no statement of its own. Omit it and the start is derived — one spacing unit (160) left of the first activity, on its centre line — and a rewrite re-derives it so the start follows the activities when they move. A start that is not at the derived spot was placed by hand (in Studio Pro or with `@start`): it survives a rewrite that does not mention it, and DESCRIBE emits `@start` for it. An explicit `@start` overrides both (#951)
- `@position(x, y)` on a **parameter** goes inside the parameter list, ahead of the parameter it places — a parameter is a stored node with its own coordinates, and this is the only annotation it takes. Omit it and the parameters form a row along the top of the canvas (200;53, 300;53, …). The `@start` rule above applies unchanged: a parameter on that derived row is re-derived on a rewrite, one anywhere else was placed by hand, survives, and is emitted by DESCRIBE (#993). Before this, a hand-aligned parameter block was moved back onto the row by any rewrite — including a describe → exec of mxcli's own output:

```
create or modify nanoflow MyModule.ACT_Clear (
@position(-77, 0)
$Feedback: MyModule.Feedback
)
```
## Error Handling

MDL supports error handling for activities that may fail (microflow calls, commits, external service calls, etc.).
Expand Down
12 changes: 12 additions & 0 deletions cmd/mxcli/lsp_diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,18 @@ func (s *mdlServer) runSemanticValidation(text string) []protocol.Diagnostic {
}
if mfStmt, ok := stmt.(*ast.CreateMicroflowStmt); ok {
violations = append(violations, executor.ValidateMicroflow(mfStmt)...)
violations = append(violations, executor.ValidateFlowParameterAnnotations(
"microflow '"+mfStmt.Name.String()+"'", mfStmt.Parameters)...)
}
// The editor reports an unusable parameter annotation for the same
// reason `check` does — a typo of @position parses and does nothing.
if nfStmt, ok := stmt.(*ast.CreateNanoflowStmt); ok {
violations = append(violations, executor.ValidateFlowParameterAnnotations(
"nanoflow '"+nfStmt.Name.String()+"'", nfStmt.Parameters)...)
}
if ruleStmt, ok := stmt.(*ast.CreateRuleStmt); ok {
violations = append(violations, executor.ValidateFlowParameterAnnotations(
"rule '"+ruleStmt.Name.String()+"'", ruleStmt.Parameters)...)
}
if setStmt, ok := stmt.(*ast.AlterSettingsStmt); ok {
violations = append(violations, executor.ValidateSettings(setStmt)...)
Expand Down
12 changes: 10 additions & 2 deletions cmd/mxcli/syntax/features_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ func init() {
"annotation", "caption", "color", "excluded", "bezier",
},
Syntax: "@position(x, y) -- the activity's centre point\n" +
"@position(x, y) -- also on a PARAMETER, in the ( … ) list\n" +
"@start(x, y) -- the start event, on the FIRST statement\n" +
"@anchor(from: right, to: left) -- which SIDE each end of the outgoing flow attaches to\n" +
"@curve(from: (40, -90), to: (-40, 90)) -- the flow's bezier control vectors\n" +
Expand All @@ -288,8 +289,15 @@ func init() {
"start is placed one spacing unit left of the first activity, on its centre\n" +
"line — and a rewrite MOVES it to follow the activities. A start that is not\n" +
"at that derived spot was put there by hand: it survives a rewrite, and\n" +
"DESCRIBE emits @start for it so the description round-trips exactly.",
Example: "create microflow MyModule.ACT_Flow ($In: String)\nreturns String as $Out\nbegin\n" +
"DESCRIBE emits @start for it so the description round-trips exactly.\n\n" +
"A PARAMETER is a stored node with its own coordinates, so it takes\n" +
"@position too — written inside the parameter list, ahead of the parameter\n" +
"it places. It is the only annotation a parameter takes. Omit it and the\n" +
"parameters form a row along the top of the canvas at 200;53, 300;53, … ;\n" +
"the same derived/authored rule as @start then applies, so a parameter on\n" +
"that row is re-derived and one anywhere else survives a rewrite and is\n" +
"emitted by DESCRIBE.",
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, to: top)\n" +
" @curve(from: (40, -90), to: (-40, 90))\n declare $Tmp String = $In;\n" +
" @position(200, 300)\n declare $Out String = $Tmp;\n return $Out;\nend;",
Expand Down
1 change: 1 addition & 0 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ it is for pages.
| Validation | `validation feedback $entity/attribute message 'message';` | Requires attribute path + MESSAGE |
| Log | `log info\|warning\|error [node 'name'] 'message';` | |
| Position | `@position(x, y)` | Canvas position (before activity) |
| Parameter position | `@position(x, y)` before a parameter, **inside** the `( … )` list | The only annotation a parameter takes. Omit it and parameters form a row at 200;53, 300;53, …; a parameter off that row is treated as hand-placed, survives a rewrite, and is emitted by DESCRIBE (#993) |
| Start event | `@start(x, y)` | Canvas position of the start, on the **first** statement. Omit it and the start is placed one spacing unit left of the first activity and MOVES with it on a rewrite; a start that is not at that derived spot is treated as hand-placed, survives a rewrite, and is emitted by DESCRIBE (#951) |
| Caption | `@caption 'text'` | Custom caption (before activity) |
| Color | `@color Green` | Background color (before activity) |
Expand Down
65 changes: 65 additions & 0 deletions mdl-examples/bug-tests/microflow-993-parameter-position.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
-- ako/mxcli#993 — @position on a flow parameter.
--
-- A MicroflowParameter is a stored node with real geometry (RelativeMiddlePoint
-- + Size 30;30), and Studio Pro lets you drag it. Before this, no annotation
-- reached it: a generated flow's parameter block landed on mxcli's derived grid
-- and a hand-aligned one was moved back there by any rewrite. Measured on a real
-- project: a nanoflow parameter at -77;0 came back at 200;53 from a describe →
-- exec of mxcli's own output.
--
-- Run, then `describe` each flow: NF_ParamPos round-trips its annotations,
-- NF_ParamDerived emits none because its parameters sit exactly where the layout
-- put them.

create or replace nanoflow MyFirstModule.NF_ParamPos (
@position(300, 100)
$A: Integer,
@position(200, 100)
$B: Integer
)
returns Integer as $R
begin
@position(300, 200)
declare $R Integer = $A + $B;
@position(500, 200)
return $R;
end;

-- Control: no annotation, so both parameters go where the layout puts them —
-- 200;53 and 300;53. DESCRIBE must emit no @position for either, or every
-- rewritten flow would come back with its parameters pinned to the grid they
-- happened to be on (the #951 failure, one node family over).
create or replace nanoflow MyFirstModule.NF_ParamDerived (
$A: Integer,
$B: Integer
)
returns Integer as $R
begin
@position(300, 200)
declare $R Integer = $A + $B;
@position(500, 200)
return $R;
end;

-- Microflows and rules share the parameter grammar, so they take it too.
create or replace microflow MyFirstModule.MF_ParamPos (
@position(140, -60)
$A: Integer
)
returns Integer as $R
begin
@position(300, 200)
declare $R Integer = $A + 1;
@position(500, 200)
return $R;
end;

create or replace rule MyFirstModule.RL_ParamPos (
@position(60, -40)
$A: Integer
)
returns Boolean
begin
@position(300, 200)
return $A > 0;
end;
11 changes: 9 additions & 2 deletions mdl/ast/ast_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,15 @@ type ErrorHandlingClause struct {

// MicroflowParam represents a microflow parameter.
type MicroflowParam struct {
Name string // Parameter name (without $ prefix)
Type DataType // Parameter type
Name string // Parameter name (without $ prefix)
Type DataType // Parameter type
Position *Position // @position(x, y) on the parameter; nil to let the layout place it
// UnknownAnnotations holds annotation names written on the parameter that
// mxcli does not implement there. Collected rather than dropped so MDL059
// can refuse them: an annotation that parses and does nothing loses whatever
// it was meant to express, silently (#884, the same reasoning one node
// family over).
UnknownAnnotations []string
}

// MicroflowReturnType represents a microflow return type.
Expand Down
Loading
Loading