From cd2b01e87f7f4fb464991d5c19e05e260180f3e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 21:43:32 +0000 Subject: [PATCH 1/3] docs: propose deriving container Size from contents (#884 problem 1) A LoopedActivity's Size is computed from a pre-pass over the AST before its body is built, so it is a function of statement count alone and the children's real positions -- @position included -- have no effect on the box meant to contain them. Measured on 11.6.6: two activities at x=150/310, at x=1500/2000, and at x=160/170 all yield Size 480;160; only changing the statement count to four moves it (800;160). In the x=1500/2000 case both children sit entirely outside their own container and mx check reports no additional error, so nothing catches it short of opening the flow in Studio Pro. The fix is not a one-liner: children are placed relative to an inner origin derived from the size, so deriving the size from the children closes a cycle. Proposes build-first / size-after / translate-once, with the translation as a single post-pass over the nested builder's objects -- the same single-choke-point shape used for @curve and @merge. Includes the four-case repro as a bug-test example, and notes the two consequences worth a release note: every flow containing a loop changes geometry once, and under ADR-0008 that means one round of writes users did not author. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8 --- .../PROPOSAL_container_autosize.md | 188 ++++++++++++++++++ .../bug-tests/container-autosize-884.mdl | 59 ++++++ 2 files changed, 247 insertions(+) create mode 100644 docs/11-proposals/PROPOSAL_container_autosize.md create mode 100644 mdl-examples/bug-tests/container-autosize-884.mdl 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/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; From 64935d48b2d682fc26c4b8e46dac4991389be2fb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 07:11:09 +0000 Subject: [PATCH 2/3] chore(lsp): regenerate completions for the FIRST keyword The import-mapping Range work (#881) added FIRST to the lexer but the committed generated completion list was never refreshed, so `make build` left the tree dirty on a clean checkout. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8 --- cmd/mxcli/lsp_completions_gen.go | 1 + 1 file changed, 1 insertion(+) 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"}, From cad70ccafda9828e5cd027c11fa8d86f1f2b509b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 11:23:33 +0000 Subject: [PATCH 3/3] build: Go toolchain 1.26.5 -> 1.26.6 for six stdlib advisories The `Vulnerability scan` step (govulncheck ./...) started failing with exit 3 on every push. Nothing in mxcli changed: all six findings are standard-library advisories reported as `Found in: @go1.26.5` / `Fixed in: @go1.26.6`, and go1.26.6 was published between the last green run and the first red one. govulncheck@latest re-resolves the vuln database on each run, so the pinned toolchain went stale underneath a workflow that had not been touched. GO-2026-6218 net/url quadratic complexity in resolvePath GO-2026-6090 crypto/tls unbounded post-handshake messages GO-2026-6089 net/http ReadHeaderTimeout on the h2c check GO-2026-6088 encoding/xml missing recursion depth guard GO-2026-5972 encoding/asn1 missing recursion depth limit GO-2026-5026 net/http idna punycode label rejection Bumps every pin together -- go.mod's toolchain plus push-test, release and nightly (two jobs) -- so a release binary is not still linked against the vulnerable standard library after CI goes green. Same treatment as the 1.26.4 -> 1.26.5 bump for GO-2026-5856. Verified by running the scan under both toolchains: go1.26.5 reports the six above and exits 3, go1.26.6 reports "No vulnerabilities found" and exits 0. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8 --- .github/workflows/nightly.yml | 4 ++-- .github/workflows/push-test.yml | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 4 ++++ go.mod | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) 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/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