Sync ako/mxcli: workflow end markers, MDL-WF07, MDL080, microflow MERGE/JOIN, non-root PostgreSQL - #1094
Merged
Conversation
TestSettleSourceReturnsPromptlyForOneChange failed CI on PR #449 with "a quiet source took 196.975373ms to settle, want under 100ms" — on a docs-only diff that cannot reach a debounce timer. The same tree passed build-and-test in the push run and failed in the pull_request run minutes apart, which is direct evidence of nondeterminism rather than a regression. The test bounded elapsed wall-clock as a multiple of the poll interval: poll * (sourceSettleWindow + 3), 100ms against a nominal 40ms. settleSource waits on time.After(poll), which guarantees AT LEAST the duration and says nothing about the upper bound, so a loaded runner blows the budget with nothing wrong. It is also the anti-pattern the PR checklist names — tests must not rely on sleeps for synchronisation. What the test is really guarding is a POLL COUNT: a quiet source costs sourceSettleWindow polls, not an unbounded wait. So the poll timer is now injected (settleSourceWith) and the test counts polls. Widening the budget would only have moved the flake threshold. settleSource keeps its signature and is still exercised with a real timer by the multi-file and interrupt tests, so the wrapper is not going untested. The seam also makes a guarantee expressible that the timing test could not reach: the window is sourceSettleWindow CONSECUTIVE quiet polls, so a write part-way through restarts the count. Deleting `quiet = 0` from the change branch was green against every pre-existing test in this file. Controls, each isolating one guarantee: - loop costs one poll more than the window it declares -> both count tests fail - `quiet = 0` removed from the change branch -> only the consecutive test fails - tick returning one shared channel instead of re-arming -> the multi-file test hangs, so the re-arm is load-bearing rather than a style choice - widening sourceSettleWindow itself -> both still pass, deliberately: the assertions track the declared window rather than pinning its value 30 consecutive runs of the four tests are green, and the package passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
`mxcli run --local --ensure-db` could not provision PostgreSQL in a non-root devcontainer. That is the environment mxcli itself generates: `mxcli init` writes a devcontainer from mcr.microsoft.com/devcontainers/base:bookworm which installs the postgresql server and runs as `vscode`, and wires `run --local --setup --ensure-db` into the Claude Code SessionStart hook — so it fired on every fresh session in an initialized project, not only when someone typed the flag. Fixing it here rather than in the template also repairs projects that have already been scaffolded. Three independent defects sat on one path, each sufficient to block it: - The service start was never elevated. Debian's /etc/init.d/postgresql runs under `set -e` and calls create_socket_directory first, which chmods /var/run/postgresql — refused for a non-root user, so the script aborts before it looks at a single cluster. Now attempted with `sudo -n` first and unprivileged second. - The mendixlabs#823 user-owned-cluster fallback was inert on Debian and Ubuntu, which wrap only the client tools into /usr/bin while initdb and pg_ctl live in /usr/lib/postgresql/<major>/bin. The safety net for a failed service start could never deploy on the platform that needs it most. Both tools are now resolved from the same versioned directory, ranked numerically — a lexical sort puts "9" above "16", and a data directory made by one major cannot be started by another. - The superuser was unreachable: `sudo -u postgres` is refused by the devcontainer's own sudoers rule, `vscode ALL=(root) NOPASSWD:ALL`, which permits root as the only target. Falls back to `sudo -n -- sudo -n -u postgres`, since root may in turn target any account. Everything stays non-interactive, so an unattended run cannot block on a password prompt. Separately, the readiness timeout discarded the service manager's own output, which this package had already collected — the permission denial was in hand and reported as a generic 20s timeout. A guard that names the wrong cause costs the reader more than no guard at all, so the diagnostics now travel with the error. Measured on Ubuntu 24.04 / PostgreSQL 16 with a real non-root user and the real sudoers rule, against a harness calling EnsureDatabase directly: the shipped build fails with `exec: "initdb": executable file not found in $PATH` and, once the cluster is up, `no local PostgreSQL superuser available`; after the fix the sudo path provisions in 2.6s and the no-sudo user-cluster path in 7.2s. Each of the four fixes was reverted individually and its test re-run against the reported symptom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
…design system
The demo capture is most of a product-demonstration film's runtime, and its
narration overlay was the one piece of furniture drawn to nothing in
particular. Build it to video-system/DESIGN-LANGUAGE.md in
ako/mxcli-intro-video, which that repo makes normative over this skill.
Fixes a silent placement bug found while doing it. Under `html{zoom:z}` —
which take.js uses to reach a fixed-width Mendix layout — Chromium reports
getBoundingClientRect() in zoom-adjusted (video) pixels but getComputedStyle()
and style.* in CSS pixels, and narrate.js had it wrong in both directions:
point() read a rect and assigned it straight to style.left, so the highlight
landed at position x zoom (measured at 1.6842: a target at (168,202) ringed at
(274,330)), while the 96px caption plate reached the file at 162 video px
against a 184px band. Every overlay dimension is now stated in video pixels
and divided by a zoom passed to configure(); checkOverlay() measures the
installed plate against the band and throws at record time. Its control is one
line — omit the zoom and it reports a 310px plate starting at y770.
Design conformance: one accent and it belongs to the pointer; flat plane (no
shadow, no radius, an opacity pulse rather than an expanding glow); the plate
fills the caption band at the x96 gutter; the plate inherits the app's own
computed body font instead of asking for 'Segoe UI', which falls back to
DejaVu Sans on a clean build machine with nothing saying so; say() refuses
tofu glyphs. The compositor keep-alive is a sweeping hairline rather than a
spinning ring, since a system with no rounded corners can only spin a square —
still continuous, not per caption.
Narration is pinned to the catalogue: Kokoro bm_george, two-pass loudnorm to
I=-18.6:TP=-1.5 (was -16, which would have made the demo the loudest film in
the catalogue), a -30 LUFS bed, and per-line verification because TTS has
failed both 0-of-12 and 10-of-12 while passing a total-duration check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L45JFiJ6y58ftg2zWVEq7h
test(run-local): count settle polls instead of timing them
docs(record-narrated-demo): conform the capture overlay to the video design system
structure.go's successors() drops every IsErrorHandler edge before building
the graph, so an error path that rejoins the normal one scores zero in the
prevalence table — not because it is rare, but because the detector is blind
to it by construction. That is a third uncounted population beside the two
already noted, and it needs the same Mode 2 merge/join syntax by a different
route.
Measured across three whole projects (235 microflows, user-written modules
included, unlike the marketplace table): 7 carry a true error-handler flow,
1 rejoins the normal path, in 3 of 3 projects, always at an ExclusiveMerge.
That one microflow is what DESCRIBE flattens to an empty `on error { }` and
what produced the CE0709 over-connected end event fixed in #450.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Measured the failure the caveat implies rather than asserting it: repointing SUB_Feedback_SendToServer's error edge from the tail merge to the merge before the AppId split gives two behaviourally different graphs that DESCRIBE renders as the same MDL, differing only in a layout annotation and carrying no warning. Executing that MDL reproduces the first graph in both cases, so the upstream rejoin is silently rewritten while check, open and mxbuild all stay green. E0 (detect and warn) is independently shippable and worth doing regardless of Mode 2; E1 (join out of an on-error block) is a scoping rule on Mode 2's Phase 1 rather than a feature of its own; E2 pins the round trip using the mutated graph as the control. Also records what not to do: folding error edges into successors() would move the prevalence figure by reclassifying, since post-dominance there treats a successor as a branch of a condition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…es them
MDL could not say where an error path rejoins the normal one. The only two
spellings were "terminate on your own" and an empty `on error { }`, which means
"fall through to the enclosing branch's continuation" — so every other rejoin
collapsed to that one target on a describe -> exec round trip, silently.
Measured: repointing FeedbackModule.SUB_Feedback_SendToServer's error edge from
the tail merge to the merge before the AppId split ("on error, return empty" ->
"on error, re-enter the split") gave two behaviourally different graphs that
DESCRIBE rendered as the same MDL, bar one layout annotation, with no warning;
executing that MDL reproduced the tail-merge graph in both cases. mxcli check,
Studio Pro and mxbuild were clean throughout, so nothing could have caught it.
`merge <label>` declares an ExclusiveMerge; `join <label>` sends a path to it.
The label is MDL-only — a Mendix merge stores no name — so it is resolved at
build time and at describe time and never written to the model. Forward and
backward references both resolve, which is what makes a retry loop expressible.
Also covers crossed branches on the AUTHORING side: an inner split's branch
landing where an outer split's branch lands, which no nesting of `if`
reproduces. DESCRIBE of that shape without an error handler still flattens and
warns (MDL-FLOW01) — the general Mode 2 case is not done.
- grammar: MERGE token (added to annotationName so @merge(x,y) still parses, and
to keyword so a name "merge" still parses), mergeStatement, joinStatement
- builder: label registry shared with the error-handler sub-builder, deliberately
NOT with a loop's, whose LoopedActivity owns its own object collection; joins
resolved in a post-pass after the graph exists
- describe: merges an error handler rejoins are labelled from POSITION, not map
order, so re-describing an unchanged microflow is not a diff
- check: MDL-FLOW02 unresolved/unjoined label, MDL-FLOW03 duplicate declaration,
MDL-FLOW04 merge/join inside a loop or while body
Verified: describe -> exec -> describe is a fixed point for every error-handling
form; the two FeedbackModule graphs now describe differently and each round-trips
to its own; 0 errors on mxbuild 11.14.0 for all of it.
Two deliberate deviations from the proposal's Mode 2 sketch, both recorded there:
@position goes before `merge`, matching every other statement; and fall-through
into a merge is allowed rather than an error.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
feat(microflow): MERGE / JOIN named join points, and DESCRIBE that uses them
…pt-smaller-37u3fu # Conflicts: # CHANGELOG.md
fix: provision PostgreSQL as a non-root user (mendixlabs#984)
Reported in ako/view-entity-examples FINDINGS §6. `PARALLEL SPLIT … PATH 1
{ … } PATH 2 { … }` passed `mxcli check`, built at 0 errors and
round-tripped through `describe`, and at runtime every path ran from the
split straight to its end. The activities inside never executed and left
no row in `system$workflowactivity`. Every gate said the workflow was fine,
including the one that reads the model back.
Mendix stores an `EndOfParallelSplitPathActivity` as the last activity of
each path, and its engine executes a path up to that marker. mxcli wrote
paths without one — from the builder, from both writers (neither
serialized the type; both switches returned nil) and from both
`INSERT PATH` mutators.
Settled in one boot on Mendix 11.14.0, two instances of the same workflow
differing only in a trailing marker, reading `system$workflowactivity`
joined to each instance's context object:
without the marker neither path's call-microflow has a record
with the marker WF_A2 Finished, WF_B2 Finished
The shipped code was then booted on its own and the same query shows both
paths' activities Finished, plus an end record for an empty path.
Every path now ends with the marker, an empty path included (a flow
holding only the marker, as Mendix stores it). `workflows.EndParallelSplitPath`
is idempotent and does not append after a jump or an end-of-workflow, where
a marker would be unreachable. The modelsdk reader reads the marker back as
its typed activity: read generically, `describe` printed a
`-- [Workflows$EndOfParallelSplitPathActivity]` comment in every split.
The first cut named every marker `EndOfParallelSplitPath` and mxbuild
refused it as CE0495 "Duplicate name". Every unit test on the new shape was
green; only the build saw it, because `deduplicateActivityNamesInFlow`
renames only the activity types its switch lists and the markers were not
among them. An env-gated experiment had used unique names, which is how the
collision hid until the shipped code was built.
Controls: reverting the builder call fails `TestBuildParallelSplitEndsEveryPath`
with each path's real last activity; taking the marker types out of the
dedupe switch fails `TestDeduplicateNamesEndOfPathMarkers` with the
duplicated name. The doctype gate runs `mx check` on 24-workflow-examples
(a three-path split plus INSERT PATH) at 0 errors on both engines.
A workflow already written with an affected split needs a
`create or modify` to pick up the markers; running instances are not
changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both from ako/view-entity-examples FINDINGS.
- system-module: the System.User association table named `User_UserRoles`.
XPath wants `System.UserRoles`; the table's spelling fails the build with
CE1613 "The selected association … no longer exists" — three user tasks
carrying the same targeting constraint made it three errors. mxcli's own
executor and its code comments already use `System.UserRoles`.
- create-page widgets reference: `datasource: microflow Module.GetData` was
described as "no `()`, the name alone". That holds only for a microflow
with no parameters; with one, Mendix fails CE1571, and `mxcli syntax page
datasource` already says so ("one argument per PARAMETER, required"). The
reference now shows the argument form beside the bare one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mendix ends each boundary event path with EndOfBoundaryEventPathActivity,
and MDL has no end statement, so a path written from MDL had no ending
unless it was a jump. Measured on 11.14.0: an interrupting path is CE0105
at build; a non-interrupting one builds at 0 errors and the runtime refuses
to start ("Expected the flow to end with an end event").
CREATE WORKFLOW and both INSERT BOUNDARY EVENT mutators now append the
marker unless the path already ends in a jump or end-of-workflow (CE6692
if a marker follows one).
The create-or-modify guard counted $Types containing "BoundaryEvent",
which includes the marker, so re-running a fixed script against a fixed
workflow was refused as dropping an event. It now matches the suffix.
Reported in ako/view-entity-examples FINDINGS §7.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`boundary event timer '…'` writes Workflows$TimerBoundaryEvent, which no Mendix 11 runtime has. mx check and mxbuild accept it; the runtime then fails to start: "Class 'Workflows$TimerBoundaryEvent' could not be found" (measured on 11.14.0). It was the example in `mxcli syntax workflow boundary-event`. check --references and exec refuse it on 11.x, in CREATE WORKFLOW and in every ALTER WORKFLOW op that can carry a boundary event. 10.x is left alone. The syntax help, the write-workflows skill and the #415 bug-test script now name the kind and use a DateTime expression for the delay. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(workflow): end split and boundary paths, refuse a bare boundary timer
…DL080) `select m.ID as MeterRef` gives a view entity an association named MeterRef. Declaring it in the attribute list as well — `MeterRef: Trends.Meter` or `MeterRef: Trends.Meter.ID` — parsed as an enumeration type and exec stored it as one. Measured on 11.14.0: CE1613 "The selected enumeration 'Trends.Meter' no longer exists", and for the three-part form mx check throws before validating anything. check passed it; check -p reported "1 columns but 2 attributes" and compared the attribute against the next column. - check, with no project (MDL080): a declared name that is an association column's alias, or a three-part type name. - check -p: an attribute typed with an entity under any other name, before the type comparison that misreported it. - exec: both, before any write, --no-check included (0 files changed). Reported in ako/view-entity-examples FINDINGS §4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(view-entity): refuse an attribute that tries to hold an object (MDL080)
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.
Sync from ako/mxcli: 11 commits not yet on mendixlabs/mxcli:main.
Workflows: shapes that built and then did nothing (ako/view-entity-examples FINDINGS §6, §7)
EndOfParallelSplitPathActivity, and the engine runs a path only up to that marker. mxcli never wrote one, so each path went straight to its end and none of its activities ran. Every path now gets the marker, fromCREATEand from bothINSERT PATHmutators. Checked at runtime on 11.14.0.EndOfBoundaryEventPathActivity, an interrupting path fails the build with CE0105, and a non-interrupting one builds but the runtime refuses to start. Every boundary path now gets the marker unless it already ends in a jump.create or modifyguard counted the new marker as a second boundary event and refused a valid rewrite. It now matches the type suffix.boundary event timeris refused on 11.x (MDL-WF07). It writesWorkflows$TimerBoundaryEvent, which no Mendix 11 runtime has, so the app will not start. It was the documented example; the syntax help and skill now name the kind.System.UserRoles, notSystem.User_UserRoles(CE1613), and a microflow datasource that takes parameters must be written with its arguments (CE1571).View entities (FINDINGS §4)
MeterRef: Trends.Meterbesideselect m.ID as MeterRef) was stored as an enumeration naming an entity. The build then failed with CE1613, ormx checkthrew before validating anything.checkrefuses it without a project;check -prefuses an entity type under any other name.execrefuses both before writing,--no-checkincluded.Microflows: error-handler rejoins
MERGE/JOINnamed join points, andDESCRIBEoutput that uses them. MDL could not say where an error path rejoins the normal flow. Every rejoin collapsed to the enclosing branch's continuation on a describe → exec round trip, silently.Local runtime and tooling
run --local --ensure-dbfailed in the non-root devcontainer thatmxcli inititself generates, on every fresh session.