diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index 6c5d2ad1f3..aa9534e819 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -510,6 +510,7 @@ {"area": "mdl/executor", "date": "2026-09-04", "symptom": "`return` inside a `loop` passes `mxcli check` AND `exec`, then mxbuild fails **CE0068** \"End events cannot be placed inside a loop.\" \u2014 even though MDL062 exists precisely to catch that. It fires only when the microflow header has NO `returns T as $Var` clause.", "cause": "MDL062 stood down for the whole microflow whenever the AS clause was present, on two claims: that buildFlowGraph synthesizes the End event from the variable so none lands in the loop, and that the shape builds CE0109 instead. `describe microflow` shows the in-loop `return` written either way, so the first was never true. The second was a measurement artefact: mxbuild reports ONE error per microflow, and in the shape that was measured the AS variable was never assigned, so CE0109 \"Undefined variable\" won the race and hid CE0068 underneath. Adding `declare $Done Boolean = false` and changing nothing else turns the same microflow from CE0109 into CE0068. The exemption was deleted, not narrowed.", "file": "`mdl/executor/validate_microflow_ce_gaps.go` (checkReturnInLoop \u2014 the `v.returnType.Variable != \"\"` early return); test inverted in `validate_microflow_ce_gaps_test.go` (TestMDL062_ExemptsReturnsAsClause -> TestMDL062_FiresWithReturnsAsClause); examples `mdl-examples/bug-tests/captrack-19-return-in-loop-as-clause{,.fail}.mdl`", "insight": "A second error in the same document can HIDE the one you are measuring, because mxbuild reports one error per microflow. An exemption justified by \"measured: builds X instead\" is only sound if the reproduction was otherwise valid \u2014 here the repro was broken in a second way, and the error that surfaced was the one nobody was asking about. When a measurement says a construct is clean, add the minimum that removes every OTHER error from that document and measure again; the differential (CE0109 -> CE0068 on one added `declare`) is what settles it. Reported as CapTrackV2 FINDINGS \u00a719."} {"area": "mdl/executor", "date": "2026-09-04", "symptom": "`dataview dv (\u2026, OnClick: SHOW_PAGE \u2026)` parses, `mxcli check` is clean, `exec` writes the page without a word \u2014 and the rendered element has no handler and no role=\"button\". The same silence on `dynamictext`, `listview` and every other widget except a container or a button.", "cause": "`OnClick:` is an ALIAS for `Action:` (both stored as Properties[\"Action\"], #603), and mxcli writes that property for three widget kinds only: container/customcontainer, the buttons, and a navigationlist item. Every other widget drops it, and the property allow-lists behind MDL-WIDGET01/07 could not see that because they are widget-type AGNOSTIC \u2014 the same blind spot as #928's `editable:`. Added MDL-WIDGET23 (warning), with two messages: Mendix models no click action at all (dataview, dynamictext, inputs, groupbox\u2026) vs Mendix models one that mxcli cannot write (listview, staticimage, dynamicimage \u2014 measured against generated/metamodel).", "file": "`mdl/executor/validate_widget_onclick.go` (new), wired in `validate_widgets.go` beside validateWidgetEditability; `.claude/skills/mendix/create-page/reference/widgets.md`; example `mdl-examples/bug-tests/captrack-21-dataview-onclick-dropped.mdl`", "insight": "The first draft reported everything OUTSIDE an allow-list of the three writers, and running it over the shipped examples flagged three of them. Cause: `mxcli check` without `-p` has no widget registry, so `lookupWidgetDef` returns nil for a PLUGGABLE widget too and the caller's \"static widgets only\" branch silently does not hold \u2014 `datagrid` is DataGrid 2, a pluggable widget whose onClick the engine does write. For any rule keyed on widget type, an allow-list makes the unknown case an ERROR and a deny-list makes it silence; pick the deny-list, because a missed warning costs nothing and a false one tells an author their working page is broken. Running a new rule across mdl-examples/ before wiring it up is what caught it \u2014 the same exercise #893 describes. Reported as CapTrackV2 FINDINGS \u00a721."} {"area": "mdl/executor", "date": "2026-09-04", "symptom": "`ALTER SETTINGS MODEL AfterStartupMicroflow = 'Mod.MF_Seed'` accepts a microflow with no return type, `mxcli check` passes, and the build fails **CE0142** \"After startup microflow should return a boolean\". `ALTER MICROFLOW \u2026 RETURNS \u2026` does not parse either, so the remedy is DROP + CREATE.", "cause": "#274 made ALTER SETTINGS resolve the qualified names it writes, which catches a MISSPELLED microflow. Here the name resolves perfectly \u2014 the constraint is on the thing the setting names, not on the reference, and nothing looked at the return type. Added MDL073: a project-less pass (ValidateAfterStartupReturnType) for a microflow the script itself creates, which is the usual shape, plus the stored return type on the project path. Both call one function so they cannot drift. flowSignature gained ReturnKind because its existing Returns field is the entity name and cannot tell Boolean from void.", "file": "`mdl/executor/validate_settings_refs.go` (checkAfterStartupReturnsBoolean + ValidateAfterStartupReturnType), `validate_program.go`, `helpers.go` (buildMicroflowReturnTypes), `validate_datasource_args.go` (flowSignature.ReturnKind); examples `mdl-examples/bug-tests/captrack-6-after-startup-must-return-boolean{,.fail}.mdl`", "insight": "\"The reference resolves\" and \"the reference is usable\" are different questions, and a resolver answers only the first. Whenever a setting stores a NAME, ask what the platform requires of the named thing \u2014 an existence check will pass and the build will still fail, and the error arrives with no connection to the statement that caused it. The scope discipline that goes with it: only AfterStartup is type-checked, because only its rule was measured; BeforeShutdown and HealthCheck are left alone rather than constrained on a guess, and there is a control test asserting that. Reported as CapTrackV2 FINDINGS \u00a76."} +{"area": "mdl/executor", "date": "2026-09-04", "symptom": "Nightly integration tests fail on older Mendix versions only (10.24 and 11.6.8 red, 11.12/11.13/11.14 green): `--- FAIL: TestDocumentation_SurvivesRewrite/ai_model … create: create model requires Mendix 11.9.0+`", "cause": "The #1018 documentation-carry fixture table covers all 29 rewrite-capable doctypes, four of which (ai model, knowledge base, consumed mcp service, agent) do not exist below Mendix 11.9. The cases had no version gate, so on an older project they failed at CREATE — which says nothing about documentation carry, because the doctype is simply absent", "file": "`mdl/executor/documentation_preserved_test.go` (docPreserveCase.minMajor/minMinor + requireMinVersion in the runner)", "insight": "A doctype fixture table is a version-compatibility surface, not just coverage: adding a row for a gated doctype silently commits you to every version in the nightly matrix. Develop-version bias is what hides it — written against 11.13, all 29 pass, and only the matrix disagrees. Reproduce locally instead of reasoning: `MX_BINARY=~/.mxcli/mxbuild/11.6.0/modeler/mx go test -tags integration` gives the identical failure in seconds, and the cached mxbuild versions under ~/.mxcli/mxbuild/ are usually enough to cover the matrix. Two controls are needed, not one — that the gate SKIPS below the minimum, and that it is INERT above it. A gate that always skips turns the matrix green while testing nothing, which is the worse failure and the one nobody notices", "refs": ["ako/mxcli#1018", "mendixlabs/mxcli actions run 33846778362"], "ce": []} {"area": "mdl/executor", "date": "2026-09-04", "symptom": "`ACTIONBUTTON \u2026 (Action: SIGN_OUT)` is refused by the default engine \u2014 \"client action *pages.SignOutClientAction not yet supported by the modelsdk engine \u2014 rerun with MXCLI_ENGINE=legacy\" \u2014 and the suggested workaround SILENTLY produces a dead button: on legacy the action is written as `Forms$NoAction`, so it renders, says \"Sign out\", and does nothing, with `mxcli check`, `exec` and `mx check` all clean.", "cause": "Neither engine had a case for the action. modelsdk's clientActionToGen ended in a loud default; sdk/mpr's serializeClientAction ended in a QUIET one that returns Forms$NoAction for anything unmatched. Added the case to both. The document is two keys \u2014 `Forms$SignOutClientAction` + `DisabledDuringExecution: true` \u2014 pinned against a Studio Pro-authored button in ako/TestApp, plus `sign_out` in the DESCRIBE renderer so it round-trips. Both engines now emit byte-identical documents; mx check 0 errors on each. OPEN_LINK is still unwritten by both (gen calls it OpenLinkClientAction and its Address is an element, not a string) \u2014 the syntax topic now says so instead of listing it as available.", "file": "`mdl/backend/modelsdk/widget_write.go` (clientActionToGen), `sdk/mpr/writer_widgets_action.go` (serializeClientAction), `mdl/executor/cmd_pages_describe_output.go` (renderClientActionMDL), `cmd/mxcli/syntax/features_page.go`; example `mdl-examples/bug-tests/captrack-10-sign-out-action.mdl`", "insight": "When one engine refuses something and points at the other, CHECK THE OTHER before repeating the advice \u2014 the refusal is visible and the fallback is not, so the recommended escape hatch can be the strictly worse path. The structural tell is the shape of the default branch: modelsdk's raises, legacy's returns Forms$NoAction, and a silent default in a serializer converts every unimplemented type into data loss rather than an error. Grep for the fallthrough before trusting a switch. Note the control this needs: a test that SIGN_OUT is no longer NoAction can pass because someone softened the default, so pin the fallback separately with a type that is still unimplemented (OPEN_LINK). Reported as CapTrackV2 FINDINGS \u00a710."} {"area": "mdl/executor", "date": "2026-09-04", "symptom": "A navigation menu's LOG-OUT item could not be authored and did not survive a round trip. MDL's `menu item` took PAGE or MICROFLOW only, so there was no spelling for it; and ako/TestApp's sign-out menu item read back as a plain `menu item 'Item 5';`, so DESCRIBE -> exec turned a working log-out entry into a dead one \u2014 silently, with `mx check` clean.", "cause": "A menu item's action goes through FOUR places that share no code with the button path: menuActionToGen (menu document, modelsdk), navMenuAction (navigation profile, raw BSON), resolveMenuAction (modelsdk read) and parseNavMenuItem (legacy read). Both writers ended in a NoAction default and both readers left the type name unmapped. Added SIGN_OUT to navMenuItemDef in the grammar (it consumes no qualifiedName, so it is read separately from the PAGE/MICROFLOW switch or an ICON after it is mis-assigned), carried it as ActionType \"SignOutAction\" / NavMenuItemSpec.SignOut, and wired all four. Studio Pro stores the same Forms$SignOutClientAction a button carries: DisabledDuringExecution true, nothing else.", "file": "`mdl/grammar/MDLParser.g4` (navMenuItemDef), `mdl/ast/ast_navigation.go`, `mdl/visitor/visitor_navigation.go`, `mdl/executor/cmd_menus.go` + `cmd_navigation.go` (conversion + printMenuMDL + the show summary), `mdl/types/navigation.go`, `mdl/backend/modelsdk/menu_write.go` + `navigation_write.go` + `navigation_read.go`, `sdk/mpr/parser_misc.go`; example `mdl-examples/bug-tests/captrack-10-sign-out-menu-item.mdl`", "insight": "A round trip closes only if the READER produces the exact string the WRITER consumes \u2014 here both readers had a raw-type-name fallback that looked like it preserved information (ActionType became \"Forms$SignOutClientAction\") while breaking the round trip, because DESCRIBE and the writers key on \"SignOutAction\". A fallback that stores the raw name is not the same as handling the case, and it hides the gap better than a NoAction default would. Also: the same logical action reaches storage through four unrelated switches (two writers x two constructs, two readers), so fixing the button path proved nothing about the menu path \u2014 grep for every switch on the action before calling such a fix complete. Controlled by neutralising both readers and re-reading TestApp: `Item 5 -> sign out` goes back to `Item 5`."} {"area": "mdl/executor", "date": "2026-09-04", "symptom": "`ACTIONBUTTON \u2026 (Action: OPEN_LINK 'https://\u2026')` was written by neither engine: modelsdk refused it, legacy fell through to its quiet default and wrote Forms$NoAction, so the button rendered and did nothing with check, exec and mx check all clean.", "cause": "Same missing-case defect as SIGN_OUT, but with two traps a reference settled and reasoning would not. (1) The STORAGE NAME is Forms$OpenLinkClientAction, while the semantic type is LinkClientAction and the executor stamped `Forms$LinkClientAction` \u2014 a wrong $Type that never reached disk only because nothing could write the action. (2) The address is not a string field but a nested Forms$StaticOrDynamicString. Pinned against 31 Studio Pro link buttons (ako/TestApp, FeedbackModule): exactly five keys, LinkType \"Web\" in all 31, and 6 of 31 DYNAMIC (IsDynamic true + AttributeRef + empty Value). MDL authors the static form only, so DESCRIBE flags a dynamic one instead of printing its address as a literal.", "file": "`mdl/backend/modelsdk/widget_write.go` (clientActionToGen + staticAddressToGen), `sdk/mpr/writer_widgets_action.go`, `mdl/executor/cmd_pages_builder_v3.go` ($Type), `mdl/executor/cmd_pages_describe_output.go`, `cmd/mxcli/syntax/features_page.go`; example `mdl-examples/bug-tests/captrack-10-open-link-action.mdl`", "insight": "gen declares a fourth property on Forms$StaticOrDynamicString \u2014 `Attribute` \u2014 that not one of the 31 stored documents carries. Writing it would be the 'never invent a key' failure: a document mxbuild accepts and Studio Pro cannot open. When gen offers more properties than the references show, the references win. Second lesson, about controls: the SIGN_OUT commit used LinkClientAction as its 'still unimplemented' control, and implementing OPEN_LINK silently invalidated it \u2014 the test then failed for a good reason, but a control naming a specific unimplemented feature has a shelf life. Point it at something structurally unwritable instead (ShowHomePageClientAction: no gen type, no metamodel counterpart, no MDL statement that builds one)."} diff --git a/mdl/executor/documentation_preserved_test.go b/mdl/executor/documentation_preserved_test.go index da5fab8db9..d519ed5c62 100644 --- a/mdl/executor/documentation_preserved_test.go +++ b/mdl/executor/documentation_preserved_test.go @@ -39,6 +39,12 @@ type docPreserveCase struct { // anything else modelsdk-only). The harness defaults to legacy, so without // this the case fails at its own precondition and says nothing about #1018. modelsdk bool + // minMajor/minMinor gate a doctype that does not exist in every supported + // Mendix version. Without this the case fails at CREATE on an older + // project, which says nothing about #1018 — the doctype is simply absent. + // Kept as a version rather than a boolean so the reason is legible at the + // case, and mirrors sdk/versions/mendix-11.yaml. + minMajor, minMinor int // create carries a doc comment; rewrite deliberately does not. create string rewrite string @@ -231,24 +237,32 @@ func docPreserveCases() []docPreserveCase { }, { name: "ai model", + minMajor: 11, + minMinor: 9, storedOnly: true, create: doc + "create model TestModule.DocModel ( Provider: MxCloudGenAI );", rewrite: "create or modify model TestModule.DocModel ( Provider: MxCloudGenAI );", }, { name: "knowledge base", + minMajor: 11, + minMinor: 9, storedOnly: true, create: doc + "create knowledge base TestModule.DocKb ( Provider: MxCloudGenAI );", rewrite: "create or modify knowledge base TestModule.DocKb ( Provider: MxCloudGenAI );", }, { name: "consumed mcp service", + minMajor: 11, + minMinor: 9, storedOnly: true, create: doc + "create consumed mcp service TestModule.DocMcp ( ProtocolVersion: 'v2025_03_26' );", rewrite: "create or modify consumed mcp service TestModule.DocMcp ( ProtocolVersion: 'v2025_03_26' );", }, { name: "agent", + minMajor: 11, + minMinor: 9, storedOnly: true, create: "create model TestModule.DocAgentModel ( Provider: MxCloudGenAI );\n" + doc + "create agent TestModule.DocAgent ( UsageType: Task, Model: TestModule.DocAgentModel, SystemPrompt: 'p' );", @@ -273,6 +287,10 @@ func TestDocumentation_SurvivesRewrite(t *testing.T) { } defer env.teardown() + if tc.minMajor > 0 { + env.requireMinVersion(t, tc.minMajor, tc.minMinor) + } + if err := env.executeMDL(tc.create); err != nil { t.Fatalf("create: %v", err) }