diff --git a/.claude/hooks/post-push.sh b/.claude/hooks/post-push.sh new file mode 100755 index 000000000..e1d9ce321 --- /dev/null +++ b/.claude/hooks/post-push.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# +# PostToolUse hook (Bash) — after a `git push`, report where the branch stands +# relative to main. +# +# Why this exists: twice in one session commits were pushed onto a branch whose +# pull request had already been merged, and reported as "added to PR #N" when +# PR #N was closed and contained none of them. A merged PR cannot take new +# commits, so the work needed a fresh branch and a new PR. +# +# It would be better to name the PR directly, but this environment's egress +# proxy intercepts api.github.com and answers 403 ("GitHub access is not enabled +# for this session"), so a shell hook cannot ask. What it CAN compute locally is +# the state both failures shared: the branch was behind origin/main because main +# had absorbed the branch's earlier commits via the merge. +# +# Prints nothing when the branch is simply ahead of main — the normal case — +# so a clean push stays quiet. +set -uo pipefail + +payload=$(cat) + +# Only react to a push. Matched anywhere in the command rather than via the +# hook's `if` filter, which is a PREFIX match and would miss the common +# `git add … && git commit … && git push …`. +command=$(printf '%s' "$payload" | jq -r '.tool_input.command // ""' 2>/dev/null || true) +case "$command" in + *"git push"*) ;; + *) exit 0 ;; +esac + +cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || exit 0 +git rev-parse --git-dir >/dev/null 2>&1 || exit 0 + +branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true) +if [ -z "$branch" ] || [ "$branch" = "HEAD" ]; then exit 0; fi + +# Best effort: a slow or offline fetch must not hold up the session. +timeout 15 git fetch -q origin main >/dev/null 2>&1 || true +git rev-parse --verify -q origin/main >/dev/null 2>&1 || exit 0 + +behind=$(git rev-list --count "HEAD..origin/main" 2>/dev/null || echo 0) +if [ "$behind" -eq 0 ]; then exit 0; fi + +ahead=$(git rev-list --count "origin/main..HEAD" 2>/dev/null || echo 0) + +if [ "$ahead" -eq 0 ]; then + # Nothing of its own: main has everything this branch has. Either its PR was + # merged, or the checkout is stale (an ephemeral container is re-cloned fresh, + # so locally-made commits can be absent while the pushed branch still has them). + msg="$branch is $behind commit(s) behind origin/main and has none of its own. +Its pull request may already be merged, or this checkout may be stale — a fresh container +re-clones the repo, so commits made earlier in the session can be missing locally. +Check both before describing the branch's state: + git log --oneline origin/$branch (what was actually pushed) + restart from main if the PR merged: git checkout -B $branch origin/main" +else + msg="pushed $branch — it is $ahead commit(s) ahead of origin/main but also $behind BEHIND. +If a pull request from this branch was already merged, it CANNOT carry those $ahead commit(s): +verify the PR is still open before saying they were added to it. +If it merged, restart the branch and keep the unmerged work: + git fetch origin main && git rebase --onto origin/main $branch" +fi + +# systemMessage surfaces it to the user; additionalContext puts the same fact in +# the model's context, which is where the wrong claim was made. +jq -nc --arg m "$msg" \ + '{systemMessage: $m, hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $m}}' diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 4d60baf86..da0b8654e 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -20,13 +20,48 @@ set -euo pipefail ANTLR_VERSION='4.13.2' ANTLR_TOOLS_VERSION='0.2.2' +cd "${CLAUDE_PROJECT_DIR:-$(dirname "$0")/../..}" 2>/dev/null || exit 0 + +# 0. Is this checkout what the session thinks it is? +# +# The container is ephemeral: it is re-cloned when reprovisioned, so commits made +# earlier in a session can be absent from the working copy while still present on +# the remote. That happened twice in one session, and the second time it was +# misread as a code bug — a grammar rule "missing" from the tree had in fact been +# committed and pushed hours earlier, and the binary under test was built from +# the rolled-back tree. +# +# Only speaks when the branch is actually behind its own remote, so a healthy +# start stays silent. Runs before the remote-only exit below because a stale +# checkout is worth knowing about on any machine. +_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true) +if [ -n "${_branch:-}" ] && [ "${_branch}" != "HEAD" ]; then + timeout 15 git fetch -q origin "${_branch}" >/dev/null 2>&1 || true + if git rev-parse --verify -q "origin/${_branch}" >/dev/null 2>&1; then + _behind=$(git rev-list --count "HEAD..origin/${_branch}" 2>/dev/null || echo 0) + if [ "${_behind}" -gt 0 ]; then + echo "WARNING: HEAD is ${_behind} commit(s) behind origin/${_branch}." + echo " This checkout may be a fresh clone that is missing work pushed earlier." + echo " Reconcile before building or testing: git log --oneline origin/${_branch}" + fi + fi +fi + +# The binary carries the commit it was built from (Makefile -X main.Version), so +# a mismatch means any behaviour observed through bin/mxcli is from other code. +if [ -x bin/mxcli ]; then + _built=$(bin/mxcli --version 2>/dev/null | grep -oE '[0-9a-f]{7,}' | head -1 || true) + _head=$(git rev-parse --short HEAD 2>/dev/null || true) + if [ -n "${_built:-}" ] && [ -n "${_head:-}" ] && [ "${_built}" != "${_head}" ]; then + echo "NOTE: bin/mxcli was built from ${_built}, HEAD is ${_head} — run 'make build' before testing." + fi +fi + # Local (devcontainer / laptop) setups already have these via the Dockerfile. if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then exit 0 fi -cd "${CLAUDE_PROJECT_DIR:-$(dirname "$0")/../..}" - # 1. ANTLR4 — required by `make grammar`, which `make build` always runs. if ! command -v antlr4 >/dev/null 2>&1; then echo "Installing antlr4-tools==${ANTLR_TOOLS_VERSION}..." diff --git a/.claude/settings.json b/.claude/settings.json index e06b0338e..051fe5d2d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -9,6 +9,18 @@ } ] } + ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/post-push.sh", + "timeout": 25 + } + ] + } ] } } diff --git a/.claude/skills/fix-issue.md b/.claude/skills/fix-issue.md index 57f14c3a4..8a7db17d9 100644 --- a/.claude/skills/fix-issue.md +++ b/.claude/skills/fix-issue.md @@ -562,3 +562,17 @@ extracting `OffsetExpression`/`LimitExpression`. | `calculated by Module.Microflow` on an attribute is accepted by `check` and by exec ("Added attribute"), but the stored document holds a plain `DomainModels$StoredValue` with no calculation link — the microflow name appears nowhere in the domain model unit, `mx check` reports **0 errors**, and the attribute is simply empty at runtime. Both the CREATE (inline) and ALTER (`ADD`/`MODIFY ATTRIBUTE`) paths. A microflow whose signature cannot work is accepted too, masked by the same drop | `attributeToGen` in the **modelsdk** writer had arms for OqlViewValue / ODataMappedValue / ODataMappedPrimitiveCollectionValue and a `default:` that emits StoredValue — no `CalculatedValue` arm — so the binding the executor had already resolved fell through and was discarded. The **legacy** writer had the arm all along (`sdk/mpr/writer_domainmodel.go`), which is why the feature read as implemented; modelsdk is the default engine (`--engine`), so everyone hit the broken path. The reader had no `CalculatedValue` case either, so an unrelated ALTER on the same entity destroyed a binding made in Studio Pro | `mdl/backend/modelsdk/domainmodel_write.go` (`attributeToGen`) + `domainmodel.go` (`attributeFromGen`) + `mdl/executor/calculated_attributes.go` (`resolveCalculatedValue`, called from the three sites in `cmd_entities.go`) | Add the write arm (`genDm.NewCalculatedValue`, `SetMicroflowQualifiedName` → the `Microflow` ByNameRef key, `SetPassEntity`) **and** the read arm — a write-only fix leaves the read-modify-write data loss in place, which is the worse half. Derive `PassEntity` from the signature rather than hardcoding it (legacy hardcoded `microflowRef != ""`): measured on 11.13.0, an entity-parameter microflow (`PassEntity=true`) and a parameterless one (`PassEntity=false`) BOTH build at 0 errors, so refusing the parameterless form would have been wrong. Signature rules are refused at exec (the #833 placement), and each was checked against mxbuild rather than assumed — wrong entity parameter and wrong return type are both **CE7247**, but the return-type message is *"should be Integer/Long"*, so **Integer and Long are one family** and a strict equality check refuses valid MDL (caught only by reading the CE text). To ask mxbuild about a binding mxcli now refuses, stub the check and rebuild — `--engine legacy` does NOT bypass it, because the validation lives in the engine-independent executor. Tests `TestAttributeToGen_CalculatedValue`, `TestAttributeFromGen_CalculatedValue`, `TestResolveCalculatedValue_*` (the backend three fail with `value is *domainmodels.StoredValue` when reverted); fixture `mdl-examples/bug-tests/917-calculated-attribute-binding.mdl`. Issue #917 | | `DESCRIBE IMPORT MAPPING` output does not reproduce the script that made it: `Total = total` comes back as `Total = Total`, an array binding as `= ItemItem`, `LineId = id` as `LineId = _id` — and the output cannot be re-run at all, failing with "import mapping already exists". Export mappings identically (unreported) | DESCRIBE printed the element's **ExposedName** (Mendix's display name — capitalised initial, `Item` suffix on an array's item object) instead of the raw JSON key from `JsonPath`, and emitted a bare `create` header where every other DESCRIBE emits `create or modify` | `mdl/executor/cmd_import_mappings.go` (`mappingMemberName`, the four print sites, the header) + `cmd_export_mappings.go` (same four + header) | Print the raw key derived from `JsonPath` — strip a trailing `\|(Object)` first, because an array's mapping element sits at the ITEM object while the script addressed the array (that suffix is what produced `ItemItem`). Fall back to ExposedName when there is no JsonPath (XML-schema / message-definition mappings have none). Safe by construction: the raw path is `jsonSchemaIndex.resolve`'s FIRST lookup, so it cannot regress #882. **Do NOT "fix" ExposedName itself** — the capitalisation is Mendix's own, confirmed against a Studio Pro-authored document in the blank app (`ExposedName "Uuid"` vs `Path "(Object)|uuid"`); rewriting it would diverge from Studio Pro. The `Item` suffix could NOT be confirmed the same way (a blank app has no Studio Pro array structure) and was left alone — with a separate `ExposedItemName` property in the BSON, that is worth checking against a marketplace module before anyone touches storage. Note the issue's framing was half wrong: the mapping DID round-trip semantically (re-executing the old output rebuilt byte-identical JsonPaths), so this was a text/diff defect, not a broken mapping — measure before agreeing with a title. Tests `TestMappingMemberName`, `TestDescribe{Import,Export}Mapping_RoundTripsMemberNames` (fail with the reported symptoms when reverted); two existing header assertions needed updating with the intentional change; fixture `mdl-examples/bug-tests/915-mapping-describe-roundtrip.mdl` is a DESCRIBE fixed point. Issue #915 | | `mxcli check -p app.mpr` passes a microflow containing `if $obj/Status = 'Open'` — comparing an enumeration attribute to a string literal, the first example in the type-checking proposal and the shape people actually write. The same mistake written as a create or change member *is* caught, which makes the gap look arbitrary | Two causes, both invisible. (a) **`inferKind` returned `KindUnknown` for every `AttributePathExpr`**, so `$obj/Attr` typed to nothing and every rule downstream of it stayed quiet. `exprcheck.Scope` is `Lookup(name) (TypeKind, bool)` — it can say "$P is an Object" but not *which* entity, so there was nowhere to put the answer; the adapter computed a variable→entity map (`buildVarEntityScope`) and used it only to label a slot path, never passing it to the checker, and it covered body-introduced variables but **not parameters** — the ordinary case. (b) Even resolved, nothing fired: **E001 keys off the SLOT** (`CreateItem.Value:Entity.Attr`), which exists for an assignment and not for a comparison | `mdl/exprcheck/interfaces.go` (`EntityScope`, `Context.Entities`), `mdl/exprcheck/parser.go` (`attributePathKind`, `pathTargetEntity`, `checkEnumComparedToString`), `mdl/exprcheck/adapters/` (`walkFlow`, `addParamEntities`, `entityScope`), `mdl/exprcatalog/` (`AssociationTarget`) | Put the object side in its **own seam** (`EntityScope`: `VariableEntity` + `AssociationTarget`) beside `Scope` rather than widening either it or `CatalogReader` — `CatalogReader`'s shape is what stays re-syncable from the upstream fork. **A multi-hop expression path is not an XPath path**: XPath spells the intermediate entity (`[Assoc/Entity/Attr]`) so a walk can read it off, while an expression does not (`$O/Mod.Assoc/Attr`), so every hop must resolve through the association index. For (b), emit the **same code and message** from the comparison as from the slot — one defect should not have two names depending on where it was spotted. **Generalisable**: when a checker is silent, separate "did the input resolve" from "is there a rule for this shape" before touching either — here resolution and detection were both missing, and fixing only one would have looked like the fix failed. Verify with a **probe corpus that actually exercises the construct**: 21 microflows described back to MDL contained 40 attribute-path lines including an association hop, and were clean before and after, which is what makes "no false positives" mean something. Controls: path→`KindUnknown` restored, `Entities` dropped from the Context, and `addParamEntities` removed each fail a distinct test. **Still open**: a *terminal* association step (`$Order/Mod.Order_Lines`) types to unknown — Object vs List depends on the association's kind and direction, and guessing costs false positives | +| `describe microflow` renders a REST call's result as `returns String` when the activity actually stores the response in a **file document**, and the describe → exec round trip then writes String back — retyping the activity with `mx check` reporting nothing either way. On the legacy engine the `$var =` output variable disappears too | MDL had no syntax for the case, `sdk/microflows` had no FileDocument variant, and both readers fell through to String: the legacy `parseResultHandling` had no `FileDocument` case (so the whole handling read back nil, taking the output-variable fallback with it), while modelsdk's `restResultHandlingFromRaw` read `VariableType.Entity` and then discarded it, matching only the literal `System.HttpResponse`. Mendix stores all five variants as ONE `Microflows$ResultHandling` carrying a `VariableType`, discriminated by the `ResultHandlingType` enum | `mdl/grammar/domains/MDLMicroflow.g4` (`restCallReturnsClause`), `mdl/ast/ast_microflow.go`, `mdl/visitor/visitor_microflow_actions.go`, `sdk/microflows/microflows_actions.go`, `sdk/mpr/parser_microflow_actions.go` + `writer_microflow_actions.go`, `mdl/backend/modelsdk/microflow_read_actions.go` + `microflow_write.go`, `mdl/executor/cmd_microflows_builder_calls.go` + `cmd_microflows_format_action.go`, rule in `validate_microflow_rest.go` (MDL064) | Add `returns Module.Entity` as the LAST alternative so the keyword forms still win, and read Mendix's own `ResultHandlingType` discriminator rather than inferring from `VariableType` (keep the inference as a fallback — the property is `omitempty`). **Delete the silent `String` fallbacks**: an unreconstructable result handling must describe as something the parser REJECTS, or a reader gap becomes a model rewrite (ADR-0005). Mendix constraints worth knowing before designing syntax here: the BASE `System.FileDocument` is rejected as a return type (**CE0362**) so a specialization is mandatory, and HttpResponse **cannot** be specialized (**CE1540** allows only User, FileDocument, Image, Paging) — which is why `returns response` needed no change and the reporter's suggested "HttpResponse specialization" does not exist. Cross-engine round trip (`gateEngines`) is what proves it: the two readers failed differently and each looked fine from inside itself. Issue #922 | +| `describe import mapping` prints only the LAST segment of a value element's JsonPath, so a mapping that binds a nested leaf onto a parent entity describes as `Attr = name` when the model says `(Object)\|customer\|name` — and re-executing mxcli's own output fails with `"name" is not a member of the JSON structure at (Object)`. Authoring that shape was impossible too (`extraneous input '/'`) | Studio Pro can bind a leaf several levels below the object element it belongs to, with no entity for the levels in between, stored as ONE multi-segment JsonPath. `mappingMemberName` took `LastIndex(path, "\|")`, assuming a value element is always a direct child, and the grammar's value alternative took a single `identifierOrKeyword` | grammar `mdl/grammar/domains/MDLDomainModel.g4` (`jsonMemberPath`), `mdl/visitor/visitor_import_export_mapping.go`, `mdl/executor/cmd_import_mappings.go` (`resolvePath`, `mappingMemberName`) + `cmd_export_mappings.go`; fixtures `mdl-examples/bug-tests/927-mapping-nested-member-path*.mdl` | Render the member RELATIVE to the enclosing object element (thread the parent's JsonPath through the printers) and resolve a `/`-separated member one segment at a time so each step keeps the raw-key/exposed-name tolerance from #882. **Use the parent path verbatim** — for an array the object element's own JsonPath is already the ITEM path, and trimming `\|(Object)` off it makes a child of that item render as `(Object)/sku`. Two shapes must be REFUSED, both measured rather than assumed: an EXPORT mapping cannot collapse levels (**CE5015** — it has to produce the intermediate node; three-way control: same member in an import is 0 errors, same export with only top-level members is 0 errors), and an import member cannot cross a `0..*` element (**CE0256** "a schema element with wrong occurrence"). Measure by patching the JsonPath into a stored mapping and running `mx check` — and assert the patch landed before trusting a 0, since an mxcli parse error leaves the baseline project untouched. Issue #927 | +| The nightly fails on ONE Mendix matrix version only, in `TestMxCheck_DoctypeScripts`, with `Execution error: this project does not store the model setting ` — while the same script passes on every newer version | The example script set a model setting that version does not have. Measured: a blank 10.24 stores 11 model settings and a blank 11.6.6 stores 12, `DecimalScale` being the only difference. mxcli's refusal is CORRECT — Studio Pro will not open a model carrying a property its version does not define, and mxbuild does not catch it — so the bug is the ungated example, not the guard. The refusal covers the WHOLE statement, so one version-specific setting takes every portable setting in the same `alter` down with it | `mdl-examples/doctype-tests/14-project-settings-examples.mdl`; guard in `mdl/executor/doctype_version_gating_test.go` | Split the version-specific setting into its own statement inside a `-- @version: N.N+` section, closed with `-- @version: any`. **Put the `/** */` doc comment INSIDE the gated section**: a block comment is a documentation comment bound to the statement after it, so gating the statement while leaving the comment outside orphans it and the script dies with `no viable alternative at input '/**...'` — reported at the NEXT statement, tens of lines away, which reads like an unrelated syntax error. `--` line comments are free-standing and safe either side. Isolate which setting is at fault by exec'ing them one at a time against a blank project of that version (`mx create-project` in a SHORT path — a long one dies with PathTooLongException). `TestDoctypeScriptsParseAfterVersionFiltering` now parses every doctype script under each nightly matrix version without needing mxbuild, so a mis-gated script fails in seconds on push instead of hours later in one nightly job | +| A widget property is accepted by `mxcli check`, written by `exec`, and does nothing: a pluggable widget's `imageUrl: '{1}', contentparams: [...]` builds as **CE0720** ("place holder index greater than parameter count"), and `editable:` on a button silently leaves it enabled with no warning at all | Both allow-lists are widget-type AGNOSTIC. `isBuiltinPropName` (`widget_engine.go`) is ONE flat list holding both `ContentParams` and `Editable`, and it backs both MDL-WIDGET01 (pluggable) and MDL-WIDGET07 (static): it answers "is this a real MDL property name anywhere", and both validators read it as "is this valid on THIS widget". The engine then only acts on each for the widget kinds that support it | `mdl/executor/widget_engine.go` (`numericTemplatePlaceholderRe`, the TextTemplate case), `mdl/backend/mutation.go` + `widgetobj/builder.go` + `mcp/widget.go` (`SetTextTemplateWithClientParams`), rules in `validate_widget_editability.go` (MDL-WIDGET20) and `validate_widget_contentparams.go` (MDL-WIDGET21) | For the template: the engine took the parameters path only for mxcli's `{AttrName}` spelling, so Mendix's own numeric `{1}` had no route and the template was written with `Parameters=[2]` (empty). A `dynamictext` with identical syntax is the control that localises it. For editability: **check the metamodel before implementing** — `PagesActionButton` has `ConditionalVisibilitySettings` but no `Editability`, and exactly 11 Pages types have editability (10 inputs + DataView) against 14 button types with none, so the request was impossible and the fix is to WARN. Both MDL spellings must be caught: `editable:` lowers to `Editable`, the bracket form `editable: [expr]` to **`EditableIf`** — dump the parsed `w.Properties` rather than assuming the key. Neither rule can be a `.fail.mdl`: they are warnings, `check` exits 0, and the fixture would report "negative test unexpectedly passed". Issue #928 | +| `run --local` / `test --local` on macOS (or Windows) die with a raw `fork/exec ~/.mxcli/mxbuild//modeler/mxbuild: exec format error`, while `setup mxbuild` and `docker build` correctly use Studio Pro's binary. `--mxbuild-path` does not help | `MxBuildCDNURL` branches on **GOARCH only, never GOOS** — both URLs are Linux tarballs (Mendix ships macOS mxbuild inside Studio Pro, not on the CDN), so an arm64 Mac caches a Linux *aarch64* ELF: the arch matches, so nothing notices until exec. `docker build` resolves via `resolveMxBuild` (PATH → Studio Pro → known locations → cache) and `setup mxbuild` via `NativeMxBuildForSetup`, but the local loop called `DownloadMxBuild` directly at `runlocal.go` and `StartServe` looked only at the cache. `LocalRunOptions.MxBuildPath` was documented as an override and never read for the serve binary, so there was no workaround either | `cmd/mxcli/docker/mxbuild_platform.go` (new: `ResolveMxBuildForLocal`, `binaryOS`, `verifyRunsHere`) + `runlocal.go` (the `DownloadMxBuild` call) + `mxserve.go` (`StartServe`) | Reuse the rule the codebase already had rather than inventing one: `NativeMxBuildForSetup(goos, version)` returns Studio Pro's path, or "" meaning "Linux, download is fine", or an error plus guidance. Order: explicit path → (non-Linux) Studio Pro → cache/CDN. **Do not stop the download on Windows** — the cache holds a Linux binary there deliberately, for Docker builds; the fix belongs at resolve/exec time. Guard exec with a magic-byte check (ELF / Mach-O incl. fat / PE) and let an unrecognised format through, since a shell wrapper has no magic and refusing on a guess would block a working setup. **Inject `goos` into the helpers** (`resolveMxBuildForLocalOn`, `verifyRunsOn`): the whole bug is platform-specific and is otherwise untestable from a Linux runner. Repro without a Mac: plant a Mach-O magic at the cache path and call `StartServe` — reproduces the reporter's message verbatim. Tests `TestBinaryOS`, `TestVerifyRunsOn_LinuxBinaryOnMac`, `TestResolveMxBuildForLocal_*`; reverting makes `ExplicitPathWins` fail **by downloading from the CDN for 34s**, which is the ignored override made visible. **Unverified:** no macOS host was available, so the Studio Pro discovery path itself (`resolveStudioProDirMacOS`) is exercised only by its own existing tests. Issue #916 | +| Windows: `run --local` behaves as though Java were undetected, and a local run can fail looking for Gradle | Two separate things. (a) **The `.exe` suffix**: `isJDK21` appended it, every CONSUMER did not — `--java-exe-path` handed to mxbuild and the path `exec.Command` runs to boot the runtime were both `\bin\java`, so a correctly-detected JDK was passed on in a form that need not resolve. Five sites built it by hand. (b) **Gradle is not mxcli's**: it ships inside the mxbuild bundle (`modeler/tools/gradle`, 8.5) and mxbuild invokes it — mxcli never calls gradle, so "Gradle missing" indicates a foreign/incomplete mxbuild bundle, usually #916 | `cmd/mxcli/docker/javaexe.go` (new `JavaExePath`) used from `mxserve.go`, `build.go`, `settle.go`, `localboot.go`, `detect.go`; `jdkSearchPathsFor` in `detect.go` | One helper, not five hand-built joins — the sixth call site would have repeated the bug. **Check the platform's own docs before adding a search path**: "add Studio Pro's JDK" turned out to be a non-task, because Mendix's install guide says Studio Pro installs **Eclipse Temurin 21** rather than bundling a JDK, so the existing Adoptium glob already IS Studio Pro's JDK; what was genuinely missing was the per-user `%LOCALAPPDATA%\Programs` install location. Inventing a `Mendix\\jdk` path would have been dead code. Make the not-found error list what was searched — "JDK 21 not found" alone sends a user reading mxcli's source. Inject `goos` (`jdkSearchPathsFor`, `javaExeName`): the only Windows CI job is the tunnel seam, scoped with `-run`, so it compiles Windows code and executes almost none of it — which is exactly how a missing `.exe` survives. **Unverified**: no Windows host; the fix is code-level with OS-injected tests. Reported via a user relay, not an issue | +| Studio Pro's version-control view shows an **entire** nanoflow/microflow as changed after editing one activity argument; `git diff` on `mprcontents/` is unreadable. A change and its revert leave a semantically identical document that shares no element IDs with the original. Separately, re-running an already-applied script prints `Modified …`/`Replaced …` for files it did not touch | Two independent things. (a) `create or replace` rebuilds the document and every sub-element gets a freshly random `$ID`; elision (ADR-0008) only covers the case where *nothing* changed, so a real change wrote a whole new identity set — measured 36 of 37 on a nanoflow, 21 of 22 on a microflow. (b) The `Modified …` lines are printed by the handler right after `ctx.Backend.Update*`, which returns nil whether or not storage elided the write | `modelsdk/canon/transplant.go` (`TransplantIDs`, called from `Reconcile` in `identity.go`); `mdl/executor/report_mutation.go` + `mdl/backend/writestats.go` | Match the incoming document against the stored one and reuse its `$ID`s: `$Type` + shape one level down (`Action=Microflows$LogMessageAction`) + `Name` as the LCS match key, positional fill in the gaps, then substitute **in place over every 16-byte binary** — a pointer is a primitive property a containment walk never sees, and any occurrence of one of the document's element IDs *is* a reference (the same insight `canon` rests on). **The correctness bar is lower than it looks**: a wrong match only makes a diff bigger, since every reference moves with the element; the one real failure is two elements sharing an `$ID`, so guard it explicitly (`dropCollisions`, run to a fixed point) and verify the resulting id set by read-back. **Do not touch `GUID`** — that is the database's identity and was already preserved (measured 8 of 8 through `ALTER ENTITY ADD ATTRIBUTE`). **A key built from content is the trap**: it stops an element matching itself the moment someone edits it, which is the case being fixed — key on shape and name only. **Watch for the control you invalidate**: `MXCLI_ALWAYS_WRITE=1` no longer changes the written bytes (identities are carried), so `TestWriteMicroflowTwice_ControlChurnsWhenElisionOff` became unprovable and had to move down a layer to the raw codec output (`TestRebuildChurnsSubElementIDs`); from the shell, control on mtimes rather than hashes. For the reporting half, downgrade the verb only on **positive** evidence (unit writes offered since the last report, none landed) so a mutation that never reaches unit storage — a theme file, a mock backend — reads exactly as before. Measured on the reporter's own project: 37/37 identities kept, diff down to the one changed line, insert/delete mints only the genuinely new elements, `mx check` unchanged at its 1 pre-existing error, both engines | +| `import from mapping M.IMM($json)` builds cleanly and then throws at runtime: `MicroflowException: key not found: Path(QName(None,),None,)` at `com.mendix.integration.importer.mapping.MappingCache.storeValueMappingElement`. Reported as "import mapping documents are broken / JSON path resolution is broken in mxcli's serialization" | **Not the mapping document — the ACTIVITY.** The Range and the result variable's cardinality are separate axes (#881), but an **unauthored** range set neither pointer, so `ForceSingleOccurrence` and `ConstantRange.SingleObject` both fell back to `SingleObject` — true for an object-rooted mapping. That is Studio Pro's **First** ("take one of a list"), a different activity. Studio Pro writes **both flags false** for a plain single-object import and expresses "one object" solely through `VariableType=ObjectType`. `all` and `first` and limit/offset all set the pointers explicitly, so only the **bare** form — the one every doc and example uses — was broken | `mdl/executor/cmd_microflows_builder_calls.go` (`addImportFromMappingAction`: add the `else` branch writing both pointers false); tests `mdl/executor/cmd_microflows_import_range_test.go`; fixture `mdl-examples/bug-tests/import-mapping-single-object-runtime.mdl` | **A valid model that fails only at runtime**: `mxcli check`, `mx check` (0 errors) and mxbuild all pass — the document is well-formed, it just means something else. The repo had **no runtime coverage of import mappings at all**; every existing test stopped at `mx check`, which is exactly the gap `.claude/skills/verify-in-runtime.md` exists for. **Get a Studio Pro-authored app and cross the variables** — `ako/TestApp` settled this in minutes after days of BSON diffing: run the SP microflow and an mxcli one *in the same boot*, then cross them (mxcli microflow → SP mapping, and mxcli mapping → SP structure+entity). The cross-test is what separated the activity from the document; an "isolation" where BOTH artifacts are mxcli's isolates nothing, and I asserted the wrong culprit twice before running it. **A control is only evidence if its harness is known good** — an earlier run had Studio Pro's own mapping failing too, which "exonerated" mxcli, but the probe app was independently broken. **Diff the ENCODING, not just the values**: `bson.M` loses key order and non-canonical extended JSON hides `int32` vs `int64`; dump with `MarshalExtJSONIndent(doc, true, …)` and compare raw key order via file order. Four differences found that way were all real and none causal (int32-vs-int64 on 14 numerics, root `MinOccurs` 0 vs 1, missing `MessageDefinition2`, blanked `OriginalValue`) — chasing document differences was the wrong tree entirely. Also: #882's "Studio Pro leaves `OriginalValue` empty" is contradicted by a second app, so it was generalised from too small a sample | +| `ALTER PAGE` at a **dotted** DataGrid2 column target (`insert after grid1.Col { container … }` / `replace grid1.Col with { container … }`) reports "Altered page", and `mx check` then aborts before checking anything: `System.InvalidCastException: Unable to cast object of type '…DivContainers.DivContainer' to type '…CustomWidgets.WidgetObject'`. Separately: a widget **inserted into a customContent cell** binds nothing — `ContentParams` naming an association path stores the raw path as the attribute name (**CE1613**) and `describe page` shows the path with its first hop silently gone | Two defects. (a) #891's guard covers only the BARE column form; the dotted form is legitimate and skips the guard entirely, so a **non-`column` body** still fell through to the generic widget path and was serialized into the grid's column list. The wrong thing is the *pairing* (widgets at an object-list target), not the target. (b) The entity-context walk descended into a pluggable widget's own widget properties but not into an object-list ITEM's — a column keeps its cell widgets one level deeper at `Objects[].Properties[content].Value.Widgets`, the same descent `findInWidgetChildren` gained in #834 — and never read a **pluggable** widget's datasource at all, so the grid's own entity was invisible too. Empty entity context ⇒ `resolveAssociationAttributePath` bails ⇒ the raw path is stored as the attribute name | `mdl/backend/pagemutator/mutator.go` (`refuseWidgetsAtColumnTarget` called from `InsertWidget`/`ReplaceWidget`; `findEntityContextInChildren`, `widgetOwnEntity`, `entityFromEntityRef`) | (a) Refuse when `columnRef != ""` — resolve the column FIRST so a mistyped name still reports not-found with the available names, then refuse and name both alternatives (`column …` body, or address the widget inside the cell by its own name, per #834). (b) Descend into `Value.Objects[].Properties[].Value.Widgets`, keyed on the BSON shape rather than the `columns` property key so Accordion groups and PopupMenu items are covered by the same code; resolve a pluggable datasource through the shared `entityFromEntityRef`, which also gives it the IndirectEntityRef (association) case the plain reader already had. **Note the guard cannot key on the target alone** the way #891's does, and a unit test on the mutator alone under-proves it: reverting only shows a nil-deref in the fixture. The honest control is the CLI + `mx check` — measured on Mendix 10.24.20.105674, exception before / 0 errors after, and CE1613 before / 0 after. Repros `mdl-examples/bug-tests/935-alter-page-widgets-at-column-target.mdl` and `935-customcontent-column-entity-context.mdl`. Issue #935 | +| `GRANT ON (READ (A))` **shrinks** an existing access rule instead of widening it: attributes granted by an earlier GRANT come back `None`, and `(CREATE, DELETE, READ *, WRITE *)` followed by a narrow re-grant loses create, delete and every write right. No error, no warning, no diff. Reported as WHERE-specific and as a "silent security regression" | Two defects. (a) The codec engine's upsert **cleared the matched rule's MemberAccesses and rebuilt them from the current statement alone**. The executor emits an entry for *every* member of the entity (unnamed ones at the rule's default, normally `None`), so each GRANT reset every member it did not mention; `AllowCreate`/`AllowDelete`/default rights were overwritten the same way. Legacy has merged additively since it shipped (`mergeAccessRule`), so this was an **engine regression**, not a longstanding gap — which is why the documented contract ("GRANT is additive … never removes permissions") held on one engine and not the default one. (b) **Both** engines matched a stored rule on its module-role set alone, ignoring `XPathConstraint`, collapsing two legitimate rules into one | `mdl/backend/modelsdk/domainmodel_security_write.go` (`AddEntityAccessRule`); `sdk/mpr/writer_security.go` (the upsert's match + `mergeAccessRule`); lattice shared via `mdl/types.AccessRightsLevel` / `HigherAccessRights` | **Measure the reported trigger before scoping the fix to it.** WHERE was a red herring — the same loss reproduced with no WHERE and with `READ *`, so a fix aimed at the constrained path would have passed the reporter's repro and left most of the bug. Equally, the report understated the damage: only attributes were mentioned, but structural rights went too, so re-derive the blast radius rather than inheriting it. **Run the other engine as the control** — `MXCLI_ENGINE=legacy` merging correctly is what turned "longstanding bug" into "regression" and handed over the reference semantics (OR create/delete, take the higher default, take the higher rights per member) instead of having to invent them. For (b), **check the platform's own docs before calling multi-rule a user error**: Mendix's refguide says "Rules are additive … all access rights of those rules are combined", so one role holding one rule per constraint is the normal way to write row-level security, and `mx check` confirms it at 0 errors. Put the constraint in the match key and treat the empty constraint as a *value*, not a wildcard, so an unconstrained and a constrained rule coexist and re-running a script stays idempotent (ADR-0008). **Fixing (b) breaks anything keyed on roles alone**: `formatAccessRuleResult` then echoed a *different* rule's rights back at the user, so it needed the constraint too (REVOKE passes `anyXPath`, since it narrows every matching rule). Rights merge on `None < ReadOnly < ReadWrite`, so a merge never narrows — that is what keeps GRANT and REVOKE inverses rather than two spellings of "set". Issue #936 | +| `Body: file from $Doc` on a consumed REST operation sends the literal text `$Doc` instead of the file. `mxcli check` passes, `mx check` reports 0 errors, the request returns **HTTP 200** — and the payload is 4 bytes where the document held 8090 | Mendix has **no binary request body**. `generated/metamodel` has exactly three: `Rest$JsonBody`, `Rest$StringBody`, `Rest$ImplicitMappingBody`. Both engines folded `FILE` into the `TEMPLATE` branch and wrote a `Rest$StringBody` whose `ValueTemplate` is the **expression text** — `mdl/backend/modelsdk/consumed_rest_write.go:218` (default engine) and `sdk/mpr/writer_rest.go:250` (legacy). `describe` shows it straight back as `Body: template '$Doc'`, so the round trip looks consistent | `mdl/executor/cmd_rest_clients.go` (`checkFileRequestBody`, called from `buildRestClientOperation`); `mdl/executor/validate_rest_mapping.go` (MDL-REST02); tests `mdl/executor/cmd_rest_clients_file_body_test.go`; fixture `mdl-examples/bug-tests/rest-file-request-body.fail.mdl` | **Refuse; do not write a different type** — there is no correct type for a CONSUMED OPERATION body. Binary POST is expressible, on the microflow REST CALL activity (`Microflows$BinaryRequestHandling`), which is where the refusal now points. A silent downgrade that returns 200 is the worst outcome available: every signal a user or an agent checks says success. Same shape as MDL-REST01, and one function is called from **both** the check pass and exec so `mxcli check` and `mxcli exec` cannot disagree. **Fix both engines, not just the one the reporter cited** — the report named only `sdk/mpr`, but the default engine is `modelsdk` and had the identical branch; a legacy-only fix would have left the default path broken. Keep the refusal **narrow**: `Response: file as $Doc` downloads correctly and is untouched (its own defect is a `CHANGE` on the result, which is separate). Judge an upload by what the server **echoes** (httpbingo `/post` returns `Content-Length` and the body), never by "the call did not throw" — a 200 proved nothing here | +| Binary upload is reported as impossible in MDL — "a consumed REST operation has no binary body, so use a Java action". A Studio Pro-authored binary POST also **describes with no body at all**, and re-executing that DESCRIBE produces a request that sends nothing | Right conclusion about the **wrong document**. Mendix models a binary request body on the microflow **REST CALL activity** as `Microflows$BinaryRequestHandling` + an action-level `RequestHandlingType: "Binary"` — a `Microflows$` type, which is why a metamodel grep for `Rest$*Body` finds only the three non-binary ones and "proves" it impossible. mxcli could **parse** it (`sdk/mpr/parser_microflow_actions.go`) and could neither write, read (modelsdk) nor describe it, so it survived a legacy read and vanished everywhere else | Grammar `mdl/grammar/domains/MDLMicroflow.g4` (`restCallBodyClause`); `mdl/ast/ast_microflow.go` (`RestBodyBinary`); `mdl/visitor/visitor_microflow_actions.go`; `mdl/executor/cmd_microflows_builder_calls.go`; writers `mdl/backend/modelsdk/microflow_write.go` + `sdk/mpr/writer_microflow_actions.go`; reader `mdl/backend/modelsdk/microflow_read_actions.go` (`restRequestHandlingFromRaw`); formatter `mdl/executor/cmd_microflows_format_action.go`; tests `mdl/executor/cmd_microflows_binary_body_test.go`; fixture `mdl-examples/bug-tests/rest-binary-post.mdl` | **"Not in the metamodel" needs the right namespace before it is a conclusion** — the search was for `Rest$…Body` and the answer lives under `Microflows$…RequestHandling`. Ask for a Studio Pro example instead of reasoning from an absence: one 4-KB unit settled in minutes what a metamodel grep had "disproved". **The discriminator and the sub-element must agree** — `RequestHandlingType` was hardcoded `"Custom"` in BOTH engines regardless of the handler; only the Binary case is derived here, because the others have no measured reference and work today (a latent mismatch worth a separate look). **The expression is the `Contents` MEMBER** (`$Doc/Contents`), not the document, and is stored as source text — quoting it sends the path as a string literal. **Verify a round trip by re-executing DESCRIBE and diffing the BSON against Studio Pro's**: mxcli's output reproduced the Studio Pro action exactly (same type, same discriminator, same expression) with mxbuild 0 errors, which is stronger than any assertion about what "should" be written. Beware the sibling trap: read support in one engine and not the other looks like a describe bug | +| A REST call's `body mapping Mod.EMM from $var` is written with the variable under a key Mendix does not read, an empty `ContentType`, and an action-level `RequestHandlingType` of `Custom` that contradicts its own `Microflows$MappingRequestHandling` sub-element. A form-data body is dropped entirely by DESCRIBE, so describe → edit → exec silently produces a call that posts nothing | `generated/metamodel` is decisive: `MicroflowsMappingRequestHandling` owns exactly three properties — `contentType` (enum Json\|Xml), `mappingId`, `mappingVariableName`. mxcli wrote `ParameterVariable`, which the type does not own, and omitted `MappingVariableName`; its own READER had known the right key since #843 and the writer was never corrected. `RequestHandlingType` was hardcoded `"Custom"` in both engines regardless of handler. `FormDataRequestHandling` / `AdvancedRequestHandling` can be parsed and not written, so a rewrite dropped them | `mdl/backend/modelsdk/microflow_write.go` (`requestHandlingTypeOf`, the Mapping case) and `sdk/mpr/writer_microflow_actions.go` (`restRequestHandlingTypeOf`, same case); guard `mdl/executor/validate_rest_request_handling.go` wired from `cmd_microflows_create.go`; tests `mdl/backend/modelsdk/microflow_restbody_test.go`, `mdl/executor/validate_rest_request_handling_test.go` | **A reader that compensates for a writer hides the writer's bug** — the `MappingVariableName` fallback made DESCRIBE round-trip correctly while every document mxcli wrote carried the wrong key. When a reader has a "the real key is X" comment, check that the writer agrees. **An unknown property is worse than a wrong value**: mxbuild tolerates it and Studio Pro refuses to open the document, so a green build proves nothing (same rule as the overlay section in CLAUDE.md). **Ask for one Studio Pro example per variant** — four microflows covering Custom/Mapping/FormData/Binary settled the discriminator question that a single example had left as "unverified, so leave it alone"; guessing the other five enum values from one measured case would have been a coin flip. **Parse-but-cannot-write is a data-loss path, not a gap**: DESCRIBE omits what the writer cannot express, so the omission looks like faithful output — refuse the rewrite (ADR-0005), and make the guard's allow-list the writable set so it stops refusing the moment a type becomes expressible. Not fixed: the legacy engine encodes `MappingId` as binary where Studio Pro stores a qualified-name string | +| `DESCRIBE MICROFLOW` emits MDL that is **not equivalent** to the microflow: a branch's activities are missing from the output, and re-executing the description builds a different flow. Reported as "roundtrip for @merge does not work" plus a separate complaint about the diagram coming back tangled | The graph is not **properly nested**. MDL's `IF/THEN/ELSE` is a single-entry/single-exit block; a Mendix microflow is an arbitrary graph, and when a branch re-enters a sibling branch's path there is no nesting that means the same thing. The describer walks it as a tree anyway. Measured on the reporter's graph: `log` ran on `¬c1 ∨ c2` and was described as `c1 ∧ c2` — with their actual expressions (`not(true)` on both decisions) the original **always** logs and the description **never** does, i.e. the exact inverse, not a near-miss. The tangled diagram is the *same* cause: `findSplitMergePointsForGraph` and `commonMergeAfter` are two independent merge-finders that agree on every nested graph and disagree here (merge2 vs merge1), so the emitted `@merge(x,y)` places the merge before an activity that structurally follows it | `mdl/microflowgraph/structure.go` (`Analyze` — post-dominators + branch-body overlap); `mdl/linter/rules/flow_irreducible_graph.go` (MDL-FLOW01); `irreducibleGraphWarnings` in `mdl/executor/cmd_microflows_show.go` | **Reconstruct the reporter's graph from the coordinates in their own output** before touching code — `@position`/`@merge` values pin the shape exactly, and `commonMergeAfter` reproducing their `@merge(-200,173)`/`@merge(100,173)` byte-for-byte is what confirmed the reconstruction was right rather than merely plausible. **Do not reuse the describer's join search in the detector**: there are already two implementations and they disagree on exactly the graphs being detected, so a detector built on either inherits whichever is wrong. Post-dominance is self-contained (`pdom(n) = {n} ∪ ⋂ pdom(succ)`, universe-initialised so it converges on the cyclic graphs retry loops create) and makes a describer/detector disagreement a *signal*. Classification: overlap with **one** entry point is a shared suffix (recombinable — the guards fold, `¬c1 ∨ c2`), **two or more** is genuine crossing (interleaved — needs activity duplication or a synthetic boolean per Böhm-Jacopini, so it is refused). **The false positive is worse than the bug** — flagging ordinary nesting would warn on nearly every microflow — so pin the shapes most likely to trip it: `if` with no `else`, an inner split whose join *is* the outer's, branches that both return, and error-handler flows (excluded, or every activity with one gets flagged). **A rule that never runs and a rule that finds nothing look identical**: prove the wiring with a forced-fire control (temporarily append a finding per microflow, lint a real project, confirm the count, revert) — `mxcli lint` reporting "No issues found" on a blank app is also just marketplace modules being excluded. Rendering it faithfully needs a label form; Mendix gives no field to hang one on (`Microflows$ExclusiveMerge` stores only `RelativeMiddlePoint` and `Size` — confirmed against `generated/metamodel`, `modelsdk/gen` and real 11.13 documents), but labels only need to be deterministic, not stored. Issue #923; design in `docs/11-proposals/PROPOSAL_structured_microflow_description.md` | diff --git a/.claude/skills/mendix/create-page.md b/.claude/skills/mendix/create-page.md index 651e59b41..720422c50 100644 --- a/.claude/skills/mendix/create-page.md +++ b/.claude/skills/mendix/create-page.md @@ -846,6 +846,45 @@ alter page Mod.Home { For theme images, use paths relative to `theme/web/` (e.g., `img/logo.svg` → `theme/web/img/logo.svg`). +**A per-row image URL comes from the entity, two ways.** `imageUrl` is a text +template, so it takes either spelling: + +```sql +-- named placeholder: shortest form for a single attribute +pluggablewidget 'com.mendix.widget.web.image.Image' cardImage ( + datasource: imageUrl, imageUrl: '{PictureUrl}' +) + +-- numbered placeholders + contentparams: needed for several values, or a format block +pluggablewidget 'com.mendix.widget.web.image.Image' cardImage ( + datasource: imageUrl, + imageUrl: '{1}/{2}', contentparams: [{1} = BaseUrl, {2} = PictureUrl] +) +``` + +Every `{N}` must have a matching parameter — Mendix rejects a shortfall with +`CE0720` ("place holder index N is greater than …, the number of parameter(s)"). +Parameters with no `{N}` to fill are reported by MDL-WIDGET21 rather than +dropped in silence. + +### Buttons Have Visibility, Not Editability + +`editable:` only exists on **input** widgets — Mendix gives exactly eleven page +widgets an editability setting (textbox, textarea, checkbox, datepicker, +dropdown, radiobuttons, referenceselector, inputreferencesetselector, +filemanager, imageuploader, and dataview). No button of any kind has one, so +`editable:` on a button is reported by MDL-WIDGET20 and does nothing. + +To disable a button conditionally, hide it instead — buttons do support +conditional visibility — or put the condition in the microflow it calls: + +```sql +actionbutton btnSubmit ( + caption: 'Submit', action: microflow Mod.ACT_Submit, + visible: [$currentObject/Status = Mod.Status.Draft] +) +``` + ### CONTAINER / CUSTOMCONTAINER Widgets Generic container for grouping widgets. `customcontainer` is an alias for `container` (both map to `Forms$DivContainer`): diff --git a/.claude/skills/mendix/manage-security.md b/.claude/skills/mendix/manage-security.md index 1fc8c9eae..06576b773 100644 --- a/.claude/skills/mendix/manage-security.md +++ b/.claude/skills/mendix/manage-security.md @@ -141,6 +141,14 @@ revoke view on page MyModule.Customer_Overview from MyModule.User; ### Entity Access (CRUD) GRANT is **additive** — it merges with existing access, never removes permissions. +Rights rank `None < ReadOnly < ReadWrite` and a merge takes the higher, so use +`revoke` to take access away; a narrower `grant` will not do it. + +A rule is identified by its role set **and** its `where` constraint. Two grants +with the same constraint update one rule; with different constraints they make +two, which Mendix combines at runtime (a role's access is the union of every rule +naming it). So adding a `where` to an existing grant creates a second rule — it +does not narrow the first. ```sql -- Full access (all CRUD + all members) diff --git a/.claude/skills/mendix/rest-call-from-json.md b/.claude/skills/mendix/rest-call-from-json.md index e961f60c8..029f6d95a 100644 --- a/.claude/skills/mendix/rest-call-from-json.md +++ b/.claude/skills/mendix/rest-call-from-json.md @@ -184,6 +184,60 @@ end; --- +## Step 6 — Sending a Request Body (Optional) + +Everything above receives data. To send it, an inline `REST CALL` takes one of +four body forms: + +```sql +-- 1. String template with placeholders +body '{{"name": "{1}", "qty": {2}}' with ({1} = $Name, {2} = toString($Qty)) + +-- 2. An expression that already yields the payload +body $JsonPayload + +-- 3. An export mapping (entity -> JSON) +body mapping Module.EMM_Item from $Item + +-- 4. Raw bytes — a file document's CONTENTS member +body binary $Doc/Contents +``` + +### Uploading a file + +The expression is the file document's `Contents` **member**, not the document, +and the content type goes on a header — the body clause carries only the bytes: + +```sql +create or modify microflow Module.POST_Document_Upload ( + $Doc: Module.UploadedFile +) +returns boolean as $Ok +begin + declare $Ok boolean = false; + $Response = rest call post 'https://api.example.com/documents' + header 'ContentType' = 'application/pdf' + body binary $Doc/Contents + timeout 300 + returns response; + set $Ok = $Response/StatusCode = 200; + return $Ok; +end; +/ +``` + +`$Doc` must be a specialization of `System.FileDocument`. Downloading is the +mirror image — `returns Module.UploadedFile` stores the response body in a new +file document. + +**A consumed REST CLIENT document cannot do this.** Its body is one of +`Rest$JsonBody`, `Rest$StringBody` or `Rest$ImplicitMappingBody` — all textual — +so `Body: file from $Doc` in a `create rest client` operation is refused as +**MDL-REST02**. Binary uploads belong in a microflow. (`Response: file as $Doc` +on an operation is fine; downloads work either way.) + +--- + ## Complete Example — Bible Verse API ```sql @@ -271,6 +325,8 @@ end; | StartEvent behind first activities | Default posX=200 vs @position(-5,...) | Fixed: executor pre-scans for first @position and shifts StartEvent left | | `TypeCacheUnknownTypeException` | Wrong BSON `$type` names | `ImportMappings$ObjectMappingElement` / `ImportMappings$ValueMappingElement` (no `import` prefix) | | Attribute not found in Studio Pro | Attribute not fully qualified | Must be `Module.Entity.AttributeName` in the BSON | +| `CE0117 "Error(s) in expression."` at the end event after a REST call | `returns response` binds a `System.HttpResponse`, so returning it from a `returns string` microflow is a type error | Match the microflow's return type to what you do with the response — e.g. `returns boolean` and `set $Ok = $Response/StatusCode = 200` | +| Upload returns HTTP 200 but the server received a few bytes | `Body: file from $Doc` on a REST CLIENT document used to be written as the literal text `$Doc` | Now refused as MDL-REST02 — upload from a microflow with `body binary $Doc/Contents` | --- diff --git a/.claude/skills/mendix/rest-client.md b/.claude/skills/mendix/rest-client.md index b983bc45b..8cdc28598 100644 --- a/.claude/skills/mendix/rest-client.md +++ b/.claude/skills/mendix/rest-client.md @@ -112,6 +112,23 @@ body: mapping Module.RequestEntity { } ``` +**There is no binary/file body here.** A consumed operation's body is one of +`Rest$JsonBody`, `Rest$StringBody` or `Rest$ImplicitMappingBody`, so a file +document has nowhere to go. `body: file from $Doc` is refused as **MDL-REST02** +— it used to be written as a string body holding the literal text `$Doc`, which +returns HTTP 200 with a 4-byte payload. + +Upload binary from a **microflow** instead, which does have a binary body: + +```sql +rest call post 'https://api.example.com/upload' + header 'ContentType' = 'application/pdf' + body binary $Doc/Contents + returns response; +``` + +`response: file as $Doc` on an operation is unaffected — downloads work. + ### Response Types ```sql diff --git a/.claude/skills/mendix/run-local.md b/.claude/skills/mendix/run-local.md index 60ea7830a..0194eaca1 100644 --- a/.claude/skills/mendix/run-local.md +++ b/.claude/skills/mendix/run-local.md @@ -58,6 +58,38 @@ association catalog only at startup; behavioural changes are hot-reloaded. createdb -h 127.0.0.1 -U mendix "$(basename app.mpr .mpr | tr '[:upper:]' '[:lower:]')" ``` +### Which mxbuild the loop uses + +- **Linux** — the CDN download cached at `~/.mxcli/mxbuild//`, as before. +- **macOS / Windows** — **Studio Pro's bundled mxbuild**, resolved before the cache. + The Mendix CDN publishes **Linux archives only** (the URL varies by architecture, + not by OS), so a cached download on a Mac is a Linux `aarch64` ELF — the arch + matches, which is why it looks fine until exec. +- `--mxbuild-path` overrides both, and is now honoured by the local loop (it used + to be documented and ignored — #916). + +If nothing runnable is found, the command says so up front instead of failing with +`fork/exec …: exec format error`: + +``` +mxbuild from the Mendix CDN is a Linux binary and cannot run natively on darwin + Install Mendix Studio Pro 11.12.0 and use its bundled mx … + Or point mxcli at it explicitly with --mxbuild-path. +``` + +### Windows and macOS toolchain + +- **JDK 21** — Mendix Studio Pro does not bundle one; its installer puts **Eclipse + Temurin JDK 21** in the usual place, which is where mxcli looks (`Eclipse + Adoptium` / `Java` / `Microsoft` under both Program Files, plus the per-user + `%LOCALAPPDATA%\Programs\…` installs winget produces). `JAVA_HOME` wins over all + of them. When nothing is found the error now lists every location it searched. +- **Gradle** — bundled inside mxbuild (`modeler/tools/gradle`) and invoked by + mxbuild, not by mxcli. Studio Pro extracts its own copy to the parent of its + install directory (usually `C:\Program Files\Mendix`). A "Gradle not found" + from a local run therefore points at an incomplete or foreign mxbuild bundle — + check which mxbuild was resolved before looking for a system Gradle. + ## The intended loop ```bash diff --git a/.claude/skills/mendix/write-microflows.md b/.claude/skills/mendix/write-microflows.md index 83f95f83b..d5012fd07 100644 --- a/.claude/skills/mendix/write-microflows.md +++ b/.claude/skills/mendix/write-microflows.md @@ -1383,6 +1383,17 @@ $response = rest call post 'https://api.example.com/items' returns string on error continue; +-- POST a BINARY body (upload a file document's contents) +-- The expression is the FileDocument's Contents MEMBER, not the document +-- itself, and the content type goes on a header. A consumed REST CLIENT +-- document has no binary body — `Body: file from $Doc` there is refused as +-- MDL-REST02 — so binary uploads belong here. +$response = rest call post 'https://api.example.com/upload' + header 'ContentType' = 'application/pdf' + body binary $Doc/Contents + timeout 300 + returns response; + -- GET with URL template parameters $response = rest call get 'https://api.example.com/users/{1}' with ( {1} = toString($UserId) @@ -1411,6 +1422,27 @@ rest call delete 'https://api.example.com/items/{1}' with ( - `returns response` — returns `System.HttpResponse` object - `returns mapping Module.ImportMapping as Module.Entity` — single object result - `returns mapping Module.ImportMapping as list of Module.Entity` — list result +- `returns Module.MyFile` — store the body in a **file document** + +**The file document form takes a specialization, never `System.FileDocument` +itself.** Mendix rejects the base type as a return type with `CE0362`, and +MDL064 reports it before the write. Create one first: + +```mdl +create persistent entity MyModule.MyFile extends System.FileDocument (); + +create microflow MyModule.ACT_Download ($Location: String) +begin + $file = rest call get '{1}' with ({1} = $Location) + header 'Accept' = 'application/octet-stream' + timeout 300 + returns MyModule.MyFile; +end; +``` + +There is **no** equivalent for an HttpResponse specialization: Mendix allows only +`User`, `FileDocument`, `Image` and `Paging` to be specialized (`CE1540`), so +`returns response` already names the only type that result can have. **Pick `as` vs `as list of` based on the call site, not the mapping shape.** The same import mapping can yield either a single object or a list — Studio Pro stores the cardinality on the microflow's `ImportMappingCall` (`Range.SingleObject` + `ForceSingleOccurrence`). Use `as Module.Entity` when the response is a single object (the mapping may still be list-typed; Studio Pro binds the first item). Use `as list of Module.Entity` when the response should bind a list. Mismatching the cardinality with the surrounding code produces `mx check` `CE0117` at the End event or `CE0013` / `CE0100` on downstream loop / aggregate / list-operation activities. diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d8ffd81..8f7379307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added + +- **Import mappings can reach a nested leaf without an entity per level** (#927) — `Attr = customer/contact/email` binds a value several levels below the object element it belongs to, which is the shape Studio Pro produces when you tick a nested leaf without ticking its parents: one entity, values pulled from several depths. Previously MDL had no way to write it, so every object level in a response became an entity whose only content was an association — one generated endpoint added 21 entities, almost all pass-throughs. + + Two shapes are refused rather than written, each measured on mxbuild 11.13 rather than assumed: an **export** mapping cannot collapse levels (**CE5015** — it has to produce the intermediate node; the same member in an import mapping builds at 0 errors, and the same export mapping with only top-level members builds at 0 errors), and an import member cannot cross a `0..*` element (**CE0256** "a schema element with wrong occurrence"). Both refusals name the build error they prevent. + +### Fixed + +- **`DESCRIBE` no longer mis-reads a mapping that binds a nested leaf** (#927) — value elements were printed as the last segment of their JsonPath alone, so a project holding `(Object)|customer|name` described as `CustomerName = name`. That is a description of a model that does not exist, and re-executing mxcli's own output failed with `"name" is not a member of the JSON structure at (Object)`. Members are now rendered relative to the enclosing object element, on both engines and for both mapping kinds. Nothing was ever corrupted — the #882 guard refused the bad re-execution — but the description was wrong. + + +### Fixed + +- **A pluggable widget's text template no longer drops its `contentparams`** (#928) — `imageUrl: '{1}', contentparams: [{1} = PictureUrl]` on an Image widget stored a template with an **empty** parameter list, and mxbuild rejected it with `CE0720` ("place holder index 1 is greater than 0, the number of parameter(s)") on the **first write** — no describe round-trip needed. The engine took the parameters path only for mxcli's `{AttrName}` convenience spelling, so Mendix's own numeric `{1}` form had no route. Both spellings now reach the same stored shape; a `dynamictext` with identical syntax was the control that localised it to the pluggable path. + +### Added + +- **`MDL-WIDGET20` — `editable:` on a widget that has no editability** (#928) — accepted on any widget and silently dropped, so a button bound this way passed check, passed the build, and stayed enabled: a silent functional failure rather than a caught error. It cannot be implemented as asked. Measured against `generated/metamodel`, exactly **11** Pages types carry `Editability`/`ConditionalEditabilitySettings` — ten input widgets plus DataView — and **none** of the fourteen button types does; a button has conditional *visibility*, not editability. So mxcli reports it instead, naming visibility as the alternative. Both spellings are caught: `editable: 'x'` and the bracket form `editable: [expr]`, which is the one that genuinely works on inputs and would be the more surprising silent drop. A test pins the list against the metamodel so it cannot drift. + +- **`MDL-WIDGET21` — `contentparams` with no placeholder to consume them** — the residue of the fix above: parameters supplied where no property text carries a `{1}`-style placeholder have nothing to attach to and are dropped on write. Previously silent. + + Root cause of both reports is one thing: the allow-lists behind MDL-WIDGET01 and MDL-WIDGET07 are widget-type **agnostic**. `isBuiltinPropName` is a single flat list holding both `ContentParams` and `Editable`; it answers "is this a real MDL property name anywhere", and both validators read it as "is this valid on this widget". + + ### Fixed +- **The Mendix 10.24 nightly is green again** — `14-project-settings-examples.mdl` set `DecimalScale`, which 10.24 does not have, so `TestMxCheck_DoctypeScripts` failed on that matrix entry on both engines while passing on every 11.x. Measured against blank projects: 10.24 stores 11 model settings, 11.6.6 stores 12, and `DecimalScale` is the only difference — each of the other five settings in that statement is accepted on 10.24 on its own. mxcli's refusal was correct (Studio Pro will not open a model carrying a property its version does not define, and mxbuild does not catch it); the example simply was not version-gated, and because the refusal covers the whole statement, one unsupported setting took five portable ones down with it. It is now its own `-- @version: 11.0+` section. + + A new guard, `TestDoctypeScriptsParseAfterVersionFiltering`, filters every doctype script for each nightly matrix version and asserts the result still parses. It needs no mxbuild, so a mis-gated script fails in seconds on push rather than hours later in a single nightly job. Writing it surfaced the trap that makes this easy to get wrong: a `/** */` block is a *documentation* comment bound to the statement after it, so gating the statement while leaving its comment outside the section orphans the comment and the script stops parsing — reported at the *next* statement, tens of lines away, which reads like an unrelated syntax error. + - **A microflow's StartEvent no longer moves on a describe→exec round-trip** — the start has no MDL statement to annotate and `DESCRIBE` cannot emit its position, so the builder always derived one (first annotated activity minus one spacing unit). A Studio-Pro-authored flow whose start sat at `145;200` came back at `100;200` — the only coordinate in it that did not survive. The position is now carried over from the microflow being replaced, the way the folder and allowed module roles already are; a fresh `CREATE` still derives it. ### Added @@ -29,6 +57,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **`DROP FOLDER` no longer orphans the documents inside it** (#892) — the command's contract ("the folder must be empty") existed only in a comment; nothing checked, so dropping a populated folder left every document pointing at a container that no longer existed. Nothing was deleted: the documents were **orphaned**, losing their module qualification (`FeedbackModule.IMM_PostResponse` → `.IMM_PostResponse`) so nothing could resolve them and mxbuild reported CE1613. Reproduced on a *stock* blank app, where `FeedbackModule/Private/Resources/Mappings` holds four documents. The drop is now refused, naming what is inside. The guard reads the type-agnostic unit list rather than the per-kind document lists, so it cannot inherit the blind spot that caused the bug, and it fails closed when contents cannot be determined. - **`LIST FOLDERS` counts mappings, JSON structures, regular expressions and image collections** (#892) — these five kinds were missing from the per-kind listing, so a folder holding them rendered as `[0]`. That empty count is what made dropping the folder look safe. +### Added + +- **`MDL-FLOW01` — a microflow whose branches cannot be described faithfully** (#923) — `DESCRIBE MICROFLOW` renders control flow as nested `IF/THEN/ELSE`, which only works when the graph is properly nested. A Mendix microflow is an arbitrary graph, and when a branch re-enters a sibling branch's path there is no nesting that means the same thing — the describer emitted one anyway, silently. On the reported graph the log activity ran on `¬c1 ∨ c2` and was described as `c1 ∧ c2`; with the reporter's actual expressions the original **always** logs and the description **never** does, so re-executing it produced the opposite program. + + The new rule reports such microflows, and `DESCRIBE` now emits a `-- WARNING:` comment naming the decision's canvas position and refusing the round trip, rather than handing back MDL that means something else. Detection is by post-dominance in the new `mdl/microflowgraph` package — deliberately independent of the describer's own merge search, since mxcli has two of those and they disagree on exactly these graphs (which is also why the regenerated diagram came back tangled: the emitted `@merge` lands before an activity that structurally follows it). Findings are split into *recombinable* (the branches share one suffix, so the conditions can usually be folded into one decision by hand) and *interleaved* (genuinely crossed — not expressible without duplicating an activity or adding a helper variable, so it is refused rather than rewritten). + + This is the detector only. Rendering these graphs faithfully needs a label form, designed in [PROPOSAL_structured_microflow_description.md](docs/11-proposals/PROPOSAL_structured_microflow_description.md) and gated on prevalence data this rule exists to collect. The rule is deliberately **not** part of `mxcli report`'s score: the model is valid and builds cleanly, and what fails is mxcli's ability to describe it. + +### Fixed + +- **`GRANT` widens an access rule again instead of shrinking it** (#936) — re-granting a role on an entity rebuilt the rule from that one statement, so anything an earlier `GRANT` had allowed came back `None`: `(READ (Name, Email))` followed by `(READ (Phone))` left a rule reading `Phone` alone. Structural rights went the same way, which the report did not mention — `(CREATE, DELETE, READ *, WRITE *)` followed by a narrow re-grant lost create, delete and every write right. Nothing was said at any point: no error, no warning, no diff. + + The reported trigger was wrong in a way that matters for re-testing: `WHERE` is **not** required. The same loss reproduces with no constraint at all and with `READ *`, so a fix aimed at the constrained path would have satisfied the repro and left most of the bug. The legacy engine has merged additively since it shipped, so this was a regression in the codec engine — the default — rather than a longstanding gap, and it is why the documented contract ("GRANT is additive … never removes permissions") held on one engine and not the other. Rights merge on `None < ReadOnly < ReadWrite`, so a merge only ever widens; narrowing stays `REVOKE`'s job. + +- **Two XPath constraints for one role are two access rules again** (#936) — the rule upsert matched on the module-role set alone and ignored `XPathConstraint`, so `GRANT … WHERE 'A'` followed by `GRANT … WHERE 'B'` folded the second onto the first and overwrote its constraint, destroying a rule with no warning. Mendix combines the rights of every rule naming a given module role ("Rules are additive", refguide/access-rules), making one rule per constraint the ordinary way to write row-level security — a pattern MDL could not previously express. The constraint is now part of the match key on **both** engines, with the empty constraint treated as a value rather than a wildcard, so a constrained and an unconstrained rule coexist and re-running a script stays idempotent. `mx check` accepts the result at 0 errors. + + Consequently the `Result:` line after a `GRANT` now reports the rule the statement actually touched, rather than the first rule mentioning that role. + ## [0.18.0] - 2026-08-14 Headline: **mxcli can now maintain a project it did not author.** Marketplace modules install and update headlessly — carrying the GUIDs the database keys on, the role grants that live outside the module, and the MPR v2 format `mx module-import` silently collapses — and `marketplace diff` reports which elements were edited locally before an update replaces them. Alongside that, a write that changes nothing no longer touches the file, five more document types become authorable (task queues, scheduled events, regular expressions, validation rules, menus), and a long tail of activities that could be written but not read back stop disappearing from the describe → edit → re-exec loop. Separately, the Windows and macOS binaries stop shipping the embedded tunnel — 13.5 MB smaller, and no longer carrying the tunnelling stack that had Defender and enterprise EDR blocking mxcli on managed corporate endpoints. diff --git a/CLAUDE.md b/CLAUDE.md index 970992194..d6e902319 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -334,6 +334,16 @@ so comparing bytes would skip nothing. The policy lives in `modelsdk/canon` `modelsdk/mpr/writer_core.go` (`updateUnit` *and* `WriteTransaction.WriteUnit` — `codec.Store` reaches storage through the latter) and `sdk/mpr/writer_units.go`. +When something *has* changed, `Reconcile` still does not let the rebuild's fresh +`$ID`s reach disk: `canon.TransplantIDs` matches the incoming document against the +stored one element by element (by `$Type` and shape, by `Name` where there is one, +LCS-anchored within each list) and puts the **stored** `$ID` back on every element +that still corresponds. Without it a one-argument edit re-minted 36 of a nanoflow's +37 element identities and Studio Pro painted the whole document as changed (#910). +Its correctness bar is lower than it looks and worth knowing: a *wrong* match only +makes a diff bigger, because every reference is rewritten with the element — the +one real failure is two elements sharing an `$ID`, which `dropCollisions` guards. + Three rules follow, and each has already been violated once: 1. **Never rewrite an element `$ID` without rewriting every reference to it in the @@ -341,7 +351,10 @@ Three rules follow, and each has already been violated once: `ChildProperty`, so a containment walk traverses the whole document and never sees one. PR #125 renumbered IDs this way and made projects unopenable (`KeyNotFoundException` at `ResolvePostponedProperties`). A unit is rewritten - wholesale or not at all. + wholesale or not at all. The transplant obeys this by substituting over *every* + 16-byte binary in the document rather than a maintained list of pointer + properties — any occurrence of one of the document's element IDs is a reference + by definition, the same insight the canonical form rests on. 2. **Adding a write path means wiring it to `canon.Reconcile`.** A new choke point that writes directly will silently churn while everything else is quiet — the worst kind of inconsistency, because the diff blames the wrong change. @@ -360,8 +373,20 @@ qualified name breaks that assumption and invalidates the argument in ADR-0008. `MXCLI_ALWAYS_WRITE=1` forces every write to land, for bisecting. It does not disable identity preservation. **Any test asserting "nothing changed" must include -the control run with it set** — otherwise the test passes against a build that -never had the fix, which is exactly how PR #125 shipped green. +a control** — otherwise the test passes against a build that never had the fix, +which is exactly how PR #125 shipped green. Note what the control can now be: +since identities are carried, a forced write of an in-sync unit produces the +**same bytes**, so "flip `MXCLI_ALWAYS_WRITE` and watch the content change" no +longer distinguishes anything (measured: same sha, mtime moves). Control on the +**rebuild** instead — encode the document twice and show the raw codec output +differs (`TestRebuildChurnsSubElementIDs`) — or, from the shell, on **mtimes** +rather than hashes. + +The executor reports which of the two happened: a statement whose unit writes were +all elided prints `Unchanged nanoflow: …` instead of `Replaced nanoflow: …` +(`ExecContext.ReportMutation`, fed by each writer's `WriteStats`). The verb is only +downgraded on positive evidence — writes offered, none landed — so a mutation that +never touches unit storage is reported exactly as before. ### The Tunnel Is Linux-Only, On Purpose — Do Not "Restore" It @@ -750,7 +775,7 @@ Full syntax tables for all MDL statements (microflows, pages, security, navigati **Implemented:** - Default styling + runtime theme switching (`mxcli theme list/show/apply/remove/switcher`, `mxcli new --theme`): three embedded themes (**signal** light-first, **ledger** light-first, **console** dark-first), each a palette in `theme/web/custom-variables.scss` + a shared Atlas wiring partial + a theme partial imported from `theme/web/main.scss` (which compiles last), plus vendored fonts. **No model changes**, so it hot-applies under `run --local --watch` and cannot affect a build. Generated regions are digest-fenced: a block carrying local edits is refused rather than overwritten. Applying a theme removes the previous one. `--variant auto` (default) ships both palettes — the app follows `prefers-color-scheme` before first paint and honours a `theme-light`/`theme-dark` class on ``; `light`/`dark` bakes one. `theme switcher install` is the only part that writes to the model (JS actions + a nanoflow for a toggle button). Package: `cmd/mxcli/theme/`. See `docs/11-proposals/PROPOSAL_default_styling.md` - MPR v1/v2 reading and writing -- Idempotent writes (ADR-0008): a unit whose new content is semantically equal to what is stored is **not written**, so re-running an MDL script against an in-sync project leaves the `.mpr` and `mprcontents/` byte-identical and Studio Pro shows no version-control changes. Comparison is on a canonical form (element `$ID`s normalised away — a rebuild mints them randomly, so byte comparison would skip nothing); `Microflows$Microflow.StableId` is carried from the stored document rather than re-minted, because the build derives every client-callable microflow's operation id from it. One policy in `modelsdk/canon`, called from both engines' write choke points. `MXCLI_ALWAYS_WRITE=1` disables elision (not preservation) for bisecting. See `docs-site/src/internals/idempotent-writes.md` +- Idempotent writes (ADR-0008): a unit whose new content is semantically equal to what is stored is **not written**, so re-running an MDL script against an in-sync project leaves the `.mpr` and `mprcontents/` byte-identical and Studio Pro shows no version-control changes. Comparison is on a canonical form (element `$ID`s normalised away — a rebuild mints them randomly, so byte comparison would skip nothing); `Microflows$Microflow.StableId` is carried from the stored document rather than re-minted, because the build derives every client-callable microflow's operation id from it. When a write **does** land, `canon.TransplantIDs` matches the rebuild against the stored document and reuses its element `$ID`s (rewriting every pointer in the same pass), so a changed document's diff is the change rather than a wholesale replacement — measured on #910's nanoflow: 1 of 37 identities survived an argument edit before, 37 of 37 after, and a change plus its revert returns to the original bytes. Inserting or deleting an activity mints IDs only for the genuinely new elements. One policy in `modelsdk/canon`, called from both engines' write choke points. `MXCLI_ALWAYS_WRITE=1` disables elision (not preservation) for bisecting — which means it no longer changes the resulting bytes, only the mtimes. The executor's output distinguishes the two: `Unchanged nanoflow: …` where the write was skipped. See `docs-site/src/internals/idempotent-writes.md` - Domain model (entities, attributes, associations) - ALTER ENTITY (add/rename/modify/drop attributes, indexes, documentation) - Microflows/Nanoflows with 60+ activity types, JavaScript action calls, nanoflow validation parity diff --git a/cmd/bsondump/main.go b/cmd/bsondump/main.go new file mode 100644 index 000000000..f2dd2ae5a --- /dev/null +++ b/cmd/bsondump/main.go @@ -0,0 +1,34 @@ +// Command bsondump prints an MPR v2 .mxunit as indented canonical extended +// JSON, so numeric/boolean properties and their BSON types are readable. +// Development helper; not part of the shipped CLI surface. +package main + +import ( + "fmt" + "os" + + "go.mongodb.org/mongo-driver/bson" +) + +func main() { + if len(os.Args) != 2 { + fmt.Fprintln(os.Stderr, "usage: bsondump ") + os.Exit(2) + } + data, err := os.ReadFile(os.Args[1]) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + var doc bson.M + if err := bson.Unmarshal(data, &doc); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + out, err := bson.MarshalExtJSONIndent(doc, true, false, "", " ") + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Println(string(out)) +} diff --git a/cmd/mxcli/cmd_lint.go b/cmd/mxcli/cmd_lint.go index 04d8ee3a6..a61187ba1 100644 --- a/cmd/mxcli/cmd_lint.go +++ b/cmd/mxcli/cmd_lint.go @@ -32,6 +32,7 @@ Built-in rules check for: - Entity access rules (SEC001) - persistent entities need access rules - Password policy (SEC002) - password minimum length should be 8+ - Demo users (SEC003) - demo users should be off at Production security + - Un-describable branch structure (MDL-FLOW01) - decision branches that do not nest, so DESCRIBE cannot render them faithfully Bundled Starlark rules (in .claude/lint-rules/): Security: @@ -139,6 +140,7 @@ Examples: rules.NewExclusiveSplitCaptionRule(), rules.NewErrorHandlingOnCallsRule(), rules.NewNoContinueErrorHandlingRule(), + rules.NewIrreducibleFlowGraphRule(), // MDL-FLOW01 - graph structure vs MDL nesting } lintRulesDir := filepath.Join(projectDir, ".claude", "lint-rules") if starlarkRules, err := linter.LoadStarlarkRulesFromDir(lintRulesDir); err == nil { diff --git a/cmd/mxcli/docker/build.go b/cmd/mxcli/docker/build.go index df63a5158..0d761089c 100644 --- a/cmd/mxcli/docker/build.go +++ b/cmd/mxcli/docker/build.go @@ -161,7 +161,7 @@ func Build(opts BuildOptions) error { fmt.Fprintf(w, "Running MxBuild (target=portable-app-package)...\n") fmt.Fprintf(w, " Output: %s\n", outputDir) - javaExePath := filepath.Join(javaHome, "bin", "java") + javaExePath := JavaExePath(javaHome) cmd := exec.Command(mxbuildPath, "--target=portable-app-package", diff --git a/cmd/mxcli/docker/detect.go b/cmd/mxcli/docker/detect.go index 9e84e095c..9a0cd6b68 100644 --- a/cmd/mxcli/docker/detect.go +++ b/cmd/mxcli/docker/detect.go @@ -324,7 +324,19 @@ func resolveJDK21() (string, error) { } } - return "", fmt.Errorf("JDK 21 not found; set JAVA_HOME or install JDK 21") + // Name what was searched. "JDK 21 not found" alone sent a Windows user + // hunting through mxcli's source for the detection logic; the list makes it + // obvious whether their JDK simply sits somewhere unlisted. + searched := jdkSearchPaths() + msg := "JDK 21 not found" + if jh := os.Getenv("JAVA_HOME"); jh != "" { + msg += fmt.Sprintf("\n JAVA_HOME is set to %s but is not a JDK 21 (java -version reports %s)", jh, javaVersionString(jh)) + } + if len(searched) > 0 { + msg += "\n Searched: " + strings.Join(searched, ", ") + } + msg += "\n Install Eclipse Temurin JDK 21 (what Mendix Studio Pro itself uses), or set JAVA_HOME to one." + return "", fmt.Errorf("%s", msg) } // resolveMacOSJavaHome uses /usr/libexec/java_home to find a JDK 21 on macOS. @@ -337,8 +349,20 @@ func resolveMacOSJavaHome() (string, error) { } // jdkSearchPaths returns OS-specific glob patterns for JDK installations. -func jdkSearchPaths() []string { - switch runtime.GOOS { +func jdkSearchPaths() []string { return jdkSearchPathsFor(runtime.GOOS) } + +// jdkSearchPathsFor is jdkSearchPaths with the OS injected, so the Windows list +// is assertable from a Linux runner — nothing in CI executes Windows code, which +// is how the java.exe suffix stayed broken. +// +// Studio Pro does not ship a JDK of its own to point at: Mendix's install guide +// lists "Eclipse Temurin JDK 21 (x64 or ARM64)" as the prerequisite and installs +// it if absent, so the Temurin location below IS Studio Pro's JDK. (Its Gradle +// 8.5 does live with Studio Pro — "extracted to the parent directory of the +// folder where Studio Pro is installed (usually C:\Program Files\Mendix)" — +// but mxbuild invokes that itself; mxcli never calls gradle.) +func jdkSearchPathsFor(goos string) []string { + switch goos { case "windows": var paths []string for _, dir := range windowsProgramDirs() { @@ -348,6 +372,14 @@ func jdkSearchPaths() []string { filepath.Join(dir, "Microsoft", "jdk-21*"), ) } + // Per-user installs (winget and the Temurin MSI both offer one) land + // outside Program Files, where nothing above would find them. + if local := os.Getenv("LOCALAPPDATA"); local != "" { + paths = append(paths, + filepath.Join(local, "Programs", "Eclipse Adoptium", "jdk-21*"), + filepath.Join(local, "Programs", "Microsoft", "jdk-21*"), + ) + } return paths case "darwin": return []string{ @@ -364,10 +396,7 @@ func jdkSearchPaths() []string { // isJDK21 checks if the given JAVA_HOME points to a JDK 21 installation. func isJDK21(javaHome string) bool { - javaBin := filepath.Join(javaHome, "bin", "java") - if runtime.GOOS == "windows" { - javaBin += ".exe" - } + javaBin := JavaExePath(javaHome) if _, err := os.Stat(javaBin); err != nil { return false } @@ -384,7 +413,7 @@ var jdk21VersionRegex = regexp.MustCompile(`version "21[\.\s"]`) // javaVersionString runs java -version and returns the output for diagnostics. func javaVersionString(javaHome string) string { - javaBin := filepath.Join(javaHome, "bin", "java") + javaBin := JavaExePath(javaHome) out, err := exec.Command(javaBin, "-version").CombinedOutput() if err != nil { return "(unknown)" diff --git a/cmd/mxcli/docker/javaexe.go b/cmd/mxcli/docker/javaexe.go new file mode 100644 index 000000000..aacc39e3b --- /dev/null +++ b/cmd/mxcli/docker/javaexe.go @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 + +package docker + +import ( + "path/filepath" + "runtime" +) + +// JavaExePath is /bin/java, with the .exe Windows needs. +// +// Detection got this right (isJDK21 appends it) and every consumer got it +// wrong: the path handed to mxbuild as --java-exe-path, and the one +// exec.Command runs to boot the runtime, were both built as a bare +// "…\bin\java". So on Windows a correctly-detected JDK was passed on in a form +// that need not resolve — which reads, from the outside, as "mxcli does not +// detect Java". +// +// One helper rather than five call sites, because the next one added would have +// made the same mistake. +func JavaExePath(javaHome string) string { + exe := "java" + if runtime.GOOS == "windows" { + exe += ".exe" + } + return filepath.Join(javaHome, "bin", exe) +} + +// javaExeName is the java binary's file name for a given OS, so the Windows +// form is assertable from a Linux runner (nothing in CI executes Windows code). +func javaExeName(goos string) string { + if goos == "windows" { + return "java.exe" + } + return "java" +} diff --git a/cmd/mxcli/docker/javaexe_test.go b/cmd/mxcli/docker/javaexe_test.go new file mode 100644 index 000000000..79ca8f918 --- /dev/null +++ b/cmd/mxcli/docker/javaexe_test.go @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 + +package docker + +import ( + "path/filepath" + "runtime" + "strings" + "testing" +) + +// TestJavaExePath_UsesHostSuffix pins the bug this helper exists for: every +// consumer built "/bin/java" without the .exe Windows needs, while detection +// appended it — so a correctly-detected JDK was passed to mxbuild, and exec'd for +// the runtime, in a form that need not resolve. +func TestJavaExePath_UsesHostSuffix(t *testing.T) { + got := JavaExePath(filepath.Join("C:", "jdk-21")) + want := "java" + if runtime.GOOS == "windows" { + want = "java.exe" + } + if filepath.Base(got) != want { + t.Errorf("JavaExePath = %q, want it to end in %q", got, want) + } +} + +// TestJavaExeName covers the Windows form from any host. The suffix cannot be +// exercised on a Linux runner otherwise, which is why it stayed wrong. +func TestJavaExeName(t *testing.T) { + if got := javaExeName("windows"); got != "java.exe" { + t.Errorf("javaExeName(windows) = %q, want java.exe", got) + } + for _, goos := range []string{"linux", "darwin"} { + if got := javaExeName(goos); got != "java" { + t.Errorf("javaExeName(%s) = %q, want java", goos, got) + } + } +} + +// TestJdkSearchPathsFor_Windows asserts the list a Windows host searches, +// including the per-user install locations that Program Files globs miss. +// Studio Pro contributes no path of its own: it installs Eclipse Temurin, which +// the Adoptium entries already cover. +func TestJdkSearchPathsFor_Windows(t *testing.T) { + t.Setenv("PROGRAMFILES", `C:\Program Files`) + t.Setenv("LOCALAPPDATA", `C:\Users\dev\AppData\Local`) + + paths := jdkSearchPathsFor("windows") + if len(paths) == 0 { + t.Fatal("no JDK search paths for windows") + } + joined := strings.Join(paths, "|") + // Separator-agnostic: filepath.Join uses "/" on the Linux runner this test + // normally executes on. + for _, want := range []string{"Eclipse Adoptium", "Microsoft", "AppData", "Programs"} { + if !strings.Contains(joined, want) { + t.Errorf("windows search paths should include %q, got:\n%s", want, strings.Join(paths, "\n")) + } + } + for _, p := range paths { + if !strings.Contains(p, "jdk-21") { + t.Errorf("every pattern should pin JDK 21, got %q", p) + } + } +} + +func TestJdkSearchPathsFor_UnixHosts(t *testing.T) { + for _, goos := range []string{"linux", "darwin"} { + paths := jdkSearchPathsFor(goos) + if len(paths) == 0 { + t.Fatalf("no JDK search paths for %s", goos) + } + for _, p := range paths { + if !strings.Contains(p, "21") { + t.Errorf("%s: every pattern should pin JDK 21, got %q", goos, p) + } + } + } +} diff --git a/cmd/mxcli/docker/localboot.go b/cmd/mxcli/docker/localboot.go index 85d922b54..f01aa816a 100644 --- a/cmd/mxcli/docker/localboot.go +++ b/cmd/mxcli/docker/localboot.go @@ -459,7 +459,7 @@ func StartLocalRuntime(opts LocalRuntimeOptions) (*LocalRuntime, error) { // configuration up to but not including start. It is used both for the initial // boot and for a restart (config is per-process and must be re-applied). func (rt *LocalRuntime) spawnAndConfigure() error { - javaExe := filepath.Join(rt.opts.JavaHome, "bin", "java") + javaExe := JavaExePath(rt.opts.JavaHome) cmd := exec.Command(javaExe, rt.opts.jvmArgs()...) cmd.Dir = rt.opts.runtimeDir() cmd.Env = localRuntimeEnv(rt.opts) diff --git a/cmd/mxcli/docker/mxbuild_platform.go b/cmd/mxcli/docker/mxbuild_platform.go new file mode 100644 index 000000000..80bc14768 --- /dev/null +++ b/cmd/mxcli/docker/mxbuild_platform.go @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 + +package docker + +import ( + "fmt" + "io" + "os" + "runtime" +) + +// The Mendix CDN publishes Linux mxbuild archives only — MxBuildCDNURL branches +// on GOARCH, never GOOS, so on an arm64 Mac it fetches a Linux *aarch64* ELF. +// The architecture matches, which is why nothing notices until exec: +// +// fork/exec ~/.mxcli/mxbuild/11.12.0/modeler/mxbuild: exec format error +// +// `setup mxbuild` has always known this (NativeMxBuildForSetup) and `docker +// build` resolves Studio Pro before the cache (resolveMxBuild). The local loop +// did neither: it called DownloadMxBuild directly, so on macOS and Windows it +// executed whatever Linux binary the cache held. (issue #916) + +// ResolveMxBuildForLocal picks the mxbuild that `run --local` / `test --local` +// can actually execute on this host, and reports why when none can. +// +// Order, and why: +// +// 1. An explicit --mxbuild-path wins. It was documented as an override and was +// silently ignored by the local path, so a user hitting the platform +// mismatch had no way out. +// 2. On a non-Linux host, Studio Pro BEFORE the cache. The cache may legitimately +// hold a Linux binary — Windows keeps one for Docker builds — and on macOS it +// is exactly what a previous `run --local` downloaded. Preferring it is the bug. +// 3. Otherwise (Linux) the cache or a CDN download, unchanged. +// +// Whatever is chosen is checked for executability on this host before it is +// handed back, so a stale or hand-placed foreign binary fails with an +// explanation rather than a raw exec error. +func ResolveMxBuildForLocal(explicitPath, version string, w io.Writer) (string, error) { + return resolveMxBuildForLocalOn(runtime.GOOS, explicitPath, version, w) +} + +// resolveMxBuildForLocalOn is ResolveMxBuildForLocal with the host OS injected, +// so the macOS and Windows branches are testable from any host — the platform +// mismatch this fixes cannot otherwise be exercised in CI. +func resolveMxBuildForLocalOn(goos, explicitPath, version string, w io.Writer) (string, error) { + if explicitPath != "" { + resolved, err := resolveMxBuild(explicitPath, version) + if err != nil { + return "", err + } + if err := verifyRunsOn(resolved, goos); err != nil { + return "", err + } + return resolved, nil + } + + if native, guidance, err := NativeMxBuildForSetup(goos, version); err != nil { + // Non-Linux host with no Studio Pro: downloading would cache something + // that cannot run. Say so, with the same guidance `setup mxbuild` gives. + return "", fmt.Errorf("%w\n %s", err, guidance) + } else if native != "" { + if err := verifyRunsOn(native, goos); err != nil { + return "", err + } + return native, nil + } + + downloaded, err := DownloadMxBuild(version, w) + if err != nil { + return "", err + } + if err := verifyRunsOn(downloaded, goos); err != nil { + return "", err + } + return downloaded, nil +} + +// binaryOS reports the OS an executable is built for, from its magic bytes, or +// "" when the format is not recognised (a shell-script wrapper, or anything +// else this does not need to be clever about). +func binaryOS(path string) string { + f, err := os.Open(path) + if err != nil { + return "" + } + defer f.Close() + + var magic [4]byte + if n, err := f.Read(magic[:]); err != nil || n < 4 { + return "" + } + switch { + case magic[0] == 0x7F && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F': + return "linux" + case magic[0] == 'M' && magic[1] == 'Z': + return "windows" + } + // Mach-O, thin (feedface/feedfacf) or fat (cafebabe), either endianness. + switch be := uint32(magic[0])<<24 | uint32(magic[1])<<16 | uint32(magic[2])<<8 | uint32(magic[3]); be { + case 0xFEEDFACE, 0xFEEDFACF, 0xCEFAEDFE, 0xCFFAEDFE, 0xCAFEBABE, 0xBEBAFECA: + return "darwin" + } + return "" +} + +// verifyRunsHere refuses a binary built for another operating system. +// +// The raw failure is `fork/exec …: exec format error`, which names neither the +// cause nor a remedy — the reporter of #916 had to run `file` on the cached +// binary to find out. An unrecognised format is allowed through: a script +// wrapper has no magic to read, and guessing wrong would block a working setup. +func verifyRunsHere(path string) error { return verifyRunsOn(path, runtime.GOOS) } + +// verifyRunsOn is verifyRunsHere with the host OS injected, so the macOS case +// can be asserted from a Linux CI runner. +func verifyRunsOn(path, goos string) error { + got := binaryOS(path) + if got == "" || got == goos { + return nil + } + msg := fmt.Sprintf("mxbuild at %s is a %s binary and cannot run on %s", path, got, goos) + if got == "linux" && goos != "linux" { + msg += "\n The Mendix CDN only publishes Linux mxbuild, so a cached download cannot run here." + + "\n Install Mendix Studio Pro for this project's Mendix version, or pass --mxbuild-path" + + "\n pointing at its bundled mxbuild." + } else { + msg += "\n Pass --mxbuild-path pointing at an mxbuild built for " + goos + "." + } + return fmt.Errorf("%s", msg) +} diff --git a/cmd/mxcli/docker/mxbuild_platform_test.go b/cmd/mxcli/docker/mxbuild_platform_test.go new file mode 100644 index 000000000..e0bc6dc18 --- /dev/null +++ b/cmd/mxcli/docker/mxbuild_platform_test.go @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache-2.0 + +package docker + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +// writeBinary drops a file whose first bytes are magic, so the format checks can +// be exercised without shipping real executables. +func writeBinary(t *testing.T, dir, name string, magic []byte) string { + t.Helper() + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + p := filepath.Join(dir, name) + if err := os.WriteFile(p, append(magic, []byte("padding-padding")...), 0o755); err != nil { + t.Fatal(err) + } + return p +} + +var ( + elfMagic = []byte{0x7F, 'E', 'L', 'F'} + machoMagic = []byte{0xCF, 0xFA, 0xED, 0xFE} + peMagic = []byte{'M', 'Z', 0x90, 0x00} +) + +func TestBinaryOS(t *testing.T) { + dir := t.TempDir() + cases := []struct { + name string + magic []byte + want string + }{ + {"elf", elfMagic, "linux"}, + {"macho", machoMagic, "darwin"}, + {"macho-fat", []byte{0xCA, 0xFE, 0xBA, 0xBE}, "darwin"}, + {"pe", peMagic, "windows"}, + {"shell wrapper is not classified", []byte("#!/b"), ""}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := binaryOS(writeBinary(t, dir, c.name, c.magic)); got != c.want { + t.Errorf("binaryOS = %q, want %q", got, c.want) + } + }) + } + + t.Run("too short to classify", func(t *testing.T) { + p := filepath.Join(dir, "tiny") + if err := os.WriteFile(p, []byte{0x7F}, 0o755); err != nil { + t.Fatal(err) + } + if got := binaryOS(p); got != "" { + t.Errorf("binaryOS = %q, want empty for a 1-byte file", got) + } + }) +} + +// TestVerifyRunsOn_LinuxBinaryOnMac is the #916 failure itself: the cached CDN +// download is a Linux ELF, and macOS produced only `exec format error`. The +// message has to name the cause and a way out. +func TestVerifyRunsOn_LinuxBinaryOnMac(t *testing.T) { + p := writeBinary(t, t.TempDir(), "mxbuild", elfMagic) + + err := verifyRunsOn(p, "darwin") + if err == nil { + t.Fatal("a Linux binary must be refused on darwin") + } + for _, want := range []string{"linux binary", "cannot run on darwin", "Studio Pro", "--mxbuild-path"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("error should mention %q, got:\n%v", want, err) + } + } +} + +func TestVerifyRunsOn_MatchingAndUnknown(t *testing.T) { + dir := t.TempDir() + if err := verifyRunsOn(writeBinary(t, dir, "native", elfMagic), "linux"); err != nil { + t.Errorf("a matching binary must be accepted: %v", err) + } + // A wrapper script has no magic to read; refusing it would block a working + // setup on a guess. + if err := verifyRunsOn(writeBinary(t, dir, "wrapper", []byte("#!/b")), "darwin"); err != nil { + t.Errorf("an unclassifiable file must be allowed through: %v", err) + } +} + +// TestResolveMxBuildForLocal_ExplicitPathWins covers the half that left the +// reporter with no workaround: --mxbuild-path was documented as an override and +// the local path ignored it, calling DownloadMxBuild unconditionally. +func TestResolveMxBuildForLocal_ExplicitPathWins(t *testing.T) { + dir := t.TempDir() + explicit := writeBinary(t, dir, "mxbuild", elfMagic) + + got, err := resolveMxBuildForLocalOn("linux", explicit, "11.12.0", &bytes.Buffer{}) + if err != nil { + t.Fatalf("explicit path rejected: %v", err) + } + if got != explicit { + t.Errorf("resolved %q, want the explicit path %q", got, explicit) + } +} + +// TestResolveMxBuildForLocal_ExplicitPathMustRunHere — an override pointing at a +// foreign binary is refused rather than exec'd. +func TestResolveMxBuildForLocal_ExplicitPathMustRunHere(t *testing.T) { + explicit := writeBinary(t, t.TempDir(), "mxbuild", machoMagic) + + if _, err := resolveMxBuildForLocalOn("linux", explicit, "11.12.0", &bytes.Buffer{}); err == nil { + t.Fatal("a darwin binary passed via --mxbuild-path must be refused on linux") + } +} + +// TestResolveMxBuildForLocal_NonLinuxWithoutStudioProRefuses pins the behaviour +// that could not be reached from CI before: on a host the CDN has no build for, +// resolution must fail with guidance instead of downloading a Linux binary and +// exec'ing it. No network is touched — NativeMxBuildForSetup returns the error +// before any download is attempted. +func TestResolveMxBuildForLocal_NonLinuxWithoutStudioProRefuses(t *testing.T) { + // A version no Studio Pro install will match, so the darwin branch reaches + // its "no native mxbuild" outcome on any machine. + var out bytes.Buffer + _, err := resolveMxBuildForLocalOn("darwin", "", "99.99.99", &out) + if err == nil { + t.Fatal("darwin without Studio Pro must refuse, not download a Linux binary") + } + for _, want := range []string{"Linux binary", "darwin"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("error should mention %q, got:\n%v", want, err) + } + } + if !strings.Contains(err.Error(), "--mxbuild-path") && !strings.Contains(err.Error(), "Studio Pro") { + t.Errorf("error should offer a way forward, got:\n%v", err) + } + if out.Len() > 0 { + t.Errorf("nothing should have been downloaded, but progress was written:\n%s", out.String()) + } +} diff --git a/cmd/mxcli/docker/mxserve.go b/cmd/mxcli/docker/mxserve.go index af78aeabb..191e3098a 100644 --- a/cmd/mxcli/docker/mxserve.go +++ b/cmd/mxcli/docker/mxserve.go @@ -112,6 +112,12 @@ func StartServe(opts ServeOptions) (*ServeServer, error) { if err := verifyMxBuildCache(mxbuildPath); err != nil { return nil, err } + // The cache can hold a binary for another OS — the CDN only ships Linux, and + // Windows keeps one deliberately for Docker builds. Exec'ing it produces a + // bare "exec format error" naming neither cause nor remedy (#916). + if err := verifyRunsHere(mxbuildPath); err != nil { + return nil, err + } javaHome := opts.JavaHome if javaHome == "" { @@ -121,7 +127,7 @@ func StartServe(opts ServeOptions) (*ServeServer, error) { } javaHome = jh } - javaExe := filepath.Join(javaHome, "bin", "java") + javaExe := JavaExePath(javaHome) host := opts.Host if host == "" { diff --git a/cmd/mxcli/docker/runlocal.go b/cmd/mxcli/docker/runlocal.go index 63dfd8f84..cff1e44bc 100644 --- a/cmd/mxcli/docker/runlocal.go +++ b/cmd/mxcli/docker/runlocal.go @@ -575,7 +575,10 @@ func RunLocal(opts LocalRunOptions) error { // 2. Ensure mxbuild + runtime are cached, and linked for the serve javac step. fmt.Fprintln(w, "Ensuring MxBuild and runtime are available...") - mxbuildPath, err := DownloadMxBuild(version, w) + // Resolve what this host can actually execute: Studio Pro before the cache + // on macOS/Windows, and honour --mxbuild-path, which the local path used to + // ignore (#916). + mxbuildPath, err := ResolveMxBuildForLocal(opts.MxBuildPath, version, w) if err != nil { return fmt.Errorf("setting up mxbuild: %w", err) } diff --git a/cmd/mxcli/docker/settle.go b/cmd/mxcli/docker/settle.go index 60331528c..ec75ed096 100644 --- a/cmd/mxcli/docker/settle.go +++ b/cmd/mxcli/docker/settle.go @@ -42,7 +42,7 @@ func SettleGeneratedSources(projectPath, mxPath, version string, w io.Writer) er cmd := exec.Command(mxbuildPath, "--target=deploy", fmt.Sprintf("--java-home=%s", javaHome), - fmt.Sprintf("--java-exe-path=%s", filepath.Join(javaHome, "bin", "java")), + fmt.Sprintf("--java-exe-path=%s", JavaExePath(javaHome)), projectPath, ) cmd.Dir = filepath.Dir(projectPath) diff --git a/cmd/mxcli/syntax/features_integration.go b/cmd/mxcli/syntax/features_integration.go index 12783c706..b4583a8ff 100644 --- a/cmd/mxcli/syntax/features_integration.go +++ b/cmd/mxcli/syntax/features_integration.go @@ -194,7 +194,45 @@ func init() { }, Syntax: "SHOW REST CLIENTS [IN Module];\nSHOW PUBLISHED REST SERVICES [IN Module];\nDESCRIBE REST CLIENT Module.Name;\nDESCRIBE PUBLISHED REST SERVICE Module.Name;", Example: "SHOW REST CLIENTS;\nDESCRIBE REST CLIENT MyModule.PetStoreAPI;\nSHOW PUBLISHED REST SERVICES IN MyModule;", - SeeAlso: []string{"rest.consumed", "rest.published", "integration"}, + SeeAlso: []string{"rest.call", "rest.consumed", "rest.published", "integration"}, + }) + + Register(SyntaxFeature{ + Path: "rest.call", + Summary: "REST CALL activity inside a microflow, and its five RETURNS forms", + Keywords: []string{ + "rest call", "call rest service", "http get", "http post", + "returns response", "returns string", "returns mapping", + "file document", "filedocument", "download", "httpresponse", + "body binary", "binary", "upload", "post binary", + }, + Syntax: "[$Var =] REST CALL GET|POST|PUT|PATCH|DELETE '' [WITH ({1} = expr, ...)]\n" + + " [HEADER 'Name' = expr]\n" + + " [AUTH BASIC $user PASSWORD $pass]\n" + + " [BODY '