diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 4e021ad4e..377422b41 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/setup-go@v7 with: - go-version: '1.26.5' + go-version: '1.26.6' - name: Cache ANTLR4 JAR uses: actions/cache@v6 @@ -58,7 +58,7 @@ jobs: - uses: actions/setup-go@v7 with: - go-version: '1.26.5' + go-version: '1.26.6' - uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/push-test.yml b/.github/workflows/push-test.yml index 455d4cdf1..e8b377a50 100644 --- a/.github/workflows/push-test.yml +++ b/.github/workflows/push-test.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-go@v7 with: - go-version: '1.26.5' + go-version: '1.26.6' - name: Cache ANTLR4 JAR uses: actions/cache@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78df8e1d9..d430adb50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-go@v7 with: - go-version: '1.26.5' + go-version: '1.26.6' - uses: oven-sh/setup-bun@v2 - name: Cache ANTLR4 JAR uses: actions/cache@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c9e2c6a..dde975e12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Changed + +- **Go toolchain 1.26.5 → 1.26.6** for GO-2026-6218 (`net/url`), GO-2026-6090 (`crypto/tls`), GO-2026-6089 (`net/http`), GO-2026-6088 (`encoding/xml`), GO-2026-5972 (`encoding/asn1`) and GO-2026-5026 (`net/http`, via `golang.org/x/net/idna`). All six are standard-library advisories fixed in go1.26.6; no mxcli code changed. Bumped in `go.mod` and in all three workflows (`push-test`, `release`, `nightly`) together, so released binaries are not still linked against the vulnerable standard library. + ## [0.17.0] - 2026-08-10 Headline: **A full Mendix build-and-test loop that fits on an iPad** — you can now design, build, run, observe, and debug a multi-app Mendix solution end-to-end from Claude Code on the web, on a phone or tablet, with no local IDE. Two capabilities make it possible: an **external browser preview** that reverse-tunnels a locally-running app out to a public URL from an egress-only container, and a **short agentic feedback loop** — a warm Docker-free runtime, sub-second microflow unit tests, live log/metric/trace observation, and a name-based microflow debugger — so an agent gets an answer in seconds instead of a build round-trip. diff --git a/cmd/mxcli/lsp_completions_gen.go b/cmd/mxcli/lsp_completions_gen.go index bdbf53f4b..51df6704e 100644 --- a/cmd/mxcli/lsp_completions_gen.go +++ b/cmd/mxcli/lsp_completions_gen.go @@ -86,6 +86,7 @@ var mdlGeneratedKeywords = []protocol.CompletionItem{ {Label: "FROM", Kind: protocol.CompletionItemKindKeyword, Detail: "Query keyword"}, {Label: "WHERE", Kind: protocol.CompletionItemKindKeyword, Detail: "Query keyword"}, {Label: "HAVING", Kind: protocol.CompletionItemKindKeyword, Detail: "Query keyword"}, + {Label: "FIRST", Kind: protocol.CompletionItemKindKeyword, Detail: "Query keyword"}, {Label: "OFFSET", Kind: protocol.CompletionItemKindKeyword, Detail: "Query keyword"}, {Label: "LIMIT", Kind: protocol.CompletionItemKindKeyword, Detail: "Query keyword"}, {Label: "AS", Kind: protocol.CompletionItemKindKeyword, Detail: "Query keyword"}, diff --git a/docs/11-proposals/PROPOSAL_container_autosize.md b/docs/11-proposals/PROPOSAL_container_autosize.md new file mode 100644 index 000000000..a5e4f06c2 --- /dev/null +++ b/docs/11-proposals/PROPOSAL_container_autosize.md @@ -0,0 +1,188 @@ +--- +title: Size containers from their contents, not from a statement count +status: draft +--- + +# Proposal: Size containers from their contents, not from a statement count + +Closes the last open item of upstream [#884](https://github.com/mendixlabs/mxcli/issues/884) — +problem 1, "container `Size` is unrelated to the positions of the activities inside it". + +## Problem + +A `LoopedActivity` is the only microflow element mxcli sizes rather than stamping +with a constant. Its `Size` is computed **before** its body is built, from a +pre-pass over the AST that counts statements. The nested builder that actually +places the children runs afterwards, by which time the box is frozen — so child +positions, including explicit `@position` overrides, have **no** influence on the +box that is supposed to contain them. + +### Measured + +Four microflows, one project, blank Mendix 11.6.6 app, mxcli `bcc3406` +(`mdl-examples/bug-tests/container-autosize-884.mdl`; `LOOP` body varied, +everything else identical): + +| | Body | Child centres (X) | Loop `Size` | +|---|---|---|---| +| **A** | 2 activities, default placement | 150, 310 | `480;160` | +| **B** | same 2, `@position(1500,60)` / `@position(2000,60)` | 1500, 2000 | `480;160` | +| **C** | same 2, `@position(160,60)` / `@position(170,60)` | 160, 170 | `480;160` | +| **D** | 4 activities, default placement | 150 … 630 | `800;160` | + +A, B and C are the same box around contents spanning 160px, 500px and 10px. +D differs from A only in statement count, and only D changes the box. Width is +exactly `(n-1)·HorizontalSpacing + ActivityWidth + 2·LoopPadding + iteratorSpace` +— a function of `n` alone. + +**B is a correctness problem, not a cosmetic one.** The box interior spans +x ∈ [0, 480]; both children sit at 1500 and 2000, entirely outside their own +container. `mx check` reports the *same* error count before and after the script +runs — validation does not model geometry, so nothing catches it. It is visible +only by opening the flow in Studio Pro. + +This is what the reporter observed as "identical child positions, different +`Size`" — the two quantities are simply not connected. + +### Where it comes from + +`measureStatementsSpan` (`mdl/executor/layout.go:92`) takes +`[]ast.MicroflowStatement` and returns `(count-1)*HorizontalSpacing + ActivityWidth`. +It is called at exactly two places, both of which size a container: + +- `mdl/executor/cmd_microflows_builder_control.go:572` — `addLoopStatement` +- `mdl/executor/cmd_microflows_builder_control.go:896` — `addWhileStatement` + +Neither has any child object at that point. `loopBuilder` — the nested +`flowBuilder` whose `objects` carry the real positions — is constructed about +twenty lines later. + +The comment on `measureStatementsSpan` is honest about the half-fix it already +is (#790): the count-based span was introduced to *stop over-sizing* boxes, and +it falls back to the older `measureStatements` for compound bodies precisely +because it "cannot reproduce the builder's geometry without duplicating it". +That is the tell — the pre-pass is trying to predict a computation that has not +run yet. + +## Why this is not a one-line change + +The current ordering contains a cycle that the obvious fix walks straight into: + +```go +loopHeight := max(bodyBounds.Height+2*LoopPadding, MinLoopHeight) // needs body +innerStartY := loopHeight / 2 // body needs this +loopLeftX := fb.posX +loopCenterX := loopLeftX + loopWidth/2 // needs width +``` + +Children are placed relative to an inner origin derived **from the size**, and +under the fix the size must be derived **from the children**. Anything that +computes the size from real positions has to break that dependency first, which +is why this is a layout-engine change rather than a patch. + +## Proposal + +**Build first, size after, translate once.** + +1. **Build the body at a provisional origin** — run `loopBuilder` from `(0, 0)`. + Nothing in the body placement genuinely needs the box; it needs *an* origin. +2. **Take the real bounding box** from `loopBuilder.objects` + (`Position ± Size/2` over every object, recursively for nested containers). + This is ground truth, and it accounts for `@position` for free — no annotation + plumbing required. +3. **Size the box** as bbox + `LoopPadding` on all sides, plus `iteratorSpace` on + the left for `LOOP`, floored at `MinLoopWidth` / `MinLoopHeight`. +4. **Translate the body once** — a single pass over `loopBuilder.objects` adding + the delta between the provisional origin and the final inner origin, mutating + `Position` only. + +Step 4 is what keeps this safe. No builder call site learns about sizing; the +translation is a pure post-pass over objects that already exist. That is the +same single-choke-point shape used for `@curve` (`applyFlowCurves`) and `@merge` +(`mergePosition`) — chosen for the same reason: a rule threaded through N +placement sites fails silently at the one site that was missed. + +### Nested containers + +The bbox must be computed bottom-up: an inner loop has to be sized before the +outer loop measures it. This already falls out of the recursion — `addStatement` +sizes the inner `LoopedActivity` before it is appended to +`loopBuilder.objects` — but it becomes load-bearing under this change and should +be asserted by a test with two nesting levels, not left to hold by accident. + +### Semantics of `@position` inside a container + +Two readings, and they must be decided explicitly rather than emerging from the +implementation: + +- **(a) container-relative, translated with everything else** — the box grows to + contain the annotated child, and the child keeps its position *relative to the + body*. +- **(b) authoritative, exempt from translation** — the annotated child stays put + and unannotated siblings move around it. + +**Recommend (a).** Positions inside a `LoopedActivity` are already stored +relative to the container, so (a) matches the storage model; (b) would leave two +adjacent children obeying different origins, which is unexplainable in a doc and +unpredictable in a diff. Whichever is chosen, it belongs in +`.claude/skills/mendix/` alongside the `@position` reference. + +## Non-goals + +- **Splits.** `IF` / enum split / inheritance split have no `Size` property in + Mendix storage — there is no box, so there is nothing to fix. `measureStatements` + stays: it is still needed to reserve horizontal room for branches *before* they + are laid out, which is a genuinely predictive use. +- **Re-flowing a body to fit a box.** Out of scope. The box follows the contents, + never the reverse. + +## Alternative considered: an `@size(w, h)` escape hatch + +The reporter asked for this directly, and it is far cheaper — one annotation, one +field, no layout change. It is rejected as the *primary* fix for two reasons: it +makes the author responsible for a number the engine can compute, and it leaves +the default behaviour wrong for everyone who does not use it. + +Worth revisiting **after** the derived size lands, if a real case wants a box +deliberately larger than its contents. Note the coupling introduced by the #884 +annotation work: `@size` is currently **rejected** by MDL059, so adding it means +adding it to `knownActivityAnnotations` in `mdl/executor/validate_microflow.go`. +The drift test compares the visitor's case labels against that list in both +directions and fails if only one side changes — which is the intent. + +## Risk + +**Every generated flow containing a `LOOP` or `WHILE` changes geometry.** That is +unavoidable for a fix of this shape, and has two consequences worth stating in +the release note: + +- Fixtures and golden BSON comparisons churn once, in a single commit. +- Under idempotent writes (ADR-0008), the first re-run of any existing script + against an existing project **writes** the affected microflows; subsequent runs + go quiet again. Users will see one round of version-control changes they did + not author. + +Neither is a reason not to do it, but a silent geometry shift across an existing +project is exactly the kind of thing that gets reported as a new bug. + +## Verification + +- **Regression test from the table above.** Cases A–D as a builder-level test + asserting the invariant directly: *every child's bounding box lies inside its + parent's*. That is the property; the specific pixel values are not. +- **Nesting test** — loop inside a loop, both boxes containing their contents. +- **`@position` test** — case B: the box must grow to contain a child pushed to + x=2000, not leave it outside. +- **Control run.** Per the standing rule, the test must be shown to fail against + a pre-fix binary with the reported symptom — a same-size box around different + contents — and not merely pass against fixed code. +- **`mx check`** on every fixture, both engines. + +### Candidate lint rule: MPR009 + +The measurement above shows `mx check` is blind to this. A rule +*"every child of a `LoopedActivity` lies within its parent's box"* would have +caught the entire class from the outside, and would keep catching it if a future +layout change reintroduces it. It sits naturally beside MPR008 (which, after the +#884 work, already partitions objects by canvas and therefore has the container +geometry in hand). Proposed as a follow-up, not part of this change. diff --git a/go.mod b/go.mod index ab948e40c..5058003a9 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/mendixlabs/mxcli go 1.26.0 -toolchain go1.26.5 +toolchain go1.26.6 require ( github.com/alecthomas/chroma/v2 v2.26.1 diff --git a/mdl-examples/bug-tests/container-autosize-884.mdl b/mdl-examples/bug-tests/container-autosize-884.mdl new file mode 100644 index 000000000..66201f237 --- /dev/null +++ b/mdl-examples/bug-tests/container-autosize-884.mdl @@ -0,0 +1,59 @@ +-- upstream #884 problem 1: a LoopedActivity's Size is a function of statement +-- count alone -- child positions, including explicit @position, have no effect. +-- +-- Measured on Mendix 11.6.6 (mxcli bcc3406): A, B and C all produce Size +-- 480;160 while their contents span 160px, 500px and 10px respectively. In B +-- both children sit outside the box (interior x in [0,480], children at 1500 +-- and 2000) and `mx check` reports no additional error. +-- +-- See docs/11-proposals/PROPOSAL_container_autosize.md + +CREATE ENTITY MyFirstModule.SzItem ( + Name: String(100) +); + +-- A: default child placement +CREATE MICROFLOW MyFirstModule.MF_SizeA () +BEGIN + RETRIEVE $Items FROM MyFirstModule.SzItem; + LOOP $Item IN $Items BEGIN + LOG INFO 'one'; + LOG INFO 'two'; + END LOOP; +END; + +-- B: identical body, children pushed far outside the box +CREATE MICROFLOW MyFirstModule.MF_SizeB () +BEGIN + RETRIEVE $Items FROM MyFirstModule.SzItem; + LOOP $Item IN $Items BEGIN + @position(1500, 60) + LOG INFO 'one'; + @position(2000, 60) + LOG INFO 'two'; + END LOOP; +END; + +-- C: identical body, children pulled tightly together +CREATE MICROFLOW MyFirstModule.MF_SizeC () +BEGIN + RETRIEVE $Items FROM MyFirstModule.SzItem; + LOOP $Item IN $Items BEGIN + @position(160, 60) + LOG INFO 'one'; + @position(170, 60) + LOG INFO 'two'; + END LOOP; +END; + +-- D: control -- only the statement count differs from A, and only D resizes +CREATE MICROFLOW MyFirstModule.MF_SizeD () +BEGIN + RETRIEVE $Items FROM MyFirstModule.SzItem; + LOOP $Item IN $Items BEGIN + LOG INFO 'one'; + LOG INFO 'two'; + LOG INFO 'three'; + LOG INFO 'four'; + END LOOP; +END;