diff --git a/.claude/commands/mxcli-dev/review.md b/.claude/commands/mxcli-dev/review.md index 0d9124d4a1..9d14c8227d 100644 --- a/.claude/commands/mxcli-dev/review.md +++ b/.claude/commands/mxcli-dev/review.md @@ -53,6 +53,9 @@ proactively. Add a row after every review that surfaces something new. | 23 | A copy-to-scaffold path renames files but not the identifiers built from the name (`@mixin mxcli--`, `@import "mxcli-"`), 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 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 | | 25 | A doc or skill shows a CLI invocation nobody ran — a command name that does not exist (`mxcli dump-bson` for `mxcli bson dump`), or a flag form the parser rejects (`--compare "A" "B"` where `--compare` is a StringSlice needing `"A,B"`). Worst when copied FROM the command's own `--help`, which had the same error, so the doc looks sourced | Docs quality | Run every command a doc shows, against a real project, before committing it. If it came from `--help`, run that form too — the example in the help text is not evidence that it works | +| 26 | A clause added to a SHARED grammar rule (a datasource, a widget-property list) is written by only ONE of the constructs that rule serves — the others parse it, `exec` reports success, and DESCRIBE does not echo it back. A silent drop, often shipped by the very change that was fixing silent drops | Code correctness | Enumerate the other constructs the rule serves and RUN one. The round-trip that proved the feature on its intended target says nothing about them. Refuse it where it cannot be stored, naming the construct that can — an error, not a warning, when the metamodel decides it and no future package can make it valid | +| 27 | A metamodel-sync or list-coverage test asserts that a GAP still exists (`clickCapableInMendix["listview"]`, "a template for the list view's own entity is the base case Mendix permits") — so it passes throughout and FAILS on the correct fix, and the belief it encodes was never measured | Test coverage | Invert such a test rather than deleting it: keep the half that is still true (the metamodel really does carry the field) and flip the half that is not. When a test justifies itself by what a helper returns rather than by a measurement, treat it as a claim to check, not as evidence | +| 28 | A describe emitter added beside a shared property formatter duplicates a field the formatter already prints (`Editable: true` twice on one widget) — invisible when the round-trip only covers the page the change was written against | DESCRIBE roundtrip | Round-trip a page OTHER than the one under test, and assert occurrence COUNT (`strings.Count(out, x) != 1`), not presence. `Unchanged page` on re-exec of the describe output is the evidence that the emitted MDL rebuilds the stored document; `Check passed!` is not | --- diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl b/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl index d5301655d9..8790ffec32 100644 --- a/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl +++ b/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl @@ -115,3 +115,4 @@ {"area": "cmd/mxcli", "date": "2026-09-17", "symptom": "`mxcli run --local --mxbuild-path /x -p app.mpr` answers `Error: unknown flag: --mxbuild-path`, while the shipped run-local skill, runlocal.go's own comment and two resolution error messages all tell the user to pass it", "cause": "The flag was never registered on `runCmd`; it exists only on the four `docker` subcommands. Everything BEHIND it was already wired \u2014 `LocalRunOptions.MxBuildPath` is declared and `ResolveMxBuildForLocal` honours it \u2014 so the gap was one missing `Flags().String` and one missing field assignment, invisible to every test because nothing exercised the command's flag set", "file": "`cmd/mxcli/cmd_run.go` (flag registration + LocalRunOptions.MxBuildPath) + `.claude/skills/mendix/run-local/SKILL.md` + `docs-site/src/tools/run-local.md`", "insight": "The interesting defect is not the missing flag, it is that **the error messages recommending it were the only documentation of it** \u2014 guidance naming an option the command does not accept is worse than no guidance, because it reads to the user as their own mistake, and on macOS it was the only advertised way out of a platform mismatch. The regression test to write is therefore not 'the flag exists' but the invariant: scan the resolution sources for `--flag` strings they tell users to pass, and assert `run` registers each one (`TestErrorGuidanceNamesAFlagThatExists`). Watch the parse test \u2014 `-p` is PERSISTENT on rootCmd, so `runCmd.Flags().Parse` rejects it with `unknown shorthand flag: 'p'` and the test fails for a reason unrelated to the fix; resolve through `rootCmd.Find` to reproduce the reporter's line honestly. Issue #1125", "refs": ["#1125", "#916", "#1122"]} {"area": "cmd/mxcli", "date": "2026-09-17", "symptom": "`mxcli run --local` on a project with Settings > Web UI > OptimizedClient = No exits 1 after the cold build with `no rollup.config.mjs and no bundle at .../web/dist/index.js ... the build did not produce a client` \u2014 about a deployment whose client is sitting in the same directory. `mxcli docker run` works", "cause": "The client gate tested for exactly two shapes (a rollup config to run, or a bundle mxbuild already wrote) and treated everything else as a failed build. A classic (Dojo) deployment is a legitimate third shape with NEITHER: there is no bundling step for the classic client in any Mendix version. Nothing in the local loop read UseOptimizedClient, so the mode was invisible", "file": "`cmd/mxcli/docker/webclient_plan.go` (new: `planWebClient`, `isClassicWebClient`, `noWebClientError`) + `webclient.go` (`BuildWebClient`, `ensureWebClientBundle`) + `webclient_watch.go` (`StartWebClientWatch`) + `runlocal.go` (`ensureClientServed`)", "insight": "Measured on one blank 11.12.2 app built twice, changing only the setting: **mxbuild swaps which client lands in `deployment/web/` and parks the other beside it** \u2014 OptimizedClient=Yes gives `web/` the React client (+`rollup.config.mjs`) and parks Dojo in `dojo-web/`; =No gives `web/` the Dojo client (`index.html` loading `mxclientsystem/mxui/mxui.js`, no rollup config, no dist) and parks React in `react-web/` WITH its own rollup config. So detect from the DEPLOYMENT, not from the model's setting: the deployment is what gets served, the two disagree exactly when the setting has just changed, it needs no plumbing through the five call sites, and it covers MigrationMode without predicting what that mode emits. Detect on POSITIVE evidence (the entry point names its client) \u2014 inferring classic from the absence of the React shapes would make every genuinely broken deployment look classic and silently skip the bundle, which is the black screen the gate exists to prevent; keep a control test that a clientless deployment still fails. **The gate had FIVE consumers, not one**: boot, the `--watch` bundler, the post-boot re-bundle guard, and `ensureClientServed`, which probes that `/dist/index.js` is *served* \u2014 measured 404 on a classic app, so fixing only the boot moves the failure to every applied change under `--watch`. Two of them carried hand-copied duplicates of the same gate and had already drifted once (the 11.14 fix, ako/mxcli-ledger #146, landed on `BuildWebClient` only, so `run --local` worked on 11.14 and `run --local --watch` did not) \u2014 so the fix collapses them into one `planWebClient` rather than adding a third copy. Repro from Linux with no Mac and no Studio Pro: `mxcli new` an 11.12.2 app, flip `UseOptimizedClient` to `No` on `Forms$WebUIProjectSettingsPart` (note the `Forms$` prefix, not `Settings$`), build, and keep the unflipped copy as the control. Patching that BSON with a Go `map` corrupts the file \u2014 mxbuild refuses it with `Expected '$ID' as the first property of a storage object` \u2014 because map iteration loses key order; use `bson.D` throughout. Verified in a browser, not just at the gate: `mx` global present, real page content, zero console errors. Issue #1123", "refs": ["#1123", "ako/mxcli-ledger#146"]} {"area": "cmd-mxcli", "date": "2026-09-17", "symptom": "`mxcli new --version 10.24.25` (and `mxcli setup mxbuild --version 10.24.25`) dies with `HTTP 404 from https://cdn.mendix.com/runtime/mxbuild-10.24.25.tar.gz`. Every 9.x and 10.x version probed 404s while 11.6.0/11.12.1/11.13.0 return 200 from the same host and path, which reads as 'Mendix 10 is no longer on the CDN'.", "cause": "Mendix 9 and 10 publish FOUR-part artifact names carrying a build number the release notes never mention: the release called 10.24.25 is `mxbuild-10.24.25.122571.tar.gz`. Mendix 11 publishes three parts. `MxBuildCDNURL` interpolates whatever string it is handed and nothing resolved a partial version, so a hand-typed 10.x version named no artifact at all. Project-driven paths were never affected — the MPR's `_ProductVersion` already carries all four parts (`10.24.25.122571`) and `parseVersion` takes the first three for major/minor/patch while the full string goes to the URL.", "file": "`cmd/mxcli/docker/version_resolve.go` (ResolveCDNVersion, highestBuild, CDNReleasesFor); wired at the two entry points where a user types a version, `cmd/mxcli/cmd_new.go` and `cmd/mxcli/setup.go`. Tests `cmd/mxcli/docker/version_resolve_test.go`.", "insight": "**A uniform 404 across a whole major version is evidence about the NAME, not about availability.** The conclusion drawn from it — 'Mendix 10 cannot be downloaded here' — blocked a verification for an entire session, and the fix was one listing call: the CDN is an S3 bucket that answers ListObjectsV2 (`?list-type=2&prefix=runtime/mxbuild-10.24.`), so what exists is enumerable rather than guessable. When a probe fails identically for every input in a class, question the query before concluding the class is empty. Three traps in the resolution itself, each a test: the `.sha256` sidecar beside every archive must not be picked as an artifact; the prefix needs its trailing dot or `10.24.2` swallows `10.24.20`..`10.24.26`; and build numbers are not zero-padded, so a text sort puts 99999 above 122571 and 10.24.9 above 10.24.26. Resolve at the entry point and thread the RESOLVED string onward — `mxcli new` checks the created project's stamp against the requested version, and `mx create-project` stamps four parts, so resolving late would fail that postcondition.", "refs": ["#1121"]} +{"area": "cmd/mxcli", "date": "2026-09-18", "symptom": "mendixlabs/mxcli#1025: `mxcli syntax` advertises `mxcli syntax workflow user-task targeting` in its own help and answers `Unknown topic: workflow user-task targeting`. Same for `workflow user-task` and `workflow parallel-split`, all of which `mxcli syntax workflow` lists as sub-topics; `--json` was the only route that reached them.", "cause": "The CLI built its path with `strings.Join(args, \".\")` and never split an argument, so a topic handed over as ONE string — a quoted copy-paste, a tool wrapper, `sh -c` — became the path `workflow user-task targeting`, which matches nothing. The REPL's `help` had resolved multi-word topics since it was written (`resolveHelpPath`, greedy hyphen-joining): one question, two answers, and the CLI held the weaker copy. The #955 segment-match fallback could not save it either — it passed the DOTTED path to `BySegmentMatch`, and no segment contains a '.', so that fallback was silently dead for every multi-word query.", "file": "cmd/mxcli/syntax/topic.go (new: Lookup, topicWords, resolvePath), cmd/mxcli/help.go, mdl/executor/cmd_misc.go (resolveHelpPath deleted), mdl/grammar/domains/MDLSettings.g4 (helpStatement, helpTopicWord), mdl/visitor/visitor_query.go (ExitHelpStatement); tests cmd/mxcli/cmd_syntax_test.go, cmd/mxcli/syntax/topic_test.go, mdl/executor/cmd_misc_test.go, mdl/visitor/visitor_help_topic_test.go; example mdl-examples/bug-tests/syntax-1025-topic-drilldown.mdl", "insight": "**The spaces in the reported error message were the whole diagnosis, and reading them as a paraphrase cost an hour.** The command prints the path it built, and the CLI joins on '.', so `Unknown topic: workflow user-task targeting` cannot come from the command as documented — it can only come from the topic arriving as a single argument. Every line of the report follows from that and nothing else does: `syntax workflow` works (one word), `--json` works (the flag is not part of the topic), the three multi-word forms fail. Take a quoted error message literally, character for character, before assuming the reporter retyped it. **The reported version is downloadable and settles it in one run**: `mxcli setup mxcli`'s own URL shape (`releases/download//mxcli-linux-amd64`, NOT the goreleaser `_Linux_x86_64.tar.gz` that 404s) fetched v0.20.0, where the unquoted command works and the quoted one reproduces the message verbatim — so 'fixed since' and 'never broken' were both wrong. **The guard that matters is not the three cases from the report** but `TestEveryRegisteredPathIsReachableBySpelling`: every registered path, tried dotted, as separate arguments, and as one string. The registry prints dotted paths and then tells the reader to drill down with words, so a spelling that does not resolve is the command contradicting its own output; a per-case test would have passed the day someone added a topic with a new shape. Control: stub the whitespace split in `topicWords` and it fails with the reported path, spaces and all. **The grammar half has a trap the CLI half does not, and only the EXISTING suite caught it.** `helpStatement: IDENTIFIER (identifierOrKeyword)*` is the grammar's catch-all — a statement that is just an identifier and some words — so whatever it can swallow, it swallows from the statement that should have had it. Widening it to `(DOT? helpTopicWord)*` to take `help workflow.user-task` made `Sec.ApiUser` a complete statement of its own, and `create module role Sec.ApiUser` then parsed, WITH NO PARSE ERROR, as CREATE MODULE (named \"role\") followed by a help topic — two statements, wrong types, six unrelated security tests red. `(helpTopicWord (DOT? helpTopicWord)*)?` — a topic word before any dot — leaves `.ApiUser` unconsumable and restores the old disambiguation. Bisect a grammar regression by SHAPE, not by reading the ATN: adding the unused rule alone was clean, the hyphen alone was clean, the leading optional DOT was the whole of it, and three regenerations said so in about a minute. **When widening a permissive rule, the test to add is not for the new spelling but for what the rule must still NOT swallow** (TestHelpRuleDoesNotSwallowATrailingQualifiedName).", "refs": ["mendixlabs/mxcli#1025", "#955"]} diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index 02a047ee96..ca0570ddd7 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -639,3 +639,13 @@ {"area": "mdl/executor", "date": "2026-09-17", "symptom": "`CREATE OR MODIFY MICROFLOW` re-enables concurrent execution on a microflow that disallowed it \u2014 the running app's concurrency protection removed \u2014 and drops the concurrency error message (all translations) and error microflow, plus `MarkAsUsed`. Every checker is green: **CE4899 fires only on disallow-without-a-message, never on allow**, so the one error that exists in this area is exactly the one the reset switches off", "cause": "`buildMicroflowFromStmt` built the rebuild struct with `AllowConcurrentExecution: true` and `MarkAsUsed: false` literals, and `microflowToGen` wrote `SetConcurrencyErrorMicroflowQualifiedName(\"\")` + a bare `genTexts.NewText()`. The backend already READ the two flags back (the #723 \u00a7A fix), so the round-trip test passed while the bug was live \u2014 the executor overwrote them before the backend ever saw them", "file": "`mdl/executor/cmd_microflows_build.go` (buildMicroflowFromStmt), `mdl/backend/modelsdk/microflow_write.go` (microflowToGen), `mdl/backend/modelsdk/microflow.go` (microflowFromGen), `sdk/microflows/microflows.go`", "fix": "Carry all four from the stored microflow, seeding the locals with the NEW-microflow defaults (true/false) so no separate preserve flag is needed. The error message reuses the existing `textFromGen`/`textToGen` pair, so translations survive; nil still emits the bare empty `Texts$Text` the writer always wrote", "insight": "**A passing round-trip test at one layer says nothing about the layer above it.** `TestMicroflowRoundTrip_ConcurrentExecutionFlags` had guarded these two flags since #723 and was green throughout, because the executor's rebuild struct overwrites them before calling the backend. When a property is reset, locate the LAST writer on the path, not the first one that looks responsible. **And check which way a reset goes**: #723's backend bug wrote the Go zero value (allow -> disallow) and hit CE4899 immediately; the executor's literal writes the opposite (disallow -> allow), and the same CE4899 that caught the first direction is structurally blind to the second. A checker that catches a property's loss in one direction is not coverage for that property. Two methodological traps in the test itself, both hit: `bytes.Equal` on two encodes of the same microflow ALWAYS differs (fresh random sub-element `$ID`s \u2014 the reason `canon` exists), and `canon.Equal` on a whole microflow always differs too, because `StableId` is a fresh GUID *value* per encode and `Equal` does not mask \u2014 only `Reconcile` may be asked that question. Compare the sub-element under test, or use Reconcile. Controls: hardcoding the executor literals back, emptying the writer's pair, and stubbing the reader each fail a different test with the reported symptom"} {"area":"mdl/executor","date":"2026-09-17","symptom":"`DESCRIBE ENUMERATION Mod.E` prints every value with an empty caption (`MyValue ''`) although Studio Pro shows them. Re-executing that output then DESTROYS the real captions (exec reports \"Modified enumeration\" and the stored Texts$Translation goes empty). Reported on Windows, single-language project, v0.18.0 and v0.22.0","cause":"The read asked `v.Caption.GetTranslation(\"en_US\")`. Mendix has no language-neutral text: a project whose DefaultLanguageCode is nl_NL stores the caption under nl_NL and nothing else, so the lookup misses and returns \"\". #970 fixed the WRITE side to use the project language and #702 fixed the widget READ side; the enumeration/validation-rule/message-template reads were the sites neither sweep reached","file":"`mdl/executor/cmd_enumerations.go` (describeEnumeration), `cmd_diff_mdl.go` (enumerationToMDL), `describe_language.go` (pickTextTranslation's fallback now sorts), `mdl/catalog/language.go` + `builder_modules.go`","insight":"**Reproduce it with mxcli alone — no Studio Pro and no non-English project needed.** `ALTER SETTINGS LANGUAGE ADD OR MODIFY 'nl_NL' (...); ALTER SETTINGS LANGUAGE DefaultLanguageCode = 'nl_NL';` on a copy of any fixture, then CREATE the enumeration: the write side already honours the project language, so the captions land under nl_NL and DESCRIBE reads '' immediately. That also gives the impact control for free — feed the '' output back through exec and grep the .mxunit for `Texts$Translation LanguageCode nl_NL Text ` with nothing after it. **The plausible wrong turn to skip**: suspecting the codec or a gen storage-name mismatch. `EnumerationValue.Caption` is NOT in keyaudit_test.go and the strings are plainly visible in the unit — dump the .mxunit with a printable-ASCII regex FIRST (one command) and the language code tells you it is a read-side language bug, not a decode bug. **The fallback has to sort**: `for _, v := range t.Translations` returns a different language per run, so a multi-language project's DESCRIBE output was undiffable — a bug that a single-language repro can never show.","refs":["mendixlabs/mxcli#1113","mendixlabs/mxcli#970","mendixlabs/mxcli#702"],"ce":[]} {"area": "mdl/executor", "date": "2026-09-17", "symptom": "`CREATE OR MODIFY EXTERNAL ENTITIES FROM` imports an OData entity with **none** of its ComplexType properties — `describe entity` lists only the key. `exec` reports `1 created, 0 failed` and prints nothing; `mx check` says 0 errors. The loss surfaces much later as CE1613 on a page written against the attributes Studio Pro would have made. `DESCRIBE CONTRACT ENTITY` compounded it by reporting the complex property as `String(200)`", "cause": "`mdl/types/edmx.go` never parsed `` at all, so a property typed `Shared.Uom.Quantity` was indistinguishable from one of an unknown type, and `createExternalEntities`' `if !strings.HasPrefix(p.Type, \"Edm.\")` dropped it with no `continue` message. `String(200)` was `edmToMendixType`'s default branch", "file": "`mdl/types/edmx.go` (EdmComplexType, FindComplexType, FlattenProperties, EdmProperty.RemotePath/Path), `mdl/executor/cmd_contract.go` (createExternalEntities, describeContractEntity, outputContractEntityMDL)", "insight": "**The local name and the remote name differ by SEPARATOR, and that is the core of the fix.** Studio Pro names the attribute `MaxQty_UoMNId` and reads it over the OData path `MaxQty/UoMNId`; assuming RemoteName == attribute name is the obvious wrong turn and it is silent in the model. Measured on mxbuild 11.12.1, three copies of one project: RemoteName `MaxQty/UoMNId` -> 0 errors; `MaxQty_UoMNId` -> 4x **CE6615** \"Attribute 'X' of external entity 'Definition' does not exist in the OData service\"; a deliberately bogus path -> the same 4x CE6615. So mxbuild resolves the path INTO the complex type and genuinely validates it — the 0-error run is evidence, not a rubber stamp, and CE6615 is the detector to reach for on any external-entity remote-name question. **Do not stop at a synthetic fixture.** A two-property complex type in its own namespace passed `mx check` clean and the fix still shipped four defects, all caught by the integration suite's live **TripPin** contract (`10-odata-examples.mdl`) at 11 errors. TripPin is the fixture to reach for: it has a base complex type, two types derived from it, a nested complex property, an Edm.GeographyPoint, and both a top-level entity set and types derived from it. What it taught, each measured: (1) Mendix imports a complex type's **own** properties only — flattening `AirportLocation`'s inherited `Address` is CE6615, while the same `Address` via `Person.HomeAddress` (typed `Location` directly) is accepted, so the line is inheritance, not path syntax; (2) `!strings.HasPrefix(t, \"Edm.\")` is not the supported-type test — `Edm.GeographyPoint` passes it and is **CE6622** \"The type of attribute 'Location_Loc' … is not supported\", so the importable primitives must be a closed set; (3) Creatable/Updatable are always false on a flattened attribute (against a contract annotated Insertable=true AND Updatable=true, Mendix still says False — 2x **CE6630** per attribute, matching the doc's \"can only be read or deleted\"), but **Filterable/Sortable are not**: they follow the ENTITY SET, and CE6630 fires in BOTH directions, so neither blanket answer survives. On TripPin, `Person` (entity set `People`) wants True and `Employee`/`Manager`/`Event` (derived, no entity set) want False; `Manager.BossOffice` is Manager's own property and still False, which rules out inheritance as the explanation. A test for a two-directional rule needs **both** controls — stamping false everywhere passes the derived case and fails People. Resolve complex types by QUALIFIED name: one document may declare `Quantity` in two namespaces, and FindEntityType's short-name fallback would silently hand over the other schema's properties. Also: the pre-fix control (attributes simply absent) is **0 errors**, so the build never catches the drop itself — a regression test asserting `mx check` clean would have passed against the bug. The report said 'only cross-namespace'; in fact every complex type was dropped, since nothing named `Edm.*` is complex. Repro `mdl-examples/bug-tests/1118-odata-complextype-flattening.mdl`", "file_refs": ["mdl/types/edmx.go", "mdl/executor/cmd_contract.go"], "refs": ["mendixlabs/mxcli#1118"], "ce": ["CE6615", "CE6622", "CE6630", "CE1613"]} +{"area": "mdl/executor", "date": "2026-09-18", "symptom": "`raise error;` on a microflow's MAIN flow passes `mxcli check`, is written by `exec`, and the build then fails with `[error] [CE0710] \"The main flow cannot join an error flow or end in an error event.\" at Sequence flow` — one per microflow. Every shape reproduces: a body that is only `raise error;`, a typed `returns … as $Var` body, an if/else whose branches are each terminal, and a guard clause that raises in one branch", "cause": "Not a wiring defect. The error event is illegal on the main flow no matter how it is wired: Mendix's error event RE-RAISES the error being handled, so one is only legal where an error is in scope — inside an `on error { … }` handler. mxcli had no check for it, so nothing stood between the script and the build", "file": "`mdl/executor/validate_microflow_raise_error.go` (MDL084, `mainFlowRaiseErrors`), wired from `validate_microflow.go` (`validate`) and `rule_validation.go` (`validateRule`)", "insight": "**The issue's own diagnosis was wrong, and checking it cost one probe test.** It blamed a trailing EndEvent appended because RaiseErrorStmt never set the builder's \"ends with return\" flag, and proposed setting it. `isTerminalStmt` had handled RaiseErrorStmt all along: dumping the graph for all four reported shapes showed exactly the document the report asked for — start event, error event, one sequence flow, no trailing end event, zero outgoing flows from the error event. When a report names a flag and a fix, dump the artefact before editing the flag; a builder change here would have been measured against a symptom that never moved. **The rule's scope needs the near-miss control, not just the reported shape.** `raise error;` one nesting level in — inside `on error { … }` — must keep working, and a rule that refuses RAISE ERROR outright passes every assertion about the bug. Measured on mxbuild 11.13.0, two copies of a blank app from a 0-error baseline: the four reported shapes -> 4x CE0710; the identical statement inside a handler (plus one inside a branch of a handler) -> 0 errors. That pair is what fixes the walk's one real requirement — skip `ErrorHandlingClause.Body` subtrees, descend into everything else — which is why it cannot reuse `walkBody`, which descends into handler bodies on purpose. Covered rules as well as microflows: a rule goes through the same flow builder, and shipping the fix for microflows only is the shape that let MDL051 cover `break` but not `continue` (#791). Posture follows the #893 CE-gap rules: error severity so exec's pre-flight refuses with nothing written, off `execEnforcedMicroflowRules` so `--no-check` still reproduces the build failure. Repro `mdl-examples/bug-tests/microflow-1030-raise-error-main-flow.fail.mdl`, control `…-main-flow.mdl`", "file_refs": ["mdl/executor/validate_microflow_raise_error.go", "mdl/executor/validate_microflow.go", "mdl/executor/rule_validation.go"], "refs": ["mendixlabs/mxcli#1030"], "ce": ["CE0710"], "rules": ["MDL084"]} +{"area": "mdl/executor", "date": "2026-09-18", "symptom": "`mxcli check -p --references` reports MDL-WIDGET25 \"`htmlelement` / `attribute` / `tagcontentcontainer` is not a widget in this project\" on a page `describe page` had just emitted, while `exec --no-check` writes the same page without complaint. check is STRICTER than exec — the inversion check exists to prevent", "cause": "Two registries. The page builder (`pageBuilder.initPluggableEngine`) calls `RefreshStaleWidgetDefinitions` before `LoadUserDefinitions`, so exec generates `.mxcli/widgets/*.def.json` from the project's installed `.mpk` on its way past. `LoadWidgetRegistry` — used by check, lint and the LSP — called only `LoadUserDefinitions`, so on a project that had never run `mxcli widget init` the validator knew the nine embedded definitions and nothing else. The `pluggablewidget ''` branch of validateWidgetKind has `packageInstalledFor` as its escape hatch; the generic-MDL-name branch had none, and it also returns BEFORE the `parentDef == nil` silence guard written a few lines below for exactly this case, so an unresolvable parent's CHILDREN were reported as missing widgets", "file": "`mdl/executor/validate_widgets.go` (`LoadWidgetRegistry` now refreshes stale defs, best-effort); `mdl/backend/pagemutator/mutator.go` (`noPluggableObjectError`)", "insight": "**The bug self-heals, which is why it reads as flaky: the first `exec` writes the definitions and every `check` after it passes.** `rm -rf .mxcli/widgets` before reproducing or it is invisible — that alone cost more than the fix. The fixture `testdata/expr-checker/` is already in the reported state (HTMLElement.mpk installed, `.mxcli` gitignored), so a unit test needs only a temp dir with one 10 KB .mpk copied in; the registry never opens the .mpr, only `filepath.Dir(projectPath)`. Control that keeps the fix honest: a typo (`htmlelemnt`) in the SAME project must still be MDL-WIDGET25 — and now it can even suggest `htmlelement`, which it could not when the candidate list was the nine embedded widgets. **Second half of the same report, and the wrong turn to skip: do not take a reporter's 'these are design properties of X' on faith — check the theme.** 'Remove empty text' / 'Remove loadmore button' / 'Reset list style' are NOT ListView design properties in the Atlas shipped with Mendix 11. **Count them with `show design properties for listview`, never by reading the design-properties.json key**: a List View has SIX — Style/Hover style/Row size under `ListView`, plus Spacing/Align self/Hide on inherited from the `Widget` group that applies to every widget. Reading the raw `ListView` key alone says three, which is the mistake this session made and had to correct; `ThemeRegistry.GetPropertiesForWidget` already prepends the inherited group, so anything built on it is right and anything built on the JSON key rejects half the real properties. ALTER STYLING writes the reported key happily and mxbuild then fails **CE6083** 'Design property Remove empty text is not supported by your theme'; the same statement with 'Row size' = 'Small' is 0 errors. So refusing the write was right and only the REASON was wrong — which is why the fix is the message, not write support. Still open, tracked as ako/mxcli#509: MDL-WIDGET11 covers CREATE PAGE / CREATE SNIPPET / ALTER PAGE INSERT|REPLACE and not `ALTER STYLING`, whose widget type lives only in the stored document, so an unsupported key is silent until mxbuild says CE6083.", "refs": ["mendixlabs/mxcli#1135", "mendixlabs/mxcli#1069", "ako/mxcli#509"], "rules": ["MDL-WIDGET25", "MDL-WIDGET26"], "ce": ["CE6083"]} +{"area":"mdl/executor","date":"2026-09-18","symptom":"`mxcli check` warns MDL-WIDGET20 that a List View's (and a grid column's) `Editable` is \"silently dropped on write and the widget stays enabled\". It is written: exec writes it, `describe page` reads back `Editable: true`, and mxbuild reports 0 errors on 11.12.2. The suggestion even reads \"buttons do support conditional visibility\"","cause":"Two different Mendix properties conflated under one MDL keyword. `editableWidgetTypes` is the set of Pages types carrying **Editability / ConditionalEditabilitySettings**, which is right for the bug the rule was written for (#928, `editable:` on a button). `Pages$ListView` and `Pages$GridColumn` carry neither — they have a plain `Editable bool`, a different property meaning \"make the inputs INSIDE me editable\" — so they fell through to the warning","file":"`mdl/executor/validate_widget_editability.go` (new `plainEditableWidgetTypes`); test `mdl/executor/widget_editable_plain_bool_test.go`","insight":"**A metamodel-sync test guards only the class it enumerates, and can lock a bug in.** `TestEditableWidgetTypesMatchMetamodel` keeps the list synced to the *Editability* set, so a type with a plain `Editable bool` and no Editability was invisible to it — the test passed throughout, and a correct fix would have made it fail if the two sets had been merged. The fix is a SECOND set with its own sibling test (`TestPlainEditableBoolTypesMatchMetamodel`), not more entries in the first. **Find the affected types by parsing generated/metamodel rather than by guessing**: scanning every `Pages*` struct for a plain `Editable bool` with no `Editability` returns exactly two (ListView, GridColumn) — the second one was not in the bug report and would have been missed. The control that keeps the fix narrow: neither type has `ConditionalEditabilitySettings`, so the BRACKET form `Editable: [expr]` (lowered to `EditableIf`) IS dropped and must keep warning — silencing both forms would re-create the worse half of #928, where the shape the docs recommend is dropped without a word. Worst-case cost of this false positive: `buildListViewV3`'s own comment records that a list view without `Editable` renders every input as `
` with entity access ReadWrite and `mx check` clean — so the warning steered authors away from the one property that fixes a symptom the code itself calls hard to diagnose","refs":["ako/mxcli#510","mendixlabs/mxcli#928"],"rules":["MDL-WIDGET20"]} +{"area":"mdl/executor","date":"2026-09-18","symptom":"`template for ` passes `mxcli check`, is written by `exec`, and mxbuild then refuses the project with **CE0543** \"The entity of the list view template is 'X' and this is not a specialization of the entity of the list view\"","cause":"The guard existed and was one case too generous. Both call sites gated on `entityIsOrDescendsFrom(spec, listEntity)`, which returns true on its FIRST loop iteration when `spec == listEntity`. Mendix requires a STRICT specialization — the list view's own body already renders an object no template matches, so a template for the base entity is a second, unreachable default","file":"`mdl/executor/cmd_pages_builder_v3.go` (new `checkListViewTemplateSpecialization`), called from `cmd_pages_builder_v3_widgets.go` (CREATE) and `cmd_alter_page.go` (ALTER INSERT/REPLACE); `cmd/mxcli/syntax/features_page.go`","insight":"**The guard's own error message named the bug, and a test asserted it.** The wording was \" is not **or a specialization of it**\" — it offered the exact case Mendix refuses — and `TestBuildListViewTemplateOnTheListEntityItself` asserted that case was \"the base case Mendix permits\", justified by what `entityIsOrDescendsFrom` returns rather than by any measurement. So the belief was encoded three times (guard, message, test) and measured zero times; reading the code confirms itself. **Three rows separate the cases and none is redundant**: a real specialization (0 errors — the control against a guard that refuses everything), an unrelated entity (already refused, so the guard was not simply missing), and the list view's own entity (written, CE0543). Do not fix `entityIsOrDescendsFrom` in place: its other callers resolve association direction, where the reflexive case is correct. One shared method for both call sites, because the message and the rule were already duplicated and this is how those drift. Left open in ako/mxcli#514: the guard runs at exec, not check, so all three rows report `Check passed!` — a false green, though a safe one since nothing is written when it refuses","refs":["ako/mxcli#514"],"ce":["CE0543"]} +{"area":"mdl/executor","date":"2026-09-18","symptom":"`alter styling … set '' = ` passes `mxcli check`, writes, and mxbuild then fails **CE6083** \"Design property X is not supported by your theme\". MDL-WIDGET11/12 never ran on ALTER STYLING","cause":"`ValidateDesignPropertiesForStatement` switches on `CreatePageStmtV3`, `CreateSnippetStmtV3` and `AlterPageStmt` (its INSERT/REPLACE trees). `*ast.AlterStylingStmt` is not among them — the one statement whose entire job is writing design properties","file":"`mdl/executor/validate_alter_styling.go` (new), wired from `validate_program.go`","insight":"**The obvious fix — map the stored `$Type` to a theme-registry key — was the wrong one, and the repo already had the trap laid out.** `mdlKeywordToDesignPropsKey` maps MDL keyword → key and an UNUSED `bsonTypeToDesignPropsKey` maps `$Type` → key (zero non-test callers, so never validated); adding a third consumer of that concept is the drift `#1069`'s `buildPropKeyMap` records. Ask instead the question that is answerable WITHOUT the document: does any widget type in the theme declare this key? A key declared nowhere cannot be right here either, which is exactly the reported case, and a key declared for another type is accepted — under-reporting, never over-reporting, the only safe direction for a check that cannot see what it is judging. **Build the declared-key set from every group, not one**: three of a List View's six properties come from the inherited `Widget` group, so a single-type lookup reports `Align self` as unknown. Two separate rule-ID guards (`TestWidgetRuleIDsAreNotReused`, `TestRuleIDHasOneOwner`) both fail when a rule is raised from a second file — register the pair rather than renumbering, since a suppression of MDL-WIDGET11 means both sites","refs":["ako/mxcli#509"],"ce":["CE6083"],"rules":["MDL-WIDGET11"]} +{"area":"mdl/executor","date":"2026-09-18","symptom":"A design property declared `\"multiSelect\": true` (Atlas `Hide on`) written as a flat value — `'Hide on': 'Phone'` — passes check and exec and fails the build with **CE6084** \"Expected design property Hide on to be of type Toggle button group, but found Option\". The same statement with `Align self` (same declared control type) is 0 errors","cause":"`multiSelect` was parsed nowhere in mxcli — `ThemeProperty` had no such field. `resolveDesignPropertyValueType` saw 'Phone' in the declared option list and returned `option`, so the write produced a plain `Forms$OptionDesignPropertyValue` where Mendix stores a compound","file":"`mdl/executor/theme_reader.go` (`ThemeProperty.MultiSelect`, `parseDesignPropertiesJSON`); `cmd_pages_builder_v3.go` (`astDesignPropToValueChecked`); `cmd_styling.go`; `validate_design_properties.go`; `validate_alter_styling.go`","insight":"**The reference document was already in the project — do not ask for one before grepping `mprcontents/`.** `grep -rl 'Hide on' mprcontents/` found three Studio Pro-authored Atlas pages in a blank 11.12.2 app; decoding one gave the shape in minutes: `Forms$DesignPropertyValue{Key:'Hide on', Value: Forms$CompoundDesignPropertyValue{Properties:[ Forms$DesignPropertyValue{Key:'Phone', Value: Forms$ToggleDesignPropertyValue} ]}}`. **That shape is `Spacing`, which MDL already writes — so the capability was never missing and the issue as filed (\"implement multi-select\") was wrong.** `DesignProperties: ['Hide on': ['Phone': on, 'Tablet': on]]` writes, round-trips through DESCRIBE, and builds at 0 errors; only the FLAT spelling was broken. So the fix is a refusal that names the compound spelling, not a feature. Note ALTER STYLING genuinely cannot express it — a `StylingAssignment` carries one flat value and the grammar has no nesting — so there it refuses and points at the inline form rather than writing something wrong. **Check the reported thing against its nearest sibling before theorising**: `Align self` and `Hide on` are both ToggleButtonGroup, both inherited, both valued with a declared option — `multiSelect` is the only difference, and one isolated run named it","refs":["ako/mxcli#511"],"ce":["CE6084"],"rules":["MDL-WIDGET12"]} +{"area":"mdl/executor","date":"2026-09-18","symptom":"CE7252 \"The parameters for remote action '' have changed\" (and CE7269 on the return) survives DROP + CREATE OR REPLACE MICROFLOW and CREATE OR MODIFY EXTERNAL ENTITIES, with no MDL that clears it. Reported as a stale 'parameter fingerprint / BSON hash' in the OData client and a request for REFRESH ODATA CLIENT Module.Client ACTIONS. Third report of this CE code after #1020 and #1073.","cause":"Two defects the same code was hiding. (1) edmReturnTypeToKind did not map Edm.TimeOfDay, so the call was written with no ParameterType / no VariableDataType - CE7252 on a parameter, CE7269 on a return - and neither field is reachable from MDL, both being derived from the contract. (2) For the types Mendix itself refuses (Edm.Duration/Stream/Binary/Geography*, a ComplexType, a TypeDefinition, Collection(Edm.*)) mxcli accepted the statement silently and left an unbuildable project; Mendix answers CE7255 'Action of service is not supported', which no BSON can change. A third case sat between them: an entity-typed PARAMETER whose external entity was never imported was unreported, though the identical case on the RETURN type already named the import statement.","file":"mdl/executor/external_action_types.go (new: classifyExternalActionType, checkExternalActionTypes); mdl/executor/cmd_microflows_builder_calls.go (edmReturnTypeToKind + refuseUntypableExternalAction); mdl/executor/validate_external_action_calls.go; mdl/types/edmx.go (FindEnumType)","insight":"**Enumerate the type space against mxbuild instead of theorising about the CE code.** One action per EDM shape, written by mxcli into a real 11.12.0 app and checked, produced a truth table in two runs, and the table is what separates OUR bug from the platform's: Edm.TimeOfDay is the only type that failed WITHOUT Mendix also calling it unsupported (CE7253/CE7255). Reasoning could not have reached that, and neither could a deny-list written from documentation. **The same table kills the obvious over-fix**: an ENUM-typed parameter builds at 0 errors with no type written at all, so 'mxcli could not name a Mendix type' is not on its own grounds to refuse - a refusal keyed on that would have rejected calls that build. **A rule wired only into the reference pass would have missed the reported workflow**: `mxcli exec` runs ValidateProgram(prog, projectPath) (the no-project linter), NOT Executor.ValidateProgram, so only `check --references` sees it; the refusal is applied at the writer too, from the same function, as CheckLayoutPlaceholderNames already does. Verified by building the faulty binary and watching exec write the call anyway. **There is no fingerprint** - the third reporter in a row believed one was stored; Mendix re-derives alignment from the cached contract every build, so 'recreate the microflow' really does rewrite everything and its failure means the rewrite is equally wrong, or the action is uncallable. **Controls**: reverting the TimeOfDay mapping alone takes the probe app 0 -> 2 errors (CE7252 + CE7269, the reported symptom verbatim) and, because the two halves are coupled, ALSO makes the refusal misfire on a call that builds - which is what the accepts-what-builds test catches. Stubbing checkExternalActionTypes to return nil restores the silent write. Repro mdl-examples/bug-tests/odata-1089-external-action-unmappable-types.mdl","refs":["mendixlabs/mxcli#1089","mendixlabs/mxcli#1073","mendixlabs/mxcli#1020"],"ce":["CE7252","CE7269","CE7255","CE7253"]} +{"area":"mdl/executor","date":"2026-09-18","symptom":"`Action:`/`OnClick:` on a `listview`, `staticimage` or `dynamicimage` is parsed, accepted, and dropped on write — the widget does nothing. Reported honestly as MDL-WIDGET23 with a workaround (wrap it in a `container`), so it read as a real capability gap","cause":"Only the BUILDERS were missing. `pages.ListView.ClickAction` and `pages.StaticImage/DynamicImage.OnClickAction` already existed, and the writers already serialised them (`widget_write.go` calls `clientActionToGen(x.ClickAction)`; `widget_write_legacy_gaps.go` does the same for both images). `buildListViewV3`/`buildStaticImageV3`/`buildDynamicImageV3` never called `w.GetAction()`, so the field was always nil","file":"`mdl/executor/cmd_pages_builder_v3_widgets.go` (three builders); `cmd_pages_describe_parse.go` + `cmd_pages_describe_output.go` (the read half); `validate_widget_onclick.go` (`clickCapableInMendix` now empty)","insight":"**Check the writer before believing a 'no writer' warning.** MDL-WIDGET23's own message said \"mxcli has no writer for it\" and that was wrong for all three — the field and the serializer were both already there, so the fix was one `if action := w.GetAction()` per builder, not a feature. A warning that names a remedy can outlive the gap it describes; `TestClickCapableInMendix_NoLongerNamesWhatIsWritten` now fails if a name is left behind. **The write half alone is not the fix**: it landed first, mxbuild reported 0 errors, and the action vanished on the next `describe -> exec` — valid BSON, clean build, construct gone. Prove it with a re-exec of the describe output and look for **`Unchanged page`**, which shows the emitted MDL rebuilds the stored document exactly; `Check passed!` does not. **Round-trip a DIFFERENT page than the one under test**: adding the emitter also printed `Editable: true` twice on every list view, because the shared property formatter already prints it from the same field — the parse side's own comment predicted exactly that and the new-page test could not see it. Two metamodel-sync tests asserted the gap still existed (`TestClickCapableTypesCarryClickActionInMetamodel`, and a case inside `TestMDLWIDGET23_ReportsTheDroppedAction`); invert such a test rather than deleting it, so the metamodel half keeps its guard","refs":["ako/mxcli#512"],"rules":["MDL-WIDGET23"]} +{"area": "mdl/executor", "date": "2026-09-18", "symptom": "A List View's \"Search attributes\" (its search bar) had no MDL spelling — `SearchAttributes: [Name]` on the widget was MDL-WIDGET07 \"not recognized and will be silently dropped on write\", so the search bar could only be set in Studio Pro", "cause": "Not modelled anywhere: `sdk/pages.DatabaseSource` had `XPathConstraint` and `Sorting` but no search, and `listViewSourceToGen` wrote an EMPTY `Forms$ListViewSearch` unconditionally", "file": "`mdl/grammar/MDLLexer.g4` (`SEARCH_BY`) + `domains/MDLPage.g4` + `domains/MDLSettings.g4` (keyword rule); `mdl/ast/ast_page_v3.go`; `mdl/visitor/visitor_page_v3.go`; `sdk/pages/pages_datasources.go`; `mdl/executor/cmd_pages_builder_v3.go`; `mdl/backend/modelsdk/widget_write.go`; `cmd_pages_describe_datasource.go`", "insight": "**No marketplace module ships a populated `ListViewSearch` — all 18 in a blank 11.12.2 app are empty — so pin the shape on the SIBLING instead.** `SearchRefs` is `[]*DomainModelsAttributeRef` and a Studio Pro `Forms$GridSortItem` carries the identical element, which gives a real reference without a Studio Pro session. Reuse the existing `attributeRefToGen` rather than writing a second builder: it means the new list inherits exactly what `sort by` has always written, including two deviations from Studio Pro that are then NOT regressions — typed-array marker **3** where Studio Pro writes **2**, and no `EntityRef: null` key. Verify that claim by dumping mxcli's own sort bars from the same project; both markers appear side by side (Studio Pro's 2, mxcli's 3). **A new lexer token needs the keyword rule too**: `TestKeywordRuleCoverage` fails otherwise, because every non-structural token must stay usable as an identifier — `SORT_BY` was already there and `SEARCH_BY` had to join it. Round-trip evidence to insist on is **`Unchanged page` on re-exec of the describe output**, not `Check passed!`: it proves the emitted MDL rebuilds the stored document byte-for-byte after canon.Reconcile. Measured gap left alone deliberately: neither `sort by` nor `search by` is reference-checked, so a nonexistent attribute passes `check --references` and surfaces as CE1613 at build time — checked on both clauses before concluding the new one was no worse **Review caught a silent drop in this very change**: `search by` hangs off the SHARED database-source rule, so the grammar accepts it on a gallery or a grid, and only the list view writer emits it — on a gallery, check passed, exec said \"Created page\", and DESCRIBE did not echo it back. **When a clause is added to a shared grammar rule, enumerate the OTHER widgets that rule serves and run one of them**; the round-trip proving it works on a list view says nothing about them. Now refused at build with the widget named, as an error not a warning: unlike an unrecognised property key, which a newer widget package might define, this is decided by Mendix's metamodel and cannot become valid later.", "refs": ["ako/mxcli#512"], "ce": ["CE1613"]} +{"area": "mdl/executor", "date": "2026-09-18", "symptom": "`describe page` emits `-- Forms$StaticImageViewer (imgAll) -- NOT re-executable: mxcli cannot author this widget, so re-running this script would drop it` for the Studio Pro-authored static images in a Selection helper's three mandatory custom slots. Replaying the description empties all three and mxbuild answers CE0642 \"Property 'All selected' is required.\" x3. The reporter's workaround is an empty `dynamictext` per slot: structurally valid, icons gone", "cause": "Two independent halves, both absent. (1) `Forms$StaticImageViewer` had no case in cmd_pages_describe_parse.go OR cmd_pages_describe_output.go, so it fell through to the unknown-type note \u2014 even though `staticimage` has been a keyword the executor dispatches and a widget both writers serialise, for years. (2) MDL had no spelling for WHICH image it shows: staticImageToGen wrote `SetImageQualifiedName(\"\")` under a comment saying `MDL cannot name an image (the builder never fills ImageID)`, and pages.StaticImage carried a dead `ImageID model.ID` that nothing ever set and that named the wrong thing \u2014 Forms$StaticImageViewer.Image is a ByNameRef to Images$Image, so a three-part NAME (Module.Collection.Image) is what is stored. A third, smaller drop sat beside them: the generic pluggable-widget emit branch was gated on explicit properties / object lists / actions and NOT on ChildSlots, so a widget whose only content is a populated slot described as a bare head", "file": "`mdl/executor/cmd_pages_describe_parse.go` + `cmd_pages_describe_output.go` (read and emit), `mdl/executor/cmd_pages_builder_v3_widgets.go` (buildStaticImageV3), `mdl/backend/modelsdk/widget_write_legacy_gaps.go` (staticImageToGen), `sdk/pages/pages_widgets_display.go` (ImageName replaces the dead ImageID), `mdl/executor/validate_widgets.go` (MDL-WIDGET07 allow-list)", "insight": "**A widget that is authorable is not therefore describable, and the two gaps hide each other.** `staticimage` had a keyword, a grammar token, a builder and two writers \u2014 everything a coverage scan looks for \u2014 and no describer at all, which is why the note read as a capability gap rather than a missing 20-line case. Grep the describe switch for the `$Type`, not the keyword. **Emitting the keyword alone would have been worse than the bug**: a visible `NOT re-executable` note becomes a silent drop. That is the third time this exact trap is recorded on this exact widget (ako/mxcli#512 for `Action:`, mxcli-formula1 FINDINGS \u00a7142 for the pluggable `image`), so treat 'describe emits it' and 'the builder reads it' as one change, never two. **Prove the round trip with `Unchanged page`, not with `Check passed`** \u2014 re-exec of the describe output printing `Unchanged` is the only signal that the emitted MDL rebuilds the stored document exactly; the pre-fix binary printed `Replaced page` on the same input. **The decisive control was a second binary, not a stubbed function**: `git stash` the fixed files, build, and run describe->exec on the SAME stored page \u2014 it reproduced CE0642 x3 verbatim, which no unit test could have claimed. Reverting only the writer (`Image` back to \"\") gives CE0436 \"No image selected.\", which is what proves the three-part name RESOLVES rather than merely being accepted. **CE0582 is not evidence of a defect here**: mxbuild 11.12.1 reports it for a static image anywhere, slots included \u2014 Mendix's own React-client deprecation, and the reason `image` is the right widget on a new page **Making a widget describable puts its OTHER properties at risk**, and it is the same failure class one level down: the writer hardcoded WidthUnit/HeightUnit to \"Auto\" and the builder hardcoded Responsive to true, harmlessly while nothing described the widget \u2014 and the moment describe emits re-executable MDL those become a SILENT normalisation on every replay, worse than the loud note they replaced. When you make something round-trip, enumerate what the type stores and decide each one; the defaults still stay unemitted, or each round trip accumulates a clause the author never wrote", "refs": ["mendixlabs/mxcli#1057", "ako/mxcli#512"], "ce": ["CE0642", "CE0436", "CE0582", "CE1613"], "rules": ["MDL-WIDGET07"]} diff --git a/.claude/skills/fix-issue/findings/mdl-other.jsonl b/.claude/skills/fix-issue/findings/mdl-other.jsonl index 15769f85a1..24638a84ef 100644 --- a/.claude/skills/fix-issue/findings/mdl-other.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-other.jsonl @@ -66,3 +66,4 @@ {"area": "mdl/exprcheck", "date": "2026-09-16", "symptom": "`$out = $out + $r/Status` inside `LOOP $r IN $reqs` (Enumeration into a String) passes `mxcli check -p --references`, is written by `exec`, and fails the native build with **CE0117** at the Change variable activity. The same mistake on a PARAMETER is refused as E004, so the checker looks like it is skipped inside LOOP bodies (mendixlabs/mxcli#1100)", "cause": "The loop BODY was walked and checked all along \u2014 the control that proves it is `'status=' + $T/Status` on a parameter written one line INSIDE the loop, which was refused before the fix. Two holes in the variable scope, in series, produced the asymmetry: (a) `buildVarEntityScope` recorded CREATE, database RETRIEVE and parameters but never `LoopStmt.LoopVariable`, so `$r/Status` resolved to no attribute and inferred KindUnknown, which every rule tolerates by design; (b) `CheckAdapter` never set `Context.Scope` at all, so a DECLARE'd `$out String` was Unknown too \u2014 and E004 needs BOTH operands typed, so closing (a) alone still reported nothing on the reported script. (b) also meant `$out = $out + $Req/Status` with no loop in sight was equally silent; the report's own case A hides that by using a string literal on the left", "file": "`mdl/exprcheck/adapters/adapter_scope.go` (`buildFlowScope` replacing `buildVarEntityScope`, `recordRetrieve`/`recordListOperation`/`recordDeclare`, `kindScope`, `StatementErrorHandling`, `DataTypeKind`), `mdl/exprcheck/adapters/check.go` (`walkFlow` passes Scope; `checkListOperationCondition`; ON ERROR bodies walked), `mdl/exprcheck/slot_resolver.go` + `slot_to_context.go` (`ListOperation.Condition`), `mdl/executor/validate_microflow.go` (delegates `astKindToExprKind` and `stmtErrorHandling`)", "insight": "**Separate \"was the walk there\" from \"did the variable resolve\" before believing a skipped-construct report.** The title said LOOP bodies were not checked; one control \u2014 the same expression on a parameter, one line deeper \u2014 showed the walk was fine and the scope was not, which changed the fix from a walk to a resolver. **A silence can need two fixes to break**: typing the loop variable alone left the reported script still reporting nothing, because the rule needs both operands. Fix one, re-measure, and do not conclude the fix failed. **The same walk already existed, correct, next door**: `mdl/executor/validate_member_refs.go` typed loop variables from the list; the expression checker's walk did not \u2014 duplicate-resolver drift, which is why `stmtErrorHandling` and the DataTypeKind table are now single copies in adapters with the executor delegating. **Order is load-bearing and silent when wrong**: parameters must seed the scope BEFORE the body walk, or an association retrieve off a parameter (and every loop over its result) stays untyped \u2014 this was written the old way first and only a test caught it. **False-positive control**: exec-then-type-check over 591 mdl-examples scripts, 11 violations before and 11 after, same rules. It earned its keep \u2014 the first cut fired E009 on `set $At = find($Hay, $Needle)`, Mendix's STRING find, which the visitor still builds as a ListOperationStmt (the flow builder disambiguates it later, ledger #63). Requiring a KNOWN element entity before checking a FIND/FILTER predicate applies the same disambiguation. Controls: each of the four scope sources reverted in turn fails a distinct test with the reported symptom (empty violations). **Still open**: a bare attribute name in a FILTER predicate resolves to nothing, `retrieve \u2026 limit 1` is typed as a list like any other retrieve, and `LOOP $r IN $T/Mod.Assoc` cannot be typed because the visitor drops the association path (`ListVariable` is empty)"} {"area": "mdl/catalog", "date": "2026-09-17", "symptom": "Every microflow behind a published REST endpoint reads as dead: `CATALOG.GRAPH_DEAD_ASSETS` lists it, `SHOW CALLERS OF` says \"(no callers found)\", `SHOW REFERENCES TO` and `impact` report nothing, and QUAL004 says \"is not called from anywhere. Remove if unused.\" On the reporter's model, 92 of 93 published operations name a microflow and all 92 were listed dead \u2014 15 percent of its dead-microflow list, pointed at the most exposed code in the app (mendixlabs/mxcli#1126)", "cause": "`buildPublishedRestServices` wrote `published_rest_operations_data.Microflow` and returned \u2014 it appended to no slice that `buildReferences` drains, and there was no RefKind for the edge. The binding was in the catalog; the edge was not. Fourth instance of one class (widget actions #773, scheduled events, project settings, this)", "file": "`mdl/catalog/builder.go` (`publishedRestRefs`), `mdl/catalog/builder_rest.go` (`publishedRestRef`, `publishedRestOpName`, collection in the operation loop), `mdl/catalog/builder_references.go` (`RefKindPublish`, `extractPublishedRestRefs`), `mdl/catalog/builder_graph.go` (`graphRefKinds`), `mdl/executor/cmd_search.go` (`callerRefKinds`), `.claude/lint-rules/orphaned_elements.star` (`MICROFLOW_ENTRY_KINDS`); tests `mdl/catalog/builder_rest_refs_test.go`, `mdl/catalog/lint_rule_vocabulary_test.go`, `mdl/executor/cmd_search_callers_test.go`", "insight": "**`GRAPH_DEAD_ASSETS` is kind-AGNOSTIC \u2014 the comment beside `schedule` in `graphRefKinds` says otherwise and is wrong.** The view is `NOT EXISTS (SELECT 1 FROM refs WHERE TargetName = \u2026)`; `git log -L` shows it has never filtered on RefKind. That false comment sent the issue's root-cause analysis down the wrong path, and it would have sent the fix there too: `graphRefKinds` matters for the ANALYSIS graph (communities/layers/cycles/centrality), not for the dead list. Measured with a one-kind insert: a `publish` row in neither `graphRefKinds` nor `callerRefKinds` still took the microflow from dead=1 to dead=0. **Check which consumers actually filter before assuming all four do**: `impact` and `SHOW REFERENCES TO` select every kind, so the refs row alone fixes them; only `SHOW CALLERS` and QUAL004 need a vocabulary edit. **The three vocabularies drift independently and nothing tests the union** \u2014 `settings` shipped in v0.22.0 into refs and into QUAL004 but NOT into `callerRefKinds`, so `show callers of ` was still blind two releases later; found only by auditing the lists while adding a fourth kind, and fixed here alongside. **`sync` looks like an entry point and is not**: it targets an ENTITY, so it belongs with `datasource`/`retrieve` in the excluded set \u2014 the test now pins it there, because the next person adding a kind will read the list, not the builder. **Carry the source's own id on the edge**: the operation's synthetic `opID` was already computed for `published_rest_operations_data`, so passing it as `SourceId` (the scheduled-event precedent passes \"\") makes 'who calls this microflow' one join from the endpoint's path and summary. **Controls**: stubbing the extractor to emit nothing reproduces \"reported dead\" verbatim, and dropping the empty-microflow guard fails the two-operation test \u2014 the suite is green against neither. Adjacent and NOT fixed: `business_events_data.PublishMicroflow`/`SubscribeMicroflow` emit no edge either, and `PublishedRestService.AuthenticationMicroflow` (and its OData sibling) is on gen but never read into the semantic model, so a REST auth handler is invisible to mxcli entirely", "refs": ["mendixlabs/mxcli#1126", "mendixlabs/mxcli#773"], "rules": ["QUAL004"]} {"area": "mdl/catalog", "date": "2026-09-17", "symptom": "A microflow that runs only as an **entity event handler** is reported as unused from three directions at once: `show callers of Mod.ACT_Order_Validate` says `(no callers found)`, `CATALOG.GRAPH_DEAD_ASSETS` lists it, and `mxcli lint` emits `[QUAL004] ... is not called from anywhere.` with the suggestion **\"Remove if unused\"** \u2014 on a microflow that runs on every commit. Reported as 32 dead of 36 handlers across 24 entities", "cause": "`mdl/catalog` touched `Entity.EventHandlers` in exactly one place and threw the list away: `hasEventHandlers = 1` in `builder_modules.go`. No `refs` row was ever emitted, and no table held the handlers, so the reference graph had no ENTITY -> MICROFLOW edge for them. The `calculate` edge two lines below in `buildReferences` is the same shape and was already there, which is why the infrastructure looked complete", "file": "`mdl/catalog/builder_entity_events.go` (new), `mdl/catalog/builder_references.go` (`RefKindEvent` + `extractEventHandlerRefs`), `mdl/catalog/tables.go` (`entity_event_handlers_data` + view, `CatalogSchemaVersion` 11->12), `mdl/catalog/catalog.go` (`Tables()`), `mdl/catalog/builder.go` (field + build step), `mdl/catalog/builder_graph.go` (`graphRefKinds`), `mdl/executor/cmd_search.go` (`callerRefKinds`), `.claude/lint-rules/orphaned_elements.star`", "insight": "**The third consumer of a new RefKind is a schema version, not a list.** Beyond the three kind lists the scheduled-event fix named (`callerRefKinds`, `graphRefKinds`, the QUAL004 rule), a new edge needs `CatalogSchemaVersion` bumped: refs are only written by REFRESH CATALOG FULL and `NewFromFile` applies the schema with CREATE TABLE IF NOT EXISTS, so without the bump an existing `.mxcli/catalog.db` gains the empty table and keeps serving the pre-fix edge set \u2014 the wrong answer, from a cache, after the fix shipped. `migrateIfSchemaMismatch` drops and rebuilds on a mismatch (verified by hand-editing catalog_meta back to '11'). **A flag is a missing table wearing a value**: `HasEventHandlers` and `NavigationProfile.OfflineEntityCount` are the same defect, and the fix is the same pair \u2014 rows for what it does, an edge for whether it is reachable. Do not encode the detail in the kind: eight kinds (`before_commit`, `after_delete`, ...) would enter every consumer's list to say one thing, so the moment/event go in the table and the edge stays one `event`. **The control has to be the binary, not the test**: stubbing `extractEventHandlerRefs` to emit nothing and rebuilding reproduced `(no callers found)` + 2 dead microflows on the same project, which is what proves the assertion detects something. mendixlabs/mxcli#1127; repro `mdl-examples/bug-tests/catalog-1127-entity-event-handler-refs.mdl`", "refs": ["mendixlabs/mxcli#1127"]} +{"area":"mdl/catalog","date":"2026-09-18","symptom":"A Starlark lint rule written from the bundled write-lint-rules skill matches zero rows and reports a clean pass — or, with an allowlist, inverts into flagging everything (138 of 282 ACT_ microflows on one real project, 49% false positives)","cause":"The skill's example tables are the only documentation of the lint API and nothing tied them to the values the catalog emits. action_type listed Mendix BSON *storage* names (CreateChangeAction, CommitAction, ShowFormAction, CloseFormAction, ShowHomeFormAction) against a catalog that labels an action with its SDK name via getMicroflowActionType; source_type/target_type/element_type/access_type were lower-cased against an upper-case vocabulary; data_type was lower-cased against TitleCase from AttributeType.GetTypeName","file":"`.claude/skills/mendix/write-lint-rules/SKILL.md` (six rows), `mdl/catalog/builder_references.go` (RefObject* constants + RefSourceObjectTypes/RefTargetObjectTypes), `mdl/catalog/builder_permissions.go` (PermissionElement*/AccessType* constants), test `mdl/catalog/lint_rule_doc_vocabulary_test.go`","insight":"**Fix the documentation the rule author reads, not just the rule that was reported.** The identical defect was found and fixed in CONV010's allowlist a month earlier (finding 2026-08-17, pinned by lint_rule_vocabulary_test.go) — and recurred, because the *source* the author copied from was never corrected. A rule pinned to the labeller and a doc that is not is one fix, not two. **Check the sibling rows before believing the report's scope**: element_type, access_type and data_type had the same lower-casing and nobody had reported them; data_type ('string' vs 'String') is the most-used filter in a lint rule, so it was the most expensive one. **A doc value is only pinnable against a named vocabulary**, so the fix is half refactor: the emitters' scattered ALL-CAPS literals became RefObject*/PermissionElement*/AccessType* constants with published lists, mirroring the SourceObjectTypes precedent already in this package. action_type needs no list — the label IS the Go type name (%T), so the test reads the isMicroflowAction marker methods out of sdk/microflows/microflows_actions.go with go/ast. **Three legs, not one, when proving a filter fix**: old values -> 7/7 flagged, corrected -> 0, corrected-minus-one -> exactly that one. Leg C is the control; without it a silent rule and a correct rule both report zero, which is the bug itself","refs":["mendixlabs/mxcli#1027"],"rules":["CONV010"]} diff --git a/.claude/skills/mendix/create-page/reference/widgets.md b/.claude/skills/mendix/create-page/reference/widgets.md index 29562f62a0..ef2a700073 100644 --- a/.claude/skills/mendix/create-page/reference/widgets.md +++ b/.claude/skills/mendix/create-page/reference/widgets.md @@ -658,7 +658,7 @@ Display images on pages: ```sql -- Image with dimensions (responsive by default) image imgLogo (width: 200, height: 100) -staticimage imgBanner (width: 400, height: 120) +staticimage imgBanner (Image: 'MyModule.Images.banner', width: 400, height: 120) -- Dynamic image (from entity data source, e.g. inside a DataView) dynamicimage imgProduct (width: 300, height: 200) @@ -669,6 +669,32 @@ image imgIcon **Properties:** `width: integer`, `height: integer`, `AlternativeText: 'text'`, `WidthUnit: pixels | percentage | auto`, `HeightUnit: pixels | percentage | auto`, `Responsive: true | false`, `DisplayAs: fullImage | thumbnail | icon`, `class: 'css'`, `style: 'css'` +#### `Image:` — which image a STATICIMAGE shows + +`Image:` names an entry in an image collection, as the three-part qualified name +`Module.Collection.Image` — the same shape `Icon:` and the pluggable `image` +widget use, because all three are by-name references to the same `Images$Image` +element. `describe image collection Module.Images` lists the names. + +Without it the widget is written with no reference and mxbuild reports +**CE0436 "No image selected."** Until mendixlabs/mxcli#1057 there was no way to +say it at all, so `describe page` marked every stored static image +`-- NOT re-executable` and the round trip dropped it. + +```sql +staticimage imgAllSelected (Image: 'MyFirstModule.Images.gallery') +``` + +`WidthUnit:`, `HeightUnit:` (`pixels` | `percentage` | `auto`) and +`Responsive: false` are written too. Leave them out for Studio Pro's defaults — +auto units and a responsive image — which `describe page` also omits, so a +round trip neither loses them nor invents them. + +Mendix 11's React client reports **CE0582** for `staticimage` wherever it +appears — it is deprecated in favour of the pluggable `image` widget, which +takes the same `Image:`. mxcli still writes it, because round-tripping a model +that already contains one is the point; prefer `image` on a new page. + #### Setting Image Source (PLUGGABLEWIDGET syntax) The IMAGE shorthand creates a pluggable Image widget. For advanced properties like image source, use PLUGGABLEWIDGET syntax: @@ -754,9 +780,10 @@ Two shapes, two remedies: the input widgets, `groupbox`, `tabcontainer`, `layoutgrid`, `snippetcall`) — put the action on a `container` inside the widget. A container renders with `tabindex="0" role="button"`, so it is the correct modelling, not a workaround. -- **Mendix models one but mxcli cannot write it yet** (`listview`, - `staticimage`, `dynamicimage`) — the container is a workaround here; the model - could hold the action. +- **Mendix models one but mxcli cannot write it yet** — nobody is in this group + today. `listview`, `staticimage` and `dynamicimage` were, and their actions + have been written since ako/mxcli#512; MDL-WIDGET23's second message survives + so the next such gap has a sentence, not because one is open. ```sql -- WRONG: silently does nothing diff --git a/.claude/skills/mendix/write-lint-rules/SKILL.md b/.claude/skills/mendix/write-lint-rules/SKILL.md index 99eccf45bb..35dde202dc 100644 --- a/.claude/skills/mendix/write-lint-rules/SKILL.md +++ b/.claude/skills/mendix/write-lint-rules/SKILL.md @@ -119,6 +119,33 @@ def check(): ## Object Properties +> **The example values below are the real ones — do not adapt their case or their +> spelling.** A filter on a value the catalog never emits is silent: the rule +> compiles, runs, matches nothing and reports a clean pass. Two traps in +> particular: +> +> - **Case is not cosmetic.** Document and element kinds are upper-case +> (`"MICROFLOW"`, `"ENTITY"`, `"READ"`), attribute data types are TitleCase +> (`"String"`, `"DateTime"`), and `ref_kind` is lower-case (`"call"`, +> `"show_page"`). Guessing wrong matches zero rows. +> - **`action_type` is the SDK name, never Mendix's BSON storage name.** The +> catalog reports `ShowPageAction` / `ClosePageAction` / `CreateObjectAction` / +> `CommitObjectsAction`; the storage names `ShowFormAction`, `CloseFormAction`, +> `CreateChangeAction` and `CommitAction` that appear in `.mpr` documents never +> reach a rule. A rule that allow-lists the storage names flags every microflow +> that opens a page — the inversion measured at 49% false positives in +> mendixlabs/mxcli#1027. +> +> To check a value against your own project rather than trusting any list: +> +> ```bash +> sqlite3 .mxcli/catalog.db "SELECT DISTINCT ActionType FROM activities;" +> sqlite3 .mxcli/catalog.db "SELECT DISTINCT SourceType, TargetType, RefKind FROM refs;" +> ``` +> +> Absence from your project means the construct is not used there; a value absent +> from the tables below is one the catalog never produces anywhere. + ### entity | Property | Type | Example | |----------|------|---------| @@ -300,7 +327,7 @@ def count_not(node): | `entity_id` | string | Parent entity UUID | | `entity_qualified_name` | string | `"Sales.Customer"` | | `module_name` | string | `"Sales"` | -| `data_type` | string | `"string"`, `"integer"`, `"datetime"`, etc. | +| `data_type` | string | `"String"`, `"Integer"`, `"Long"`, `"Decimal"`, `"Boolean"`, `"DateTime"`, `"Date"`, `"Enumeration"`, `"AutoNumber"`, `"Binary"`, `"HashedString"` | | `length` | int | Field length (for strings) | | `is_unique` | bool | Has unique constraint | | `is_required` | bool | Is required | @@ -314,8 +341,8 @@ def count_not(node): | `id` | string | Activity UUID | | `name` | string | Activity name | | `caption` | string | Activity caption | -| `activity_type` | string | `"ActionActivity"`, `"ExclusiveSplit"`, `"LoopedActivity"`, etc. | -| `action_type` | string | `"CreateChangeAction"`, `"CommitAction"`, `"ShowFormAction"`, etc. | +| `activity_type` | string | `"ActionActivity"`, `"ExclusiveSplit"`, `"ExclusiveMerge"`, `"LoopedActivity"`, `"InheritanceSplit"`, `"StartEvent"`, `"EndEvent"` | +| `action_type` | string | The action inside an `ActionActivity`: `"CreateObjectAction"`, `"ChangeObjectAction"`, `"CommitObjectsAction"`, `"DeleteObjectAction"`, `"RetrieveAction"`, `"MicroflowCallAction"`, `"ShowPageAction"`, `"ClosePageAction"`, `"LogMessageAction"`, `"JavaActionCallAction"`. Empty for an activity that is not an action | | `microflow_id` | string | Parent microflow UUID | | `microflow_qualified_name` | string | `"Sales.ACT_Customer_Create"` | | `module_name` | string | `"Sales"` | @@ -328,11 +355,11 @@ Returned by `permissions()` (all types) or `permissions_for()` (entity-specific) | Property | Type | Example | |----------|------|---------| | `module_role_name` | string | `"Admin"` | -| `element_type` | string | `"entity"`, `"microflow"`, `"page"`, `"ODATA_SERVICE"` (from `permissions()` only) | +| `element_type` | string | `"ENTITY"`, `"MICROFLOW"`, `"PAGE"`, `"ODATA_SERVICE"` (from `permissions()` only) | | `element_name` | string | `"Sales.Customer"` | | `module_name` | string | `"Sales"` | | `entity_name` | string | `"Sales.Customer"` (from `permissions_for()` only) | -| `access_type` | string | `"create"`, `"read"`, `"write"`, `"delete"`, `"execute"`, `"view"`, `"access"`, `"MEMBER_READ"`, `"MEMBER_WRITE"` | +| `access_type` | string | `"CREATE"`, `"READ"`, `"WRITE"`, `"DELETE"` (entity), `"EXECUTE"` (microflow), `"VIEW"` (page), `"ACCESS"` (OData service), `"MEMBER_READ"`, `"MEMBER_WRITE"` | | `member_name` | string | Attribute name (for MEMBER_READ/MEMBER_WRITE) | | `xpath_constraint` | string | XPath constraint or empty | | `is_constrained` | bool | True if XPath constraint is set | @@ -361,13 +388,13 @@ Returned by `permissions()` (all types) or `permissions_for()` (entity-specific) ### reference | Property | Type | Example | |----------|------|---------| -| `source_type` | string | `"microflow"`, `"page"`, etc. | +| `source_type` | string | The document the edge comes FROM, upper-case: `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"SNIPPET"`, `"ENTITY"`, `"ASSOCIATION"`, `"WORKFLOW"`, `"NAVIGATION"`, `"SCHEDULED_EVENT"`, `"PUBLISHED_REST_OPERATION"`, `"PROJECT_SETTINGS"` | | `source_id` | string | Source UUID | | `source_name` | string | `"Sales.ACT_Customer_Create"` | -| `target_type` | string | `"entity"`, `"microflow"`, etc. | +| `target_type` | string | What it points AT, upper-case: `"ENTITY"`, `"ASSOCIATION"`, `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"LAYOUT"`, `"WORKFLOW"`, `"WIDGET"`, `"JAVA_ACTION"`, `"REST_OPERATION"`, `"REGULAR_EXPRESSION"`. `LAYOUT` and `WIDGET` are only ever targets; `SCHEDULED_EVENT` and `PROJECT_SETTINGS` only ever sources | | `target_id` | string | Target UUID | | `target_name` | string | `"Sales.Customer"` | -| `ref_kind` | string | Reference kind | +| `ref_kind` | string | How it references: `"call"`, `"create"`, `"retrieve"`, `"change"`, `"delete"`, `"show_page"`, `"datasource"`, `"action"`, `"layout"`, `"parameter"`, `"return"`, `"generalize"`, `"associate"`, `"home_page"`, `"login_page"`, `"menu_item"`, `"calculate"`, `"schedule"`, `"validate"`, `"settings"`, `"widget"`, `"sync"`, `"publish"`, `"event"` — lower-case, unlike the types above | | `module_name` | string | Source module | ### project_security diff --git a/.claude/skills/mendix/write-microflows/SKILL.md b/.claude/skills/mendix/write-microflows/SKILL.md index 21675580de..a5e7e875c5 100644 --- a/.claude/skills/mendix/write-microflows/SKILL.md +++ b/.claude/skills/mendix/write-microflows/SKILL.md @@ -66,7 +66,7 @@ If you're not sure whether the logic belongs in a microflow or a nanoflow, read | **JavaScript actions** | Not supported | Supported | | **SYNCHRONIZE** | Not available | Available (offline sync) | | **File downloads** | Supported | Not supported | -| **Error handling** | Full `ON ERROR` blocks + `RAISE ERROR` | Per-action `ON ERROR` supported; `RAISE ERROR` / `ErrorEvent` forbidden | +| **Error handling** | Full `ON ERROR` blocks; `RAISE ERROR` **inside a handler only** (main flow = MDL084 / CE0710) | Per-action `ON ERROR` supported; `RAISE ERROR` / `ErrorEvent` forbidden | | **Offline** | Not available | Available | | **Binary return type** | Supported | Not supported | diff --git a/.claude/skills/mendix/write-microflows/reference/control-flow.md b/.claude/skills/mendix/write-microflows/reference/control-flow.md index 022bc5582d..76fdb7ac88 100644 --- a/.claude/skills/mendix/write-microflows/reference/control-flow.md +++ b/.claude/skills/mendix/write-microflows/reference/control-flow.md @@ -353,6 +353,41 @@ commit $Order on error without rollback { | `on error { ... }` | Execute handler block, then continue (with rollback) | | `on error without rollback { ... }` | Execute handler block, keep database changes | +### RAISE ERROR is handler-only + +`raise error;` builds Mendix's **error event**, which *re-raises the error +currently being handled*. Mendix therefore allows one only where an error is in +scope — that is, inside an `on error { ... }` block. Studio Pro will not even +let you draw the connection from the normal flow to an error event. + +```mdl +-- ✅ inside a handler: an error IS in scope +call microflow Module.RiskyOperation() +on error { + log error node 'Module' 'failed, re-raising'; + raise error; +}; + +-- ❌ on the main flow: MDL084, and mxbuild rejects it with +-- CE0710 "The main flow cannot join an error flow or end in an error event." +create microflow Module.Fail () +begin + raise error; +end; +``` + +Nesting does not change this: a `raise error;` inside an `if` or a `loop` on the +main flow is still on the main flow, and one inside a branch of a handler body is +still on the error flow. + +Mendix has **no main-flow "throw" activity**. To fail deliberately from the normal +path, call a Java action that throws: + +```mdl +create java action Module.JA_RaiseTechnicalError(Message: string not null) returns boolean as +$$ throw new com.mendix.systemwideinterfaces.MendixRuntimeException(Message); $$; +``` + ### When to Use Each Type - **CONTINUE**: Non-critical operations where failure is acceptable diff --git a/CHANGELOG.md b/CHANGELOG.md index cb9abb0b68..a63e97aceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- **`raise error;` on a microflow's main flow passed check and exec, then failed the build** (mendixlabs/mxcli#1030) — with `[error] [CE0710] "The main flow cannot join an error flow or end in an error event."`, one per microflow. Mendix's error event *re-raises the error being handled*, so it is legal only where an error is in scope: inside an `on error { … }` handler. Studio Pro will not draw the connection from the normal flow to an error event; mxcli could, and did. It is now **MDL084**, at error severity, so `exec`'s pre-flight refuses the script with nothing written (`--no-check` still applies it, for reproducing the build failure). + + The report's own diagnosis — a trailing End event appended because `RaiseErrorStmt` never set the builder's "ends with return" flag — is not the cause: `isTerminalStmt` has treated it as a terminator all along, and the graph mxcli builds for `raise error;` is exactly the one the report asks for (start event, error event, one sequence flow, no trailing End event, no outgoing flow), pinned by a test. No wiring makes a main-flow error event legal, which is why the fix is a refusal rather than a builder change. + + Nesting does not exempt anything: a raise inside an `if`, a `loop` or a split on the main flow is still on the main flow, while one inside a branch of a handler body is still on the error flow. Rules are covered too — a rule goes through the same flow builder. Measured on mxbuild 11.13.0, two copies of a blank app from a 0-error baseline: the four reported shapes → 4× CE0710; the identical statement inside a handler → 0 errors. + - **Every microflow behind a published REST endpoint read as dead** (mendixlabs/mxcli#1126). The catalog recorded each operation's microflow in `published_rest_operations_data.Microflow` and emitted no `refs` edge to it, so a microflow whose only caller is an endpoint had zero inbound references: `CATALOG.GRAPH_DEAD_ASSETS` listed it, `SHOW CALLERS OF` answered "(no callers found)", `SHOW REFERENCES TO` and `impact` reported nothing, and lint rule **QUAL004** said "is not called from anywhere. Remove if unused." On the reported model 92 of 93 operations name a microflow and all 92 were listed dead — 15 percent of its dead-microflow list, aimed at the most exposed code in the app. A published REST operation is an entry point of the same shape as a scheduled event — the platform invokes it, so nothing in the model calls it — and now emits a **`publish`** edge from the operation to the microflow it runs. The source is the operation, not the service, so `show references to` names the one endpoint rather than the service holding thirty of them, and the edge carries the operation's catalog id, so its path and summary are one join away. diff --git a/cmd/mxcli/cmd_syntax_test.go b/cmd/mxcli/cmd_syntax_test.go new file mode 100644 index 0000000000..27f42f6a5c --- /dev/null +++ b/cmd/mxcli/cmd_syntax_test.go @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "context" + "strings" + "testing" +) + +// runSyntax executes `mxcli syntax ` and returns the combined output. +// +// Uses rootCmd.SetArgs + rootCmd.Execute because subcommand writers are +// inherited by walking up to the root. +func runSyntax(t *testing.T, args ...string) string { + t.Helper() + resetCmdFlags(syntaxCmd) + + var out bytes.Buffer + rootCmd.SetOut(&out) + rootCmd.SetErr(&out) + rootCmd.SetArgs(append([]string{"syntax"}, args...)) + if err := rootCmd.ExecuteContext(context.Background()); err != nil { + t.Fatalf("mxcli syntax %v: %v", args, err) + } + return out.String() +} + +// mendixlabs/mxcli#1025: `mxcli syntax` advertises +// +// mxcli syntax workflow user-task targeting # Drill down to targeting +// +// and an agent that passes the topic as ONE string — which is what a tool +// wrapper, a quoted copy-paste or `sh -c` produces — got +// "Unknown topic: workflow user-task targeting" back, verbatim, down to the +// spaces. Reproduced on the reported v0.20.0 binary and on main. +// +// The REPL's `help` has resolved multi-word topics since it was written; the +// CLI joined its arguments and never split them. One question, two answers. +func TestSyntaxTopicSpellings(t *testing.T) { + // Every spelling of the same topic must reach the same page. + tests := []struct { + name string + args []string + }{ + {"separate arguments", []string{"workflow", "user-task", "targeting"}}, + {"one quoted argument", []string{"workflow user-task targeting"}}, + {"dotted path", []string{"workflow.user-task.targeting"}}, + {"words, no hyphen", []string{"workflow", "user", "task", "targeting"}}, + {"one quoted argument, no hyphen", []string{"workflow user task targeting"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out := runSyntax(t, tt.args...) + if strings.Contains(out, "Unknown topic") { + t.Errorf("mxcli syntax %q reported an unknown topic:\n%s", + strings.Join(tt.args, " "), firstLines(out, 3)) + } + if !strings.Contains(out, "workflow.user-task.targeting") { + t.Errorf("mxcli syntax %q did not reach workflow.user-task.targeting:\n%s", + strings.Join(tt.args, " "), firstLines(out, 3)) + } + }) + } +} + +// The other two commands the report names, plus the second advertised example. +func TestSyntaxQuotedTopicsFromIssue1025(t *testing.T) { + for _, topic := range []string{ + "workflow user-task", + "workflow parallel-split", + "security entity-access", + } { + t.Run(topic, func(t *testing.T) { + out := runSyntax(t, topic) + if strings.Contains(out, "Unknown topic") { + t.Errorf("mxcli syntax %q reported an unknown topic:\n%s", topic, firstLines(out, 3)) + } + }) + } +} + +// Every example in the command's own help text must resolve, whether its topic +// arrives as separate words or as one string. The help block is what the +// reporter followed. +func TestSyntaxHelpExamplesResolve(t *testing.T) { + for _, line := range strings.Split(syntaxCmd.Long, "\n") { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "mxcli syntax ") { + continue + } + if i := strings.Index(line, "#"); i >= 0 { + line = strings.TrimSpace(line[:i]) + } + var args, flags []string + for _, w := range strings.Fields(strings.TrimPrefix(line, "mxcli syntax ")) { + if strings.HasPrefix(w, "-") { + flags = append(flags, w) + continue + } + args = append(args, w) + } + if len(args) == 0 { + continue + } + t.Run(strings.Join(args, "_"), func(t *testing.T) { + if out := runSyntax(t, append(append([]string{}, args...), flags...)...); strings.Contains(out, "Unknown topic") { + t.Errorf("help advertises %q, which reports an unknown topic:\n%s", + line, firstLines(out, 3)) + } + // The same topic handed over as ONE string is the shape that + // failed in #1025. + quoted := append([]string{strings.Join(args, " ")}, flags...) + if out := runSyntax(t, quoted...); strings.Contains(out, "Unknown topic") { + t.Errorf("help advertises %q; quoted as one argument it reports an unknown topic:\n%s", + line, firstLines(out, 3)) + } + }) + } +} + +func firstLines(s string, n int) string { + lines := strings.SplitN(s, "\n", n+1) + if len(lines) > n { + lines = lines[:n] + } + return strings.Join(lines, "\n") +} diff --git a/cmd/mxcli/help.go b/cmd/mxcli/help.go index cd297b77a7..5477a18889 100644 --- a/cmd/mxcli/help.go +++ b/cmd/mxcli/help.go @@ -4,8 +4,6 @@ package main import ( "fmt" - "os" - "strings" "github.com/mendixlabs/mxcli/cmd/mxcli/syntax" "github.com/spf13/cobra" @@ -18,6 +16,8 @@ var syntaxCmd = &cobra.Command{ Use --json for machine-readable output (optimized for LLM consumption). Drill down with multiple arguments: mxcli syntax workflow user-task targeting +The topic may be given as separate words, as one quoted string, or dotted — +all three reach the same page, as does the plain-word spelling ("user task"). Top-level topics: domain-model - Entities, associations, enumerations, constants, keywords, types @@ -40,53 +40,40 @@ Examples: mxcli syntax workflow --json # All workflow features mxcli syntax workflow user-task targeting # Drill down to targeting mxcli syntax security entity-access # Entity access rules + mxcli syntax workflow user task # Plain words resolve too mxcli syntax entity # Legacy alias → domain-model.entity `, Run: func(cmd *cobra.Command, args []string) { jsonFlag, _ := cmd.Flags().GetBool("json") + out := cmd.OutOrStdout() // No args: show full index (JSON) or help text if len(args) == 0 { if jsonFlag { - syntax.WriteJSON(os.Stdout, syntax.All()) + syntax.WriteJSON(out, syntax.All()) return } cmd.Help() return } - // Build registry path from args - path := strings.ToLower(strings.Join(args, ".")) - - // Apply aliases - path = syntax.ResolveAlias(path) - - // Query registry - if syntax.HasPrefix(path) { - features := syntax.ByPrefix(path) + // One resolver for both surfaces — see syntax.Lookup. The topic may + // arrive as separate words, as one quoted string, or dotted. + m := syntax.Lookup(args) + if len(m.Features) > 0 { if jsonFlag { - syntax.WriteJSON(os.Stdout, features) - } else { - syntax.WriteText(os.Stdout, features) + syntax.WriteJSON(out, m.Features) + return } - return - } - - // Not a path from the left. Before giving up, match the query against - // any SEGMENT of a path: `rule` names a real topic (two, in fact), it - // just is not the first segment of either. Without this the answer to a - // topic that exists is "Unknown topic" (#955). - if features := syntax.BySegmentMatch(path); len(features) > 0 { - if jsonFlag { - syntax.WriteJSON(os.Stdout, features) - } else { - fmt.Printf("No top-level topic %q. Showing %d topic(s) matching it:\n\n", path, len(features)) - syntax.WriteText(os.Stdout, features) + if !m.Exact { + fmt.Fprintf(out, "No topic %q. Showing %d topic(s) matching %q:\n\n", + m.Path, len(m.Features), m.Fallback) } + syntax.WriteText(out, m.Features) return } - fmt.Printf("Unknown topic: %s\n\n", path) + fmt.Fprintf(out, "Unknown topic: %s\n\n", m.Path) cmd.Help() }, } diff --git a/cmd/mxcli/lsp_completions_gen.go b/cmd/mxcli/lsp_completions_gen.go index f6ef41c76a..fcf9f4f368 100644 --- a/cmd/mxcli/lsp_completions_gen.go +++ b/cmd/mxcli/lsp_completions_gen.go @@ -13,6 +13,7 @@ var mdlGeneratedKeywords = []protocol.CompletionItem{ {Label: "GROUP BY", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, {Label: "ORDER BY", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, {Label: "SORT BY", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, + {Label: "SEARCH BY", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, {Label: "NON-PERSISTENT", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, {Label: "REFERENCE_SET", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, {Label: "LIST OF", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, diff --git a/cmd/mxcli/syntax/features_microflow.go b/cmd/mxcli/syntax/features_microflow.go index 40680730a4..dc375d7ee9 100644 --- a/cmd/mxcli/syntax/features_microflow.go +++ b/cmd/mxcli/syntax/features_microflow.go @@ -160,8 +160,15 @@ func init() { "-- path (CE0108). End the handler, or expect that.\n" + "--\n" + "-- An EMPTY handler `{ }` is not a no-op: it means \"on error, do whatever\n" + - "-- the enclosing branch does next\". Say where the path goes with JOIN.", - Example: "COMMIT $Order ON ERROR {\n LOG ERROR 'Failed to save order';\n RETURN empty;\n};\n\n" + + "-- the enclosing branch does next\". Say where the path goes with JOIN.\n" + + "--\n" + + "-- RAISE ERROR re-raises the error being handled, so it belongs INSIDE an\n" + + "-- ON ERROR handler and nowhere else. On the main flow it is MDL084:\n" + + "-- Mendix needs an error in scope to re-raise, Studio Pro will not draw\n" + + "-- the shape, and mxbuild rejects it with CE0710 \"The main flow cannot\n" + + "-- join an error flow or end in an error event.\". To fail deliberately\n" + + "-- from the main flow, call a Java action that throws.", + Example: "COMMIT $Order ON ERROR {\n LOG ERROR 'Failed to save order';\n RAISE ERROR;\n};\n\n" + "COMMIT $Batch ON ERROR WITHOUT ROLLBACK {\n LOG WARNING 'Batch save failed, continuing';\n};\n\n" + "DECLARE $Name String = 'default' ON ERROR {\n RETURN 'could not initialise';\n};", SeeAlso: []string{"microflow.control-flow"}, diff --git a/cmd/mxcli/syntax/features_page.go b/cmd/mxcli/syntax/features_page.go index 1f855c38d4..5a9b1bb860 100644 --- a/cmd/mxcli/syntax/features_page.go +++ b/cmd/mxcli/syntax/features_page.go @@ -155,6 +155,10 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "-- `check --references` rather than failing the build with CE1613.\n" + "-- The alternatives are the URL form above, or `ImageType: icon`.\n\n" + "-- Any pluggable widget by its id (id FIRST, then the name)\nPLUGGABLEWIDGET 'com.mendix.widget.web.badge.Badge' name (value: 'x')\nCUSTOMWIDGET 'com.mendix.widget.custom.x.X' name (prop: 'x') -- legacy spelling\n\n" + + "-- STATICIMAGE takes the same three-part image-collection reference as IMAGE,\n" + + "-- so a stored one round-trips through DESCRIBE (mendixlabs/mxcli#1057). Without\n" + + "-- it the widget is written with no image and mxbuild reports CE0436:\n" + + "STATICIMAGE imgLogo (Image: 'MyModule.Images.logo', Width: 64, Height: 64)\n\n" + "-- Deprecated in the Mendix 11 React client. These are written correctly by\n" + "-- both engines, but mxbuild reports CE0582 (\"not supported in React client\")\n" + "-- on each, so prefer the alternative:\n" + @@ -188,7 +192,9 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "TEMPLATE FOR Module.Entity and not TEMPLATE name. (A Gallery's TEMPLATE name is a\n" + "different thing: a named content slot.)\n\n" + "Rules:\n" + - " - the entity must be the list view's entity or a specialization of it\n" + + " - the entity must be a SPECIALIZATION of the list view's entity; the list\n" + + " view's own entity is CE0543, since its body already renders objects\n" + + " no template matches\n" + " - at most one template per entity\n" + " - templates keep their source order, which is the order Mendix stores and matches in\n" + " - inside a template the context object is the specialization, so its own attributes resolve\n\n" + @@ -222,7 +228,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "datasource", "data source", "database", "microflow", "selection", "variable", "binding", "binds", "association", "data from context", }, - Syntax: "DataSource: $Variable -- Parameter/variable binding\nDataSource: DATABASE Module.Entity -- Database query\nDataSource: MICROFLOW Module.MF -- Microflow datasource, no parameters\nDataSource: MICROFLOW Module.MF($P) -- ...one argument per PARAMETER, required:\n -- Mendix does NOT auto-map an object in\n -- scope, not even one of the exact type,\n -- so a missing argument is CE1571\nDataSource: SELECTION widgetName -- Selection from another widget\nDataSource: $currentObject/Module.Assoc -- Over an association (\"data from context\")\n -- list widget → to-many collection\n -- nested DATAVIEW → the to-one referenced object\nAttribute: AttributeName -- Attribute binding (inputs)", + Syntax: "DataSource: $Variable -- Parameter/variable binding\nDataSource: DATABASE Module.Entity -- Database query\nDataSource: DATABASE Module.Entity WHERE [Attr != ''] SORT BY Attr ASC\n -- ...optionally constrained and sorted\nDataSource: DATABASE Module.Entity SEARCH BY Attr, Attr2\n -- LIST VIEW only: the attributes its\n -- search bar filters on. Mirrors SORT BY,\n -- but takes no direction.\nDataSource: MICROFLOW Module.MF -- Microflow datasource, no parameters\nDataSource: MICROFLOW Module.MF($P) -- ...one argument per PARAMETER, required:\n -- Mendix does NOT auto-map an object in\n -- scope, not even one of the exact type,\n -- so a missing argument is CE1571\nDataSource: SELECTION widgetName -- Selection from another widget\nDataSource: $currentObject/Module.Assoc -- Over an association (\"data from context\")\n -- list widget → to-many collection\n -- nested DATAVIEW → the to-one referenced object\nAttribute: AttributeName -- Attribute binding (inputs)", Example: "-- Database datasource with grid\nDATAGRID grid (DataSource: DATABASE Module.Customer) {\n COLUMN colName (Attribute: Name, Caption: 'Name')\n}\n\n-- Microflow datasource\nDATAVIEW dv (DataSource: MICROFLOW Module.GetData) {\n TEXTBOX txtName (Label: 'Name', Attribute: Name)\n}\n\n-- Over an association: a nested DataView shows the referenced (to-one) object\nDATAVIEW dvOrder (DataSource: $Order) {\n DATAVIEW dvCustomer (DataSource: $currentObject/Order_Customer) {\n TEXTBOX txtCustName (Label: 'Name', Attribute: Name)\n }\n}\n\n-- Over an association: a list widget shows the (to-many) collection\nLISTVIEW lvLines (DataSource: $currentObject/Order_OrderLine) {\n DYNAMICTEXT dtLine (Content: 'Line')\n}", SeeAlso: []string{"page.widgets", "page.create"}, }) diff --git a/cmd/mxcli/syntax/topic.go b/cmd/mxcli/syntax/topic.go new file mode 100644 index 0000000000..ee3aabccef --- /dev/null +++ b/cmd/mxcli/syntax/topic.go @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 + +package syntax + +import "strings" + +// Match is the outcome of looking a topic up in the registry. +type Match struct { + // Path is the registry path the words resolved to. + Path string + // Features are the topics to show. Empty when nothing matched. + Features []SyntaxFeature + // Exact reports whether Path named a topic (or the prefix of one) + // directly. When it is false and Features is non-empty, the features came + // from a segment match on Fallback rather than from Path. + Exact bool + // Fallback is the word the segment match ran on, set only when Exact is + // false and Features is non-empty. + Fallback string +} + +// Lookup resolves the words a caller typed to a set of topics. +// +// It is the ONE answer to "which topic is this?", shared by `mxcli syntax` and +// the REPL's `help`. They used to resolve separately and disagree: +// `help workflow user task` found the page, `mxcli syntax "workflow user-task"` +// did not, because the CLI joined its arguments on "." and never split them. +// A topic handed over as a single string — a quoted copy-paste from the +// command's own help, a tool wrapper, `sh -c` — came out as the path +// "workflow user-task", which matches nothing, and the answer to a topic that +// exists was "Unknown topic: workflow user-task" +// (mendixlabs/mxcli#1025, and #955 before it). +func Lookup(args []string) Match { + words := topicWords(args) + if len(words) == 0 { + return Match{} + } + + path := ResolveAlias(resolvePath(words)) + if HasPrefix(path) { + return Match{Path: path, Features: ByPrefix(path), Exact: true} + } + + // Not a path from the left. Before giving up, match against any SEGMENT of + // a path: `rule` names a real topic (two, in fact), it just is not the + // first segment of either (#955). + // + // The segment match runs on the LAST word, not on the dotted path: no + // segment contains a ".", so passing the whole path could only ever match + // when the query was a single word — which made this fallback silently + // dead for every multi-word query. + last := words[len(words)-1] + if features := BySegmentMatch(last); len(features) > 0 { + return Match{Path: path, Features: features, Fallback: last} + } + return Match{Path: path} +} + +// topicWords flattens the arguments into lower-case words, splitting on +// whitespace and on ".". That is what makes these one query: +// +// mxcli syntax workflow user-task targeting +// mxcli syntax "workflow user-task targeting" +// mxcli syntax workflow.user-task.targeting +// help workflow user task (in the REPL) +func topicWords(args []string) []string { + var words []string + for _, arg := range args { + for _, field := range strings.Fields(strings.ToLower(arg)) { + for _, seg := range strings.Split(field, ".") { + if seg != "" { + words = append(words, seg) + } + } + } + } + return words +} + +// resolvePath converts words like ["workflow", "user", "task"] into a registry +// path like "workflow.user-task", greedily merging adjacent words with hyphens +// to find the longest matching prefix at each level. +func resolvePath(words []string) string { + var segments []string + i := 0 + for i < len(words) { + matched := false + for j := len(words); j > i; j-- { + candidate := strings.Join(words[i:j], "-") + testPath := strings.Join(append(segments, candidate), ".") + if HasPrefix(testPath) { + segments = append(segments, candidate) + i = j + matched = true + break + } + } + if !matched { + segments = append(segments, words[i]) + i++ + } + } + return strings.Join(segments, ".") +} diff --git a/cmd/mxcli/syntax/topic_test.go b/cmd/mxcli/syntax/topic_test.go new file mode 100644 index 0000000000..0c3202fd5f --- /dev/null +++ b/cmd/mxcli/syntax/topic_test.go @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 + +package syntax + +import ( + "strings" + "testing" +) + +func TestLookupSpellings(t *testing.T) { + tests := []struct { + name string + args []string + want string + exact bool + }{ + // The three spellings of the example `mxcli syntax` advertises. + {"separate arguments", []string{"workflow", "user-task", "targeting"}, "workflow.user-task.targeting", true}, + {"one quoted argument", []string{"workflow user-task targeting"}, "workflow.user-task.targeting", true}, + {"dotted path", []string{"workflow.user-task.targeting"}, "workflow.user-task.targeting", true}, + // Words instead of hyphens — what the REPL's `help` has always taken. + {"words", []string{"workflow", "user", "task"}, "workflow.user-task", true}, + {"one quoted argument, words", []string{"workflow user task targeting"}, "workflow.user-task.targeting", true}, + {"security prefix", []string{"security entity access"}, "security.entity-access", true}, + {"domain model", []string{"domain model"}, "domain-model", true}, + // Aliases still resolve, and still only as whole paths. + {"legacy alias", []string{"entity"}, "domain-model.entity", true}, + // A leaf name that is nobody's first segment (#955). + {"segment match", []string{"parallel-split"}, "parallel-split", false}, + {"unknown", []string{"nonexistent-topic"}, "nonexistent-topic", false}, + {"no words", nil, "", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := Lookup(tt.args) + if m.Path != tt.want { + t.Errorf("Lookup(%q).Path = %q, want %q", tt.args, m.Path, tt.want) + } + if m.Exact != tt.exact { + t.Errorf("Lookup(%q).Exact = %v, want %v", tt.args, m.Exact, tt.exact) + } + if tt.exact && len(m.Features) == 0 { + t.Errorf("Lookup(%q) resolved to %q but returned no features", tt.args, m.Path) + } + }) + } +} + +// Whatever a topic's listed path is, all three ways of typing it must reach it: +// as separate arguments, as one string, and dotted. `mxcli syntax` prints the +// dotted paths and then tells the reader to drill down with the words, so a +// spelling that does not resolve is the command contradicting its own output +// (mendixlabs/mxcli#1025). +func TestEveryRegisteredPathIsReachableBySpelling(t *testing.T) { + for _, f := range All() { + segments := strings.Split(f.Path, ".") + spellings := map[string][]string{ + "dotted": {f.Path}, + "separate arguments": segments, + "one argument": {strings.Join(segments, " ")}, + } + for name, args := range spellings { + m := Lookup(args) + if !m.Exact || m.Path != f.Path { + t.Errorf("%s (%s): Lookup(%q) = %q exact=%v, want %q exact=true", + f.Path, name, args, m.Path, m.Exact, f.Path) + } + } + } +} + +// Aliases are whole-path spellings, and every one must land on a real topic. +func TestAliasesResolveToRegisteredTopics(t *testing.T) { + for alias := range topicAliases { + if m := Lookup([]string{alias}); !m.Exact { + t.Errorf("alias %q resolves to %q, which names no topic", alias, m.Path) + } + } +} diff --git a/docs-site/src/language/control-flow.md b/docs-site/src/language/control-flow.md index 48bc7f6e9d..58e95dfb63 100644 --- a/docs-site/src/language/control-flow.md +++ b/docs-site/src/language/control-flow.md @@ -211,6 +211,33 @@ COMMIT $Order ON ERROR ROLLBACK; > **Note:** `ON ERROR` is not supported on `EXECUTE DATABASE QUERY` activities. +### RAISE ERROR (handler-only) + +`RAISE ERROR` builds Mendix's **error event**, which re-raises the error currently +being handled. Mendix allows one only where an error is in scope, so it belongs +inside an `ON ERROR { ... }` block: + +```sql +CALL MICROFLOW Integration.CallExternalAPI (Payload = $Body) ON ERROR { + LOG ERROR NODE 'Integration' 'API call failed, re-raising'; + RAISE ERROR; +}; +``` + +On the **main flow** it is refused as **MDL084**, at any nesting depth — inside an +`IF`, inside a `LOOP`, or as the whole body. Studio Pro will not draw that shape, +and mxbuild rejects it with **CE0710** *"The main flow cannot join an error flow or +end in an error event."* The same applies inside a rule, which the same flow builder +produces. + +Mendix has no main-flow "throw" activity. To fail deliberately from the normal path, +call a Java action that throws: + +```sql +CREATE JAVA ACTION Module.JA_RaiseTechnicalError (Message: String NOT NULL) RETURNS Boolean AS +$$ throw new com.mendix.systemwideinterfaces.MendixRuntimeException(Message); $$; +``` + ## CASE (Enum Split) `CASE` branches on an **enumeration** and compiles to a Mendix enum split. It is not a diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md index 25eb48a807..bd2f6ef8c0 100644 --- a/docs/01-project/MDL_QUICK_REFERENCE.md +++ b/docs/01-project/MDL_QUICK_REFERENCE.md @@ -569,6 +569,7 @@ it is for pages. | Import mapping | `[$Var =] import from mapping Module.IMM($SourceVar) [all\|first\|limit [offset ]];` | Apply import mapping to string variable. Trailing clause is Studio Pro's Range; omitted = infer from the mapping's root. `first` binds one OBJECT (`limit 1` is a one-element LIST). Mendix rejects `offset` on a non-list mapping (CE6100) | | Export mapping | `$Var = export to mapping Module.EMM($EntityVar);` | Apply export mapping to entity, returns string | | Error handling | `... on error continue\|rollback\|{ handler }\|without rollback { handler };` | Goes on the activity that may fail — including `declare`, `set`, `change`, `log`, `show page`, `close page`, `show message` and `validation feedback`, which gained it in mendixlabs/mxcli#1078 so a Studio Pro handler survives DESCRIBE. `on error continue` is refused (MDL076) where Mendix raises CE6035: create, change, commit, log, show page, close page, show message, validation feedback — a custom `{ handler }` is accepted on all of them. The list-operation and aggregate forms of `set` have no error handling at all (MDL077). Not supported on EXECUTE DATABASE QUERY. **In a nanoflow** only `declare` and `set` take a clause at all — `change`, `log`, `show page`, `close page`, `show message` and `validation feedback` are CE6035 there in every form, and are refused. A handler that does not end in `return`/`throw` merges back into the main flow, so a later variable is out of scope on the error path (CE0108) | +| Re-raise the error | `raise error;` | **Inside an `on error { … }` handler only.** The error event re-raises the error being handled, so Mendix needs one in scope; Studio Pro will not draw the shape and mxbuild rejects it with **CE0710** "The main flow cannot join an error flow or end in an error event". On the main flow — at any nesting depth, and in a rule too — it is **MDL084**. Mendix has no main-flow "throw": call a Java action that throws | | Named join point | `merge