Sync from ako/mxcli: microflow canvas layout authoring (#884), marketplace cache, dev container fix - #886
Merged
Conversation
Two of the four problems in upstream mendixlabs#884. ## An unknown annotation was silently dropped The grammar accepts any @name and extractMicroflowAnnotations' switch had no default arm, so an unrecognised annotation parsed and did nothing. The reporter found this as "@SiZe(600, 300) parses without error but produces no effect". The sharper case is a TYPO of an annotation that does work: @postion(999, 111) -- check passed; the position was silently discarded On a workflow whose entire point is scripted canvas layout, that throws away the thing being authored and reports success. Both forms are now MDL059, an error rather than a warning, and enforced by exec as well as check. Annotations are read generically rather than through a type switch: fifteen statement types carry them, and a switch that skips the sixteenth reintroduces the silent drop one level up. TestEveryActivityAnnotationsFieldIsNamedAnnotations parses the AST package's own source for every *ActivityAnnotations field and asserts the accessor reaches it, and TestKnownAnnotationsMatchTheVisitor parses the visitor's case labels and compares both directions, so the restated list cannot drift from what the visitor implements. Checked against the real corpus for false positives: 405 MDL blocks across .claude/skills/mendix and docs-site/src all still pass. ## MPR008 compared nodes on different canvases The rule recursed into a LoopedActivity's ObjectCollection and appended the children into the same flat list as the microflow's own canvas, then compared every pair. A LoopedActivity's children are positioned RELATIVE to the container — verified in the stored BSON, an outer activity at 200;230 beside a loop child at 141;130 — so it reported overlaps that cannot happen on screen. A false positive is worse than silence for a rule whose job is to be trusted about positions. overlapPlanes now returns one plane per canvas and the pair loop iterates within each. The container itself stays on its parent's plane; only its children move. Extracting it also gives the rule its first real coverage: its test file carried a note that the logic "cannot be unit-tested without building a mock mpr.Reader", which is why this had none. Verified both directions against a real project with a pre-fix binary: the cross-canvas pair is reported by the old binary and not the new one, while a genuine same-canvas pair at (300,200)/(310,210) is still reported by both. The reporter's own quoted pair does not reproduce — (141,130) vs (134,230) is dy=100 against activityBoxHeight=60, so it never fired; the mechanism is real but that evidence was not, and a constructed pair was needed. Refs upstream mendixlabs#884. The remaining two problems (container Size from child positions, and bezier control vectors surviving a rewrite) are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
A cache entry was 34 MB because it stored whatever `mx create-project` produced — a whole blank Mendix app. Most of it is never read. Measured on Administration at 11.12.1: PackageRef.mpr 14 MB read widgets/ 9.6 MB never read themesource/ 6.4 MB never read theme-cache/ 2.1 MB never read (compiled CSS) javascriptsource/ 1.6 MB never read Both consumers take the .mpr and nothing beside it: SnapshotModule opens it, and PerformUpdate takes the reference's model from it while taking the module's bundled widgets from the .mpk — deliberately, because the reference project's widgets/ also holds the blank template's copies. Entries are now model-only (.mpr plus mprcontents/, kept in case a reference ever stays MPR v2). 34 MB -> 14 MB, and warm runs got faster because there is less to copy: the same diff went 13s -> 9s, with the same answer, 21 of 21 elements unchanged and 6 an upgrade would touch. The bound rises 6 -> 12 on the back of it. Twelve is what a six-module update sweep builds, base and target each, so a whole sweep now stays cached at ~170 MB instead of half of it being evicted. This is a constraint on future changes, and isModelFile says so: a cached reference is model-only, so anything that starts reading a sibling directory of the reference .mpr will see it on a cache miss and not on a hit — findings that come and go. The blank-project cache deliberately keeps its whole tree, since `mx module-import` reads all of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`mxcli run --local` needs a real database, and `--ensure-db` provisions one in-container by starting the local PostgreSQL service and creating the app role + database through a `sudo -u postgres` superuser. The generated Dockerfile installed `postgresql-client` only, so on a freshly built `mxcli init` dev container there was no service to start and no superuser — `--ensure-db` failed even though `psql` was on PATH, which made the container look correctly provisioned. Add the `postgresql` server package alongside the client, and assert it in `TestGenerateDockerfile_PostgresServer` for both the docker and podman variants. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKdcQRPJZxTWq87SH34WYy
Two existing proposals assume a repo where several .mpr projects live one level deep under a single workspace — warm-loop slice 5 (`run --solution`) and the multi-project tree view. Nothing creates that repo: `mxcli init` discovers only the first .mpr in a directory and writes a dev container per project, so initialising two projects yields two competing containers and VS Code attaches to one at a time. Propose `mxcli init --solution`: one root dev container whose forwardPorts follow slice 5's port-triple rule, a root agent context indexing the projects, per-project init without a nested dev container, a SessionStart hook covering every project, and a `mxcli.solution.yaml` skeleton for slice 5 to consume. Scoped deliberately to the repo-shape prerequisite. The orchestration half (manifest schema, `run --solution`, sibling-URL wiring) stays in slice 5 and is listed as a non-goal rather than duplicated. Records two findings for slice 5: `--constant` is now the primitive its constant wiring needs, and app-to-app links must use loopback, since an owner-gated hub answers a cookie-less OData call with a login page. Also documents a bug found while writing this up: the SessionStart hook marker is a substring match, so `init` in a second project sees the first project's hook, reports "already present", and silently never bootstraps the second app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKdcQRPJZxTWq87SH34WYy
…analysis analyze-runtime.md already covers joining logs, metrics, traces, and the catalog. What it cannot supply is the correlation key: which spans belong to the thing the user just did in the browser. That key exists only in the user's head and reaches the agent as prose, so the agent guesses at a time window and greps. Propose a panel injected into the app under development that captures the interaction — widget, page, time window, XHR, action — plus an in-process OTLP collector so spans are queryable without an external one, plus an mxcli MCP server exposing both to the agent. Deliberately avoids the unverified push API. Sending a prompt into a running Claude Code session needs a "post to session X" surface that is not exposed to an agent today and is outside mxcli's control, so v1 inverts the flow: the panel records, the agent pulls, and answers land in the Claude Code conversation. In-panel replies stay a purely additive follow-up. Two design points worth the space. Correlation via inbound W3C traceparent would be exact, but is unverified — outbound propagation is known to work, which is suggestive, not proof — so a window-based fallback ships first and a spike gates the precise path. And the panel must be gated on the viewer matching Backend.Owner rather than on preview reachability: external testers require --require-auth=false, which would otherwise expose the panel to everyone with the URL. Rejects shipping the panel as a JavaScript action: theme apply is deliberately model-free so it cannot break a build, and a panel written into the model would ship into production artifacts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKdcQRPJZxTWq87SH34WYy
The third problem in upstream mendixlabs#884, and the one that loses data. Mendix stores NO waypoints. A sequence flow's shape is two bezier control vectors on its Microflows$BezierCurve line — the tangent handles at each end — so a hand-curved edge is a pair of (x, y) offsets, not a polyline. The request asked for waypoints; that shape does not exist, and @Curve maps to what does. Both writers already emitted the vectors and the legacy parser already read them, but nothing could set them, so they defaulted to "0;0" and every rebuild flattened the curve. Measured before the fix by patching "40;-90" into the stored line and re-running the same script unchanged: it came back "0;0". @Curve(from: (40, -90), to: (-40, 90)) Needed no grammar change — `name: (x, y)` is already annotationParenValue, the same shape the association anchors use (mendixlabs#872). Either end may be omitted, which leaves that end straight; "0;0" is straight and DESCRIBE omits it, so an untouched flow describes exactly as it did before. A malformed coordinate is MDL060 rather than a silently straightened edge. The curve is recorded against the ACTIVITY and stamped onto its outgoing flows in a single pass once the graph is complete. Threading it alongside the anchor would have meant editing all seven sites that create a flow, and missing one silently straightens that edge; applyPendingAnnotations already runs at every activity, so there is one place to get right. DESCRIBE now emits @Curve, which is what makes the round trip real: the reporter's "DESCRIBE output is identical before and after manual adjustment" was the read half of the same bug. The modelsdk engine also reads the Line back, which it never did. Verified on mxbuild 11.6.6, both engines: 0 errors, the authored curve on exactly the annotated activity's outgoing flow and "0;0" everywhere else, and re-executing DESCRIBE's own output preserving both vectors. Both call sites are covered by tests that fail when unwired — deleting them left the direct-call unit tests green, which is exactly the trap of a test that only passes against fixed code. Still open: a curve drawn in Studio Pro is preserved only once the script names it, since a rebuilt flow has no stable identity to match on. DESCRIBE now surfaces it so it can be captured. Container Size (mendixlabs#884 problem 1) and merge-node positioning (problem 3) remain untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
…qxghl0 Dev container PostgreSQL server + two proposals for multi-project solutions
Cache the reference model, not the whole reference project
fix(microflow): reject unknown annotations, scope MPR008 per canvas, and author flow curvature (mendixlabs#884)
The last of upstream mendixlabs#884's four problems, and the one I reverted from #143 because it was only half done. The statement's own @position belongs to the SPLIT, so the implicit merge that closes it had no annotation of its own: three split builders computed its position with no override, and it routinely landed on top of a neighbouring activity with no way to move it. @merge(900, 400) All three split builders (addIfStatement, addEnumSplit, addStructuredInheritanceSplit) now read through one mergePosition helper, so the override cannot be honoured at one split type and silently ignored at another. The half that was missing before is DESCRIBE. Without it the layout pass recomputes the merge on the next exec and puts it straight back on its neighbour — the same round-trip data loss as mendixlabs#872, mendixlabs#881 and mendixlabs#882, introduced by the change meant to fix it, which is why the authoring-only version was pulled. Finding the merge to describe is the interesting part. The describer's own split->merge map is not in scope where annotations are emitted, and threading it there would mean editing ten-plus call sites of emitObjectAnnotations — the multi-site change that fails silently when one is missed. commonMergeAfter walks the split's branches instead and takes the nearest merge reachable from ALL of them, using the flowsByOrigin and activityMap already passed in. The walk is bounded per branch and node-capped, because a retry loop makes the flow graph cyclic. Verified on mxbuild 11.6.6, both engines: the merge lands at 900;400, mx check reports 0 errors, and re-executing DESCRIBE's own output keeps it there. Both call sites are covered by tests that fail when unwired — verified by removing each in turn, since calling mergePosition and emitMergeAnnotation directly passes either way. mendixlabs#884 problem 1 (container Size from child positions) remains open: that is a layout-engine change and wants its own proposal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
feat(microflow): position a split's merge node with @merge (mendixlabs#884)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Title
Sync from ako/mxcli: microflow canvas layout authoring (#884), marketplace cache, dev container fix
Description
Sync of
ako/mxcli:main— 7 non-merge commits since #885, 31 files, +2202 / −99.mendixlabs/mainis fully contained in this branch (0 commits ahead), so the diff is additive and conflict-free.Three threads: a feature set closing most of #884, two contained fixes, and two proposals.
1. Microflow canvas layout — upstream #884 (3 commits)
The reporter generates TOF-convention microflows from MDL and controls layout with
@position/@anchor. Three of their four problems are fixed here. Every claim was checked against the code and against Mendix's own storage first; all four stand up, two turned out worse than filed, and one asked for something Mendix does not store.Unknown annotations were silently dropped → MDL059
The grammar accepts any
@nameand the visitor's switch had no default arm. The reporter found this as "@size(600, 300)parses without error but produces no effect". The sharper case is a typo of an annotation that works:On a workflow whose whole point is scripted layout, that throws away the thing being authored and reports success. Now an error, enforced by
execas well ascheck.Annotations are read reflectively: fifteen statement types carry them, and a hand-written type switch that skips the sixteenth reintroduces the silent drop one level up. Two tests parse the source to keep this honest — one asserts every
*ActivityAnnotationsfield is named so the accessor reaches it, the other parses the visitor's own case labels and compares them against the validator's list in both directions. Checked against 407 real MDL blocks for false positives.A hand-curved edge was flattened by any rewrite →
@curveThe request asked for edge waypoints. Mendix stores none: a flow's shape is two bezier control vectors on its
Microflows$BezierCurveline. So the syntax maps to what exists:Both writers already emitted the vectors and the legacy parser already read them, but nothing could set them, so they defaulted to
"0;0"and every rebuild flattened the curve — measured by patching"40;-90"into the stored line and re-running the same script unchanged. Data loss, not a missing feature. Needed no grammar change:name: (x, y)is alreadyannotationParenValue.The merge that closes a split was unpositionable →
@mergeA statement's
@positionbelongs to the split, so its end-if join had no annotation and routinely landed on a neighbouring activity.All three split builders read through one
mergePositionhelper, so the override cannot be honoured at one split type and ignored at another.MPR008 compared nodes on different canvases
The rule flattened a
LoopedActivity's children — positioned relative to the container — into the same list as absolute canvas coordinates, and reported overlaps that cannot happen on screen. Verified in the stored BSON: an outer activity at200;230beside a loop child at141;130. Now one plane per canvas.The design constraint shared by all three
Both new annotations are applied at one choke point. Threading a curve alongside the anchor would mean editing all seven sites that create a flow, and threading the describer's
splitMergeMapto emit@mergewould mean editing ten-plus call sites ofemitObjectAnnotations— the multi-site change that fails silently when one is missed. Instead: the curve is recorded against the activity and stamped in a single pass after the graph is built, andcommonMergeAfterfinds a split's merge by walking its branches using data already in scope. The walk is bounded and node-capped, because a retry loop makes the flow graph cyclic.DESCRIBE emits both, which is what makes them real rather than write-only — the reporter's "DESCRIBE output is identical before and after manual adjustment" was the read half of the same bug.
@mergewas implemented and reverted from the first PR for exactly this reason, and re-landed only once the describe half existed.2. Fixes (2 commits)
widgets/(9.6 MB),themesource/(6.4 MB),theme-cache/(2.1 MB) andjavascriptsource/(1.6 MB) are never read. Both consumers take the.mprand nothing beside it. Entries are now model-only.postgresql-clientonly — somxcli run --local --ensure-dbhad no service to start and no superuser, whilepsqlon PATH made the container look correctly provisioned. Adds the server package, asserted for both the docker and podman variants.3. Proposals (2 commits, docs only)
mxcli init --solution— two existing proposals assume a repo where several.mprprojects sit under one workspace, and nothing creates that shape:initdiscovers only the first.mprand writes a dev container per project, so two projects yield two competing containers. Scoped deliberately to the repo-shape prerequisite, with the orchestration half left in warm-loop slice 5 as a non-goal.analyze-runtime.mdalready covers joining logs, metrics, traces and the catalog; what it cannot supply is which spans belong to the thing the user just did. Proposes a panel that captures the interaction plus an in-process OTLP collector and an MCP surface. Deliberately avoids the unverified push API: the panel records, the agent pulls.Verification
mxbuild 11.6.6, both engines (
modelsdkandlegacy), for the #884 work:mx checkon every fixture@curveon the annotated activity's flow40;-90/-40;90,0;0elsewhere@merge900;400as authored-tags integrationTwo process notes worth carrying upstream. My first
@curvetests called the functions directly; deleting both call sites left them green — they proved the functions worked and nothing about the wiring. Every annotation added here now has a test that fails when unwired, verified by removing each call site in turn. Separately, an intermediate "no MPR008 violations" reading turned out to be the scratch project having been reaped rather than the fix working, so that A/B was redone with a pre-fix binary against a rebuilt project.Scope of my own testing: I authored and verified the #884 thread end-to-end. The marketplace cache, dev container fix and both proposals came from other sessions; they build and pass the suite in this branch, but their own measurements are theirs, not re-run by me.
Still open in #884
Problem 1, container
Size. Diagnosis confirmed:measureStatementsSpanderives width from statement count, never from child positions, which is exactly the reporter's evidence of identical child positions producing differentSize. Deriving it from a child bounding box is a layout-engine change affecting every generated flow's geometry, and wants its own proposal rather than a patch. Note that@size(600, 300)is now rejected rather than ignored, so adding it later means updating the known-annotation list — the drift test fails loudly if only one side changes, which is the intent.