Sync ako/mxcli: rules as a document type, workflow data-loss fixes, and the test-annotation framework - #952
Merged
Conversation
`if Module.SomeRule(param = $x) then` was stored with no Condition at all
on the modelsdk (default) engine. Measured on mxbuild 11.13.0:
default engine → [CE0080] "The 'Condition' property is required."
at Decision 'Sample.Rule_IsActive(IsActive = $IsActive)'
MXCLI_ENGINE=legacy → 0 errors, same script, same project
after this change → 0 errors, BSON identical to legacy's modulo key order
Two defects, one behind the other.
splitConditionToGen handled only ExpressionSplitCondition and fell through
to `default: return nil`; the caller then skipped SetSplitCondition
entirely, so the ExclusiveSplit was written with only its caption. The read
side had been implemented for mendixlabs#723 — DESCRIBE renders the broken document
as `if true then` while the caption still shows the call — and that
asymmetry is why the corruption was silent, and why re-applying an
identical CREATE OR MODIFY over a Studio Pro-authored microflow turned a
clean project into a broken one.
modelsdk/gen binds Microflows$RuleCall.Rule to the BSON key "Rule", where
Mendix stores "Microflow" (rules share the microflow namespace).
generated/metamodel (`json:"microflow"`), sdk/mpr/writer_microflow.go and
the keyaudit ledger all agree. Fixing only the missing case would have
written a reference Mendix never reads and produced the same CE0080 for a
different reason, so this is a STORAGE-NAME OVERRIDE applied to both the
encode and the decode literal, with the ledger row struck off.
The reported "works on Mendix 10, fails on 11" is an engine difference, not
a version one: the microflow write path branches only on major >= 10 / <= 9,
so 10.24 and 11.12 take identical code.
Controls: reverting the writer case alone fails
TestMicroflowRoundTrip_RuleSplitCondition with a nil condition; reverting
the storage-name override alone fails
TestRuleSplitConditionUsesMicroflowStorageKey while the round-trip test
still passes — which is why the storage key is asserted separately.
Refs mendixlabs#939
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
… have one
A Mendix expression has no user-callable functions — its library is built-in
and unqualified — so `Module.Name(arg = $x)` in a value position is not an
expression at all. mxcli wrote the literal text on both engines. Measured on
mxbuild 11.13.0:
declare $b Boolean = Sample.Rule_IsActive(IsActive = $IsActive);
→ [CE0117] "Error(s) in expression." at Create variable activity
declare $n Integer = Sample.MF_Callee(N = $N);
→ [CE0117] — a microflow is no more callable there than a rule
Both have a working spelling, which is what makes this worth a diagnostic
rather than a shrug: a microflow or Java action is `$r = CALL MICROFLOW …`,
and a rule belongs in a decision.
The check splits by what it needs. Whether a qualified call is illegal in a
given position is project-less — no built-in Mendix expression function has a
dot in its name — so MDL066 reports it from the AST alone, and exec's
pre-flight gate refuses the script. The one legal home for a bare qualified
call is a decision condition, which mxcli stores as a RuleSplitCondition, so
MDL066 exempts `if` and nothing else; a while condition has no rule-split form
and is flagged. (While bodies were not walked at all before, so nothing inside
one was checked.)
Whether the name in a decision resolves to a RULE needs the backend, so that
half lives in the flow builder: a qualified call that is not a rule used to
fall back to an ExpressionSplitCondition holding the call text — valid-looking
MDL that is CE0117 — and is now refused with the CALL MICROFLOW spelling.
Controls: with the MDL066 walk stubbed out, the value-position and while tests
report 0 violations; the builder test pairs the refusal with a real rule that
must still produce a RuleSplitCondition.
Refs mendixlabs#939
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
…en as junk
`mxcli check -p` reported
Unexpected token after expression — the expression appears incomplete or
malformed (possible missing space between keywords)
→ Check for glued keywords such as 'emptyor' …
with an empty location, on a script that parses fine without -p and executes
correctly. exprcheck did not model `Module.Name(...)`: the qualified name
parsed as a member path and the `(` was left on the stream, which Parse
reports as leftover.
It fired on the VALID decision form as much as the invalid ones, so it carried
no signal, named a typo that was not there, and masked the real defect in the
same script (mendixlabs#939). parseQualifiedCall now consumes the argument list.
UnknownFunctionCalls skips the new node. It feeds MDL044's did-you-mean, and a
qualified name has no near built-in, so it would emit nonsense — and it would
fire on the legal decision form. MDL066 owns that diagnostic and knows which
positions are legal.
Control: with parseQualifiedCall reverted, the test reports the trailing-token
hint and a *QNameExpr. A bare qualified name with no parentheses must still
parse as a QNameExpr (enumeration/entity reference), which is pinned too.
Refs mendixlabs#939
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
`show callers of Module.SomeRule` reported none even with the condition correctly stored, and `show references to` was empty. A rule is not an activity — Mendix evaluates one only as an ExclusiveSplit's condition — and the reference extractor walked only action activities, so the edge was never emitted. mendixlabs#939's reporter read that as "the reference never resolves". collectRuleCalls walks the object collection for a RuleSplitCondition, recursing into LoopedActivity bodies as collectActionActivities does, and emits a `call` edge into CATALOG.REFS. Measured on a real 11.13 project: `show callers of Sample.Rule_IsActive` goes from "(no callers found)" to the calling microflow. Rules are still not catalog OBJECTS, so a microflow called only from inside a rule's body remains dead code to QUAL004 / GRAPH_DEAD_ASSETS. Adding the object type without also indexing rule bodies would report every rule itself as dead, so it needs both halves at once — noted in the repro and the symptom table rather than half-done here. Also adds the mendixlabs#939 reproduction script and two symptom-table rows. Refs mendixlabs#939 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
…catalog Two upstream issues have now landed on the consequences of mxcli not supporting Microflows$Rule rather than on the gap itself, and both were fixed one symptom at a time: mendixlabs#723 §A4 (IsRule unimplemented → CE0117) and mendixlabs#939 (no write path → CE0080, plus three secondary symptoms with their own causes). The underlying state is unchanged, so neither is the last one. Surveyed against a real 11.13 project carrying a rule: LIST FOLDERS shows it (for free, via ListDocumentUnits + DocumentKind) and a decision can call it, but SHOW/DESCRIBE/CREATE/ALTER/DROP/MOVE do not parse, it is absent from ast.MoveDocumentTypeByKeyword's 31 doctypes and from CATALOG.OBJECTS, and its body is never walked — so a microflow called only from inside a rule reads as dead code. The proposal's shape: a rule is a third flow flavour. createNanoflowStatement is already a verbatim mirror of createMicroflowStatement sharing the body grammar, the flow builder and the describer, differing by a $Type, a flowBuilder flag and a disallowed-activity list. A rule is the same relationship with Mendix's own restriction list, so almost none of the work is new. Four slices, the first two read-only. Slice 3 (authoring) is blocked on a Studio Pro-authored rule: the document shape here is verified by construction — a synthetic rule keeping the ten keys initRule declares checks clean on mxbuild 11.13 — and mxbuild tolerating it proves little, since Studio Pro is stricter. The open measurement is the canvas parameter's $Type, where the metamodel lists both Microflows$MicroflowParameter and Microflows$RuleParameter and real documents show the metamodel's split is not what storage does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
…endixlabs#945) ALTER WORKFLOW over --mcp wrote CE0495 "Duplicate name" into live projects while reporting success, because two independent gaps lined up. A CALL MICROFLOW activity is named after its target microflow, so two calls to the same microflow collide before anything inspects them. The file backends deduplicate in wfmutator; the MCP backend had no equivalent and sent the derived name verbatim. Measured live: PED accepts the duplicate with SUCCESS and does not auto-rename, and it refuses a later `set` on the activity's /name ("Element type does not support renaming") — so the name has to be right at add time. Nothing caught it afterwards either. mcpWorkflowMutator.Save() early-returned when only activity ops had run, so ped_check_errors — the only thing that sees a consistency error — was never called. Extract the naming policy into mdl/backend/wfnames, shared by both file backends and MCP: a taken-set rather than a seen-count (counting re-collides when the workflow already holds the suffixed name), recursing into outcome and boundary-event sub-flows because uniqueness is workflow-wide. The MCP side seeds it from names collected on the walk resolve() already performs, so it costs no extra PED round-trips, and it is wired into all six activity-adding ops. CREATE / CREATE OR REPLACE were never affected — the executor deduplicates for every backend. Wiring the validation in exposed a second defect: ped_check_errors reads Studio Pro's background error list, which lags the write in both directions. Measured at a 20ms poll interval, 26 samples across three write-load levels: an error becomes visible 77-115ms after the write and stops being shown 75-128ms after the fix — one symmetric debounce, no growth under load. (An earlier 170-350ms reading was a polling-granularity artifact.) Asked immediately, the check misses a real error and equally reports one a just-applied op has already cleared, failing a valid statement. Backend.pedCheckDocument now owns the pacing for every call site: wait settleDelay, then re-ask across settleWindow while the answer stays clean, short-circuiting on an error. It lives in the shared helper so a new write path cannot forget it; no backend operation validates more than once. Verified live with the immediate check as control, 6 trials per direction: immediate missed the real error 6/6 and falsely failed on a cleared transient 6/6; settled was 0/6 on both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ako/TestApp (Mendix 11.13.0) carries two Studio Pro-authored rules — Boolean return with a String parameter, and enumeration return with an entity parameter — which answers both questions the authoring slice was blocked on. The canvas parameter is Microflows$MicroflowParameter carrying the ParameterObject shape, identical to a microflow's; Microflows$RuleParameter appears in neither document, so it is an SDK-side name with no storage counterpart and the rule writer reuses microflowParameterToGen unchanged. ReturnType is NOT written. gen declares it and generated/metamodel 11.6.0 does not; Studio Pro 11.13 writes only MicroflowReturnType. So it is not an Interval/IntervalType-style carry-through — there is nothing to carry, and mxcli must not invent it. Both rules store exactly ten properties and nothing else. That the microflow-only keys are absent is now measured rather than inferred from the type definition: a Studio Pro microflow in the same app stores 19, including AllowedModuleRoles, StableId, Url and the concurrency group. Also records two deltas found while diffing, both general to microflows rather than rule-specific and neither with a known symptom: Studio Pro writes a bare marker for an unconditional flow's CaseValues where mxcli writes an explicit NoCase, and stores the connection indices as int64 where mxcli writes int32. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
… count() assertions Two defects in .test.mdl handling, both of which made a suite report green while asserting less than it looked like it did. Bug 1 — a file-level `/** … */` header made the first test vanish: not a pass, not a fail, not an error, with every later test renumbering over it. A header is not separated from the test below it by a '/', so both land in one chunk, and extractDocAndBody took the chunk's FIRST doc comment as the test's own. It carried no @test, so the whole chunk — real test included — was skipped. The delimiters were found by substring search with no notion of comment or string state, which is why the documented workaround (a `--` header) re-triggered the fusion the moment that header's prose spelled `/**` and `*/` out. The chunk is now scanned rather than searched: the doc is the last comment of the leading run, the body starts after it, and a '/' inside a line comment or a string literal is text. Two @test doc comments in one chunk are refused by name instead of resolved — either resolution runs one and silently drops the other, which is the bug. Tests are numbered as tests, not as chunks (a skipped header used to leave a file's only test reporting as test_2), and each test's line travels with its chunk instead of being recovered by searching the file for its first 20 bytes, which found the wrong chunk when two started alike and panicked on a chunk shorter than that. Bug 2 — `@expect count($Var) = N` on a bare retrieve block reported PASS unconditionally, for any N, against an empty table. count() is not a Mendix expression function: counting a list is an Aggregate list activity, so the condition could never be compiled into the decision that evaluates the assertion. On 0.18.0 the annotation was dropped during parsing, and a test with no assertions passes as long as its body does not throw. Since 0bf9382 it is an ERROR — fail-closed, but it left the natural spelling unusable. The count is now lifted into the activity an author would write by hand, `$mxtest_count_X = COUNT($X);` ahead of the decision, in both the endpoint and the monolithic runner; in the latter the generated variable is renamed per test like the body's own, or two tests counting same-named lists declare it twice. sum/average/minimum/maximum aggregate an attribute an assertion cannot supply, so they stay refused — now naming the helper-microflow workaround rather than reporting that they are not expression functions. Verified at the Mendix layer, not just the parser's: the generated flows were exec'd into a real 11.13.0 project and mx check run against a pristine-copy baseline (1 pre-existing error either side, none in the generated flows). Controls: the pre-fix binary finds 2 of the repro file's 4 tests, and stubbing out the aggregate branch restores "count() is not a Mendix expression function". Fixes mendixlabs#927 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…afc4e fix(mcp): deduplicate workflow activity names and settle validation (mendixlabs#945)
modelsdk/gen bound Security$ProjectSecurity's two user-role references as
"AdminUserRoleName" and "GuestUserRoleName". Every Studio Pro document stores
them without the suffix, and generated/metamodel agrees (`json:"adminUserRole"`,
`json:"guestUserRole"`). Both were on the known-mismatch ledger, unfixed.
The read half was live and engine-dependent: SHOW PROJECT SECURITY printed no
Guest User Role on the modelsdk engine while the legacy parser — which reads the
right key — printed it for the same project. The same blind spot reached the
Starlark lint API, where the documented anonymous_user_role and role.is_anonymous
fields resolved to "" and never matched, so a rule asking what anonymous visitors
can read found nothing.
The write half had not fired yet but was the more serious one. gen preserved the
stored GuestUserRole as an unknown-key passthrough, so existing writes lost
nothing; calling the setter would have added a second key beside it, which
Studio Pro refuses to open even though mxbuild tolerates it.
A property.Primitive decodes and encodes under one bound name, so one literal per
property covers both directions.
Control: with the role rewritten under the pre-fix key, mx check 11.13 reports
CE0133 ("No user role for anonymous users selected...") — 6 errors against 5 with
the storage name.
Refs mendixlabs#924
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Anonymous access was readable but not writable, so an app with a public area
could not be built unattended — every headless run ended with a manual step in
Studio Pro.
ALTER PROJECT SECURITY GUEST ACCESS ON ROLE <UserRole>;
ALTER PROJECT SECURITY GUEST ACCESS ON; -- role already stored
ALTER PROJECT SECURITY GUEST ACCESS OFF;
Wired the way DEMO USERS ON|OFF already was: same AST node, same unit, same write
choke point, both engines.
Two mxbuild 11.13 behaviours shape the syntax, and they pull in opposite
directions. Guest access on with an empty role is CE0133 (6 errors against 5), so
the role cannot be optional in effect — but a role that does not exist builds
with the same count as a valid one, leaving anonymous visitors with no access at
all. So ROLE is optional in the grammar, because a stored role satisfies CE0133
and re-enabling should not force a retype, while the executor refuses ON when
neither source supplies one and validates the name itself against the project's
user roles.
OFF keeps the stored role: guest-off-with-role is valid Mendix, and clearing it
would lose the operator's choice on a toggle.
No check-time counterpart in validate_security.go, deliberately — whether a bare
ON is legal depends on what the project stores, which mxcli check cannot see.
Closes mendixlabs#924
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
TestApp now places both rules in a folder, which settles the placement question by measurement: the rule's unit row is ContainmentName "Documents" with ContainerID pointing at the folder unit, itself Folders under the module — identical to a microflow. Nothing about a rule's placement is special. That shrinks the plan twice. LIST FOLDERS already renders foldered rules with no change, because the ListDocumentUnits walk is containment-generic; and the MOVE/FOLDER work becomes one entry in ast.MoveDocumentTypeByKeyword rather than a placement implementation. Still outstanding, and still not blocking: TestApp contains no Microflows$RuleSplitCondition. Rules.MicroflowUsingRule exists but is empty (start to end, no decision), so the shape mendixlabs#939 writes remains validated against the legacy engine's output rather than against Studio Pro's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
The annotation patterns were unanchored, so `@expect` was matched anywhere in a doc-comment line rather than at its start. Prose that quotes a tag became a real annotation: a sentence reading "`@expect $x = 1` in a sentence" gave the test that assertion, and "@cleanup none would apply here" changed its cleanup strategy. The invented assertion usually fails to compile, so the test reports an ERROR whose message quotes the prose. Found while writing the mendixlabs#927 repro file — its header explained the bug, and explaining it created an assertion that then errored against a test nobody had written. An annotation is a javadoc tag, and a tag opens its line. Every pattern is now anchored; the leading `*` and its indentation are stripped before they run, so the real spellings are unaffected at any indentation, including the one-line `/** @test x */` form — which the control test pins, since a fix that simply stopped reading annotations would otherwise pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
A test carrying both @throws and @expect reported making two assertions and made one. @throws selects a different generated shape in both generators — the verdict starts as a failure and only the error handler clears it — and neither emits the @expect checks into it, so the assertion could not fail whatever it claimed, while AssertionCount still counted it. This is the same silent-absence class as a dropped @expect or an unimplemented @verify, in the one combination where the assertion cannot be made to work: the body was expected not to produce a result to assert on. So it is refused rather than quietly ignored — the expects become assertion errors, which makes the test an ERROR and gives it no microflow. The @throws itself still stands; it is the assertion the test can actually make. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
The three testing pages documented a format the implementation has never had. They showed `.test.mdl` tests as `-- @test` line comments over DDL statements with `-- @expect 0 errors` for an outcome, and `.test.md` as plain ```sql blocks. mxcli reads javadoc blocks separated by `/`, `@expect` is any Mendix condition over the body's variables, and the markdown format needs an `mdl-test` fence. Someone following the pages wrote a file with no tests in it and got "Found 0 test(s)" with nothing to explain why. test-annotations.md and test-formats.md are rewritten against the parser and the runner; testing.md's framing and example follow. The annotation reference now covers @verify, @throws and @cleanup, which were missing entirely, and carries the rules that are only visible when you hit them: one-row-one-column for @verify, its refusal under the rollback default, count() as the one aggregate an assertion can make, a tag being read only when it opens its line, and @expect being refused alongside @throws. @setup is deliberately not documented: it is parsed and read by nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…ured TestApp gained a decision calling Rules.Rule1, so the RuleSplitCondition shape is no longer validated only against the legacy engine's output. Studio Pro stores the reference under the Microflow key with a marker-2 ParameterMappings list and a fully-qualified Parameter — every element of which is what the mendixlabs#939 fix writes, giving independent confirmation of the initRuleCall override. Round-tripped it too: DESCRIBE renders the rule call and show callers lists the microflow (the read and reference paths exercised against a Studio Pro document for the first time), and re-executing that describe output leaves mx check at 0 errors against a 0-error baseline, with the SplitCondition block absent from the before/after diff entirely. The diff's remaining entries are all pre-existing and general to microflows — connection-index widths, CaseValues shape, bezier control vectors a @Curve annotation did not carry, and object ordering. Recorded so the next person diffing a rule does not attribute them to this feature. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
…oflow A rule gets its own semantic type mirroring microflows.Nanoflow (already a distinct struct, not an alias of Microflow), its own statements, and its own listing: SHOW/LIST MICROFLOWS stays microflow-only, with SHOW/LIST RULES beside it via the existing showOrList rule so both spellings work. One place the nanoflow parallel stops: there is no GRANT EXECUTE ON RULE. Both Studio Pro reference rules store no AllowedModuleRoles, because a rule is not independently callable and so has no module-role security to grant. Records what 'like a nanoflow' buys: a rule's surface is much smaller, since a nanoflow is reachable from pages, navigation and widget actions while a rule is reachable only from a decision — so the page grammar, widgetobj, sdk/pages and gen/navigation need learn nothing about rules, while microflowBody, the flow builder, the describer and the validator are shared exactly as the nanoflow shares them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
…613)
Dropping an attribute reported success but left its validation rule behind,
so the project failed to build:
[error] [CE1613] "The selected attribute 'DropAttr.Account.AccountNumber'
no longer exists." at Validation rule of entity 'DropAttr.Account'
Two independent bugs on one statement, each of which hid the other.
1. The executor's cleanup never matched. A validation rule and an access
rule's MemberAccess reference their attribute by BY_NAME qualified name
("Mod.Ent.Attr"), while the cleanup compared the dropped attribute's
element ID. Both arrive in the same model.ID field, so the comparison
matched nothing and every rule was kept. Which field carries the name
also varies by engine: legacy fills AttributeID and AttributeName,
modelsdk fills only AttributeName. The check now accepts either form in
either field. Index references really are element IDs
(AttributePointer), which is why that one path always worked.
2. With (1) fixed the rule was removed in memory and still not written.
entityToGen only appends to a child list when it is non-empty, so a list
the update empties stays "clean" and the codec passes the stored raw
bytes through. The Indexes list already had a one-off fix for this
(ledger #39); ValidationRules, Attributes, AccessRules and EventHandlers
are now covered by one loop instead of being rediscovered one error code
at a time.
The Attributes case was worse than a dangling reference: dropping an
entity's only attribute reported success and wrote nothing at all.
Both bugs only bite when the update removes the LAST member of a list --
removing one of two dirties the list and works fine -- which is why they
survived: any two-member fixture passes against the broken code. The new
tests use exactly one member each, and the two-member case is kept as a
positive control.
Verified on a real 11.13.0 project: mx check goes 1 error -> 0, and with
the member-access half fixed the downstream ReconcileMemberAccesses pass
finds nothing left to repair. Controls: reverting either fix makes the
corresponding test fail with the reported symptom.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
A Microflows$Rule was the one document type mxcli could reference but not read. IsRule answered a yes/no question about a name; nothing could list a rule, show its signature, or render its body, so a rule was invisible to every surface except the decision that called it. Slice 1 of PROPOSAL_rule_documents: read-only, no new BSON writes. A rule is handled the way a nanoflow is — its own semantic type, its own listing — so SHOW/LIST MICROFLOWS still lists microflows only. microflows.Rule already existed as an unused stub whose comment claimed the return type is "always boolean". Rules.Rule2 in the reference app returns an enumeration, so that is now stated correctly, and the struct carries the ten properties a rule document actually stores, measured against two Studio Pro-authored rules. Both engines read it: ruleFromGen mirrors nanoflowFromGen, and the legacy parseRule mirrors parseNanoflow. Neither reads AllowedModuleRoles (a rule is not independently callable, so it has no module-role security) nor gen's ReturnType sibling, which Studio Pro does not write and generated/metamodel does not list — a pre-7 legacy property with nothing to carry through. DESCRIBE RULE renders re-executable MDL by wrapping the body in a Microflow and reusing formatMicroflowActivities, exactly as the nanoflow describer does. Verified against the Studio Pro rules rather than only against fixtures: LIST RULES reports both with their folder, parameter count and return type, and DESCRIBE renders the Boolean rule with its String parameter and the enumeration rule with its entity parameter. The encode test asserts the ten keys a rule carries and the absence of the nine it does not, with a control so it cannot pass against an empty document; both read tests were confirmed to fail when ruleFromGen drops the return type or ReturnVariableName. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
…abs#944) ALTER WORKFLOW ... REPLACE ACTIVITY X WITH <activity named X> renamed the activity to X_2, and re-running the same script compounded it: X_2_2, X_2_2_2, and so on without bound. A no-op replace renamed too. Nothing surfaced it — not mxcli check, not exec's output, and for CALL MICROFLOW steps not even DESCRIBE, which renders the target microflow rather than the activity's own name. The dedup helper was written for INSERT and reused by REPLACE. collectAllActivityNames walks the still-unmodified tree, so the activity about to be spliced out is still in the name pool when the replacement is checked against it: a same-name replace always collided with itself. Free the outgoing activity's name before deduplicating. It is read off the resolved element, not from activityRef, which may be a caption rather than the name. Only that one name is freed, so a replacement colliding with a surviving activity still dedupes — covered by a control test on both backends. The MCP backend had the same shape. I introduced it there in #204 on the guess that the file backends' behaviour was a deliberate conservative choice, and said so in a comment; it was neither deliberate nor correct, and that comment is corrected here. On severity: the report argued the rename detaches in-flight System.WorkflowUserTask records. Mendix's workflow-versioning documentation lists "changing names, captions, and titles" as explicitly non-conflicting and matches activities structurally, so that consequence does not follow. The case for fixing it is the unbounded accretion and the ADR-0008 idempotence break. That break is only half closed here: a no-op replace still rewrites the .mpr because addFreshPersistentID re-mints PersistentId on every activity write in both engines and neither reads it back. Tracked separately as mendixlabs#949. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
) Every Workflows$* element -- activities, outcomes, boundary events, and the workflow document itself -- carries a PersistentId. Both engines minted a fresh GUID for it on every write (addFreshPersistentID in modelsdk, a literal idToBsonBinary(generateUUID()) in legacy) and neither read the stored value back, so a rebuilt workflow never equalled the stored one. The visible consequence is that no-op elision could never fire for a workflow: re-running an unchanged ALTER WORKFLOW rewrote the .mpr every time and Studio Pro showed a version-control change on every run. That is the half of the ADR-0008 break that mendixlabs#944 could not close. Carry it in canon rather than in the writers. One place covers both engines and every nested type at once, and the writers keep minting fresh for genuinely new elements, which is correct: the carry only applies where a stored counterpart exists. identityFields/CarryIdentity were the wrong home -- they reach only top-level properties of the document root, and PersistentId sits on elements arbitrarily deep in the flow tree. CarryPersistentIDs reuses the structural pairing TransplantIDs is built on, with the traversal deliberately identical to pairer's: a divergence would let one element's $ID and PersistentId come from two different stored elements. Unlike an $ID a PersistentId is not a pointer target, so substituting one touches only its own element. TestFreshGUIDFieldsHaveAnIdentityDecision did not catch this and could not: it sees only properties registered through the codec's FreshGUIDFields, and both writers minted this one by hand. That blind spot is now written down where the guard lives. Verified by byte-comparing the .mpr across repeated no-op runs on both engines (MXCLI_ENGINE=legacy): three runs each, unchanged every time, where each run previously produced a different sha. Controls cover a real edit still writing, a differing $Type not inheriting an identity, injectivity, and MXCLI_ALWAYS_WRITE still preserving identity while disabling elision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`action: show_page Mod.Detail(Car: $Other)` inside a data view bound to $Car opened the page with $Car. The argument was not stored, not rejected and not visible afterwards. mxcli stores a widget show-page action with an EMPTY ParameterMappings array and lets Mendix infer the argument from the enclosing widget's context object. That half is deliberate and twice-confirmed: an explicit Forms$PageParameterMapping whose Argument is "$currentObject" is rejected as CE0115 "parameters do not match" (mendixlabs#296, re-confirmed on mxbuild 11.12.1 for findings §56). The half that was missing is the case where the author names something else — the builder built PageClientParameterMapping objects and both engines dropped them (formSettingsToGen takes only the page name; the legacy writer hardcodes the empty array). Nothing caught it. `mx check` reports 0 errors on the corrupted page, because an inferred mapping is a valid mapping, and DESCRIBE prints `(Car: $currentObject)` — so the description reads as a diagnosis of a lost mapping rather than an accurate report of a model that never held one. That is the trap §39's reporter spent three cycles in while distrusting a button that was correct. Refuse rather than author: mendixlabs#296 already established that Mendix rejects an explicit mapping, so the only honest options are "bind the context object" or "say no", and silently doing the first while the author wrote the second is the bug. Telling them apart needs the context VARIABLE, not its entity — both $Car and $Other have entity Mod.Car — so the data source's own name for the context object is now tracked alongside entityContext and an argument is accepted when it matches either spelling. That keeps the documented form working (create-page.md's `show_page Mod.EditPage(Product: $Product)` inside a data view on $Product is the arg-equals-context case) and flags only arguments that are provably discarded. Mirrored at check time as MDL-PAGEARG01, so the LSP and `mxcli check` catch it with no project — the same pairing as MDL-WIDGET09. Control: stub pageArgumentBindsContextObject to return true and the probe writes silently again, describing as $currentObject; drop the validator call and the check-time test reports 0 violations where it wants 1. `mx check` is worthless as a control here, which is the point of the bug. Found while verifying findings §39, whose reported half — DESCRIBE omitting the inferred mapping entirely — was already fixed by 4551a4e. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
…ences A rule was not an object in the catalog, and nothing ever entered a rule's body. Both halves matter and they had to land together: an object type without the body walk would report every document a rule calls as dead, which is worse than not knowing about rules at all. Rules now land in microflows_data as MicroflowType 'RULE' — a CATALOG.RULES view beside CATALOG.NANOFLOWS, and a RULE row in CATALOG.OBJECTS — with their parameters, activities and complexity, and their bodies indexed into CATALOG.SOURCE so `search` can find an expression only a rule contains. builder_references walks each rule the way it walks a nanoflow, so the documents a rule calls become reachable in the reference graph. Measured on the reference app with a microflow called only from inside a rule: before, `show callers` found none and GRAPH_DEAD_ASSETS listed it as dead; after, it has its caller and is gone from the dead list, while a genuinely uncalled microflow stays dead. The integration test encodes both — the unreferenced microflow is the control, so the assertion cannot pass by the dead-asset view going empty — and was confirmed to fail with the reported symptom when the rule walk is removed. Slice 2 of PROPOSAL_rule_documents. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
Add ALTER PROJECT SECURITY GUEST ACCESS, and fix the gen keys it depends on
fix(test): make a .test.mdl suite report what it actually asserts
fix(workflow): keep the name on a same-name REPLACE ACTIVITY (mendixlabs#944)
fix(domain-model): DROP ATTRIBUTE left orphaned validation rules (CE1613)
@setup is parsed into TestCase.Setup and read by nothing — it arrived with the original framework as one row of an annotation table ("Reference to setup block") and was never designed past that row. `mxcli test` accepts it and does no setup. The proposal works from the constraint that decides the design: the test endpoint runs one microflow per request in a transaction it owns, so anything that must be undone with the test has to run inside the test's own call, and a once-per-file fixture cannot be rolled back at all — which under --attach means seeding the developer's own database. So it proposes the shape that fits: @setup names a microflow, repeatable, run before the body inside the test's transaction, declarable once in a file header. A failing setup is an ERROR naming the microflow, not a FAIL blaming the test — that attribution and the file-level default are what earn the annotation its place over the line of MDL an author can already write. Deleting the annotation instead is kept as the first open question, not buried: it is a defensible answer and costs nothing to maintain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…alidator MDL could not create a rule, so a project's decision logic could be read but never written, and mendixlabs#939's own reproduction needed a rule hand-built by rewriting a microflow unit. Slice 3 of PROPOSAL_rule_documents. createRuleStatement mirrors createNanoflowStatement verbatim, sharing microflowBody, the flow builder and the describer; DROP RULE and a "RULE" entry in MoveDocumentTypeByKeyword follow the same pattern, and MOVE needed nothing rule-specific because a foldered rule's containment is an ordinary document's. Authoring is modelsdk-only. The legacy engine refuses with the message menus already use: a rule document is close enough to a microflow that a half-written one would look valid, and sdk/mpr has no serializeRule. What a rule may not contain is refused at check time by the same validateRule the executor calls, so `check` and `exec` cannot disagree. The restrictions are measured, not taken from the documentation — converting a microflow unit into a rule to get past the validator gives, on mxbuild 11.13.0, CE0009 "This action is not supported in rules." for a create, and CE0103 + CE0139 for a void or String return. Two keys the reference documents caught that a build would not. ExportLevel: Studio Pro writes "Hidden" on every rule and the first authored rule omitted it while mx check stayed green. Flows: Studio Pro writes it as the bare marker even when a rule has none, so it is registered as a mandatory list. An mxcli-authored rule now carries exactly Studio Pro's twelve keys — the test asserts the set both ways, so neither inventing microflow-only properties nor dropping one passes. Verified end to end on the reference app: two rules authored (Boolean with a String parameter, enumeration with an entity parameter) plus a decision calling one build to 0 errors; DESCRIBE round-trips; re-running reports "Unchanged"; MOVE, DROP and `show callers` all resolve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
fix(canon): carry workflow PersistentId across a rebuild (mendixlabs#949)
A workflow calling a microflow that exists nowhere passed `check --references` with "All references valid" and was written by exec with exit 0. Only Mendix's own validator noticed, as CE1613. The identical mistake inside a plain microflow body was caught, because validateFlowBodyReferences is wired to CreateMicroflowStmt/CreateNanoflowStmt only. validateWorkflowParameterMappings documents the intended behaviour and defers a target that is not in the project to "the missing-reference check". That check was never written, so this restores its assumption. The gap was not one reference kind. Measured against a baseline binary, every reference a workflow can hold went unvalidated: call microflow target call workflow target user task page user task targeting microflow workflow context entity the workflow's own module The module one matters more than it looks: exec creates a module on demand, so `check` was the only thing between a typo'd module name and a silently created module -- and it did this for a microflow but not for a workflow. ALTER WORKFLOW had no case in the validation switch at all and fell through to "skip validation", so it got nothing, not even a check that the workflow it targets exists. It now gets the same passes, applied to the activities any op introduces. check and exec run different passes -- exec does not run validateProgram -- so the guard is also called from the statement handlers, before findOrCreateModule. Same shape and reasoning as mendixlabs#833, where exec wrote microflows check rejected. The scriptContext is nil there: exec applies statements one at a time, so an earlier statement's output is already in the project. Two supporting pieces were missing: scriptContext did not track workflows at all (collectDefinitions had no CreateWorkflowStmt case), so a script creating a workflow and then calling it would have reported a false missing reference; and there was no buildWorkflowQualifiedNames. False positives are the real risk in a change like this. Verified by diffing error counts against a baseline binary across every workflow-touching script in mdl-examples/ with the referenced modules present: identical before and after, including the 400-line 24-workflow-examples.mdl suite at zero. System.* targets are exempt via isBuiltinModuleEntity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(pages): refuse a show_page argument that is not the context object
…kills Slice 4 of PROPOSAL_rule_documents, and the last of the four. A feature nobody can find is not shipped: `mxcli syntax microflow.rule` now documents the whole surface, MDL_QUICK_REFERENCE gains a row per statement, and a write-rules skill sits beside write-microflows and write-nanoflows as the third flow flavour. Each document carries the same three facts, because each is the one people get wrong: a rule returns Boolean or an enumeration and nothing else; a decision is the only place it can be called; and there is no `grant execute on rule`, because a rule's document stores no module-role security. The restrictions are listed with the CE numbers measured behind them rather than as prose. LSP completion needed no change — RULE and RULES are generated from the grammar keywords. The proposal is marked done and CLAUDE.md's implemented list records what the reference documents settled, including the two keys that are invisible to mx check: ExportLevel, which Studio Pro writes on every rule, and Flows, which it writes as a bare marker even when empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
TestMxCheck_DoctypeScripts runs every doctype example on both engines, and rule authoring is modelsdk-only by design — sdk/mpr has no serializeRule, and a rule document is close enough to a microflow that a half-written one would look valid, so the legacy backend refuses create/modify/drop rather than emitting one. Reads work on both engines. Same shape as the menu skip directly above it, with the same reasoning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
…xlabs#948) Three defects, each measured on the v1 fixture. 1. The default engine could not READ boundary events. mdl/backend/modelsdk/workflow_read.go had no boundary-event support at all (0 mentions) while the legacy parser has had it all along (17). Both engines WRITE them, so a boundary event mxcli had just written read back as absent: DESCRIBE rendered nothing and a describe -> edit -> re-exec round trip silently dropped the timer, its handler flow and the jump inside it. The reader now reconstructs all three timer variants, wired into the six gen types that carry them, and renders more than the legacy parser does (legacy omits the handler's inner call microflow). 2. A rewrite deleted what the script did not restate. CREATE OR REPLACE|MODIFY rebuilds the workflow from the statement, so a stored boundary event the script does not mention is gone along with its handler flow -- measured 1 -> 0 while exec reported "Created workflow" and exit 0. Nothing signals it afterwards: the result is a valid workflow that simply no longer does what it did. checkNoDroppedWorkflowConstructs refuses that, modelled on checkNoQueuedCalls. Boundary events are authorable, so restating them lets the rewrite through -- the normal way to edit such a workflow, and what describe now emits. Event sub-processes are not authorable at all (they exist in modelsdk/gen but have no semantic type, reader or writer anywhere in mxcli), so a stored one refuses outright. The stored side is read from the RAW unit, not through the semantic model: the reader is what was blind here, and a guard sharing its blind spot cannot see what it is meant to protect. Matching $Type by substring catches all three timer variants and any added later. 3. A describer bug the reader fix exposed. With boundary events finally readable, describe -> exec stopped parsing: "mismatched input 'outcomes' expecting ';'". The call-microflow describer emitted boundary events BEFORE outcomes, and the grammar requires the reverse (workflowCallMicroflowStmt: ... OUTCOMES? BOUNDARY EVENT?). Pre-existing on the legacy engine, invisible while the default engine emitted neither. The shipped workflow skill claimed describe -> drop -> exec round-trips safely, with boundary events conspicuously absent from its list of what comes back. It now says what does not round-trip and what a rewrite refuses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…endixlabs#943) The workflow roundtrip integration tests named user-task pages they never created. Reference validation for workflows did not exist, so the missing page went unnoticed and the workflow was written anyway -- which is the very thing mendixlabs#943 is about: Mendix rejects that workflow, mxcli did not. With the check in place, four of them fail: page not found: RoundtripTest.ReviewPage (referenced by user task page) page not found: RoundtripTest.SubPage (referenced by user task page) Create the pages instead of weakening the check. createTaskPages follows the page shape already proven by roundtrip_mxcheck_datagrid_test.go in this same suite. Covers all four names the tests reference, including ApprovePage, which sits inside a parallel split -- the validator walks nested flows, so it is caught too even though the run aborted before reaching it. These tests carry a build tag and skip without an mx binary and the source project, which is why a green local ./mdl/... did not catch this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat: full rule support in MDL — read, author, catalog (mendixlabs#939)
…ndixlabs#948) setupTestEnv defaults to the LEGACY backend, and legacy could always read boundary events. That is precisely why the default (modelsdk) engine having no boundary-event reader at all stayed invisible: DESCRIBE emitted nothing on the engine users actually run, so describe -> exec silently dropped the timer and its handler flow, while these tests stayed green. Identical to the TableMappings gap already documented in roundtrip_dbconnection_test.go -- "legacy renders the clause, modelsdk did not" -- and fixed the same way, by running across gateEngines. Also pins the USER TASK shape at unit level. The wiring is per-gen-type and genWf.UserTask (the older type) has no BoundaryEventsItems accessor at all, so the reader can only reach these through SingleUserTaskActivity / MultiUserTaskActivity. The integration tests use a user task, so it is worth asserting the encoder puts one somewhere the reader can actually see it rather than inferring it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ces (mendixlabs#901) `DELETE_BEHAVIOR PREVENT` parsed, printed "Modified association", and stored DeleteMeButKeepReferences — overwriting whatever the association had, which for the reporter was a cascade. The grammar and the generated parser were never at fault. DeleteBehaviorContext exposes an accessor for all five tokens; buildDeleteBehavior called CASCADE() and nothing else, so PREVENT, DELETE_IF_NO_REFERENCES and DELETE_AND_REFERENCES fell through to the zero value ast.DeleteKeepReferences. That is a *legal* behaviour, so no layer below could tell it had been substituted. Two more defects were underneath, found while proving the first fix: - Fixing the visitor alone put an out-of-domain enum on disk. ALTER ASSOCIATION SET built the stored value as DeleteBehaviorType(s.DeleteBehavior.String()), and String() spells the prevent case "DeleteIfNoReferences" where Mendix writes "DeleteMeIfNoReferences". Both write paths now share one conversion, storageDeleteBehavior. - DESCRIBE emitted `delete_behavior DELETE_CASCADE`, which is not a token — the parser rejects it. It is the line the reporter pasted as their starting state, so describe -> edit -> exec died on any cascading association. It now emits DELETE_AND_REFERENCES, matching the other two arms. Two beyond the report: DELETE_AND_REFERENCES (the canonical spelling of cascade, whose alias CASCADE worked) was downgraded the same way, and the built-in help advertised the non-token DELETE_CASCADE. mx check is not the oracle here. Measured on mxbuild 11.6.6, a project carrying "DeleteIfNoReferences" builds with 0 errors exactly like a correct one — only Studio Pro refuses it. So the test asserts the stored value is one of Mendix's three DeletingBehavior values, not merely the one that was asked for. Verified: each fix reverted individually reproduces its own symptom and no other; all ten token x path combinations correct end to end on a real 11.6.6 project; on-disk enums legal; mx check 0 errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
@setup was parsed into the test case and read by nothing — no generator, no runner, no reporter — so `@setup Mod.Seed` did no setup and said nothing about it. It now names a microflow called before the test's own statements. The design is in PROPOSAL_test_setup_annotation.md, and turns on one constraint: the test endpoint runs one microflow per request inside a transaction it owns, so a fixture that must be undone with the test has to run inside the test's own call, and a once-per-file fixture could not be rolled back at all — under --attach it would seed the developer's own database. Emitting the call into the generated microflow needs no protocol change and behaves the same on --local, --attach, Docker and the legacy after-startup runner. @setup eShop.ACT_SeedCatalog is repeatable, and a file's header comment may declare one for every test in the file — the file's fixtures running before a test's own. That is what makes the annotation worth more than `call microflow X;` at the top of each body, and it is only readable because a header comment stopped being swallowed by the first test (mendixlabs#927). A header may carry nothing else: @expect, @verify, @throws and @cleanup describe one test's execution, and are refused there by name rather than ignored. A failing setup is an ERROR naming the microflow, not a FAIL — the test never ran, and a broken fixture must not read as a broken feature. The endpoint carries that back as a third verdict prefix; the monolithic runner, which has no returned verdict, emits an MXTEST:ERROR: line that ParseLogResults now knows. An unresolvable setup microflow is refused before anything runs, without the pre-flight check the proposal considered: the generated flows are injected through `mxcli exec`, whose own check names the missing microflow and writes nothing. The whole-script --references pre-flight would have refused suites for references that have nothing to do with setup. Verified on mxbuild 11.13.0, both generators: the flows were exec'd into a real project and mx check run against a pristine-copy baseline — 1 pre-existing error either side, none in the generated flows. Covered shapes include a void fixture, a value-returning one, two composed, and one on a @throws test. Controls: stub the emission and the header lookup, and the setup tests fail with the setup absent from the generated MDL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
fix(executor): validate a workflow's references (mendixlabs#943)
fix(workflow): stop CREATE OR REPLACE deleting boundary events (mendixlabs#948)
fix(associations): DELETE_BEHAVIOR PREVENT was stored as keep-references (mendixlabs#901)
feat(test): implement @setup, the annotation that did nothing
The 65 bundled skills that ship with `mxcli init` had no guidance on standing up an HTTP endpoint you control: zero mentions of Prism, WireMock or mitmproxy anywhere in the repo. Every REST skill assumed a live third-party API, so a REST integration was developed against network, rate limits, credentials and a payload that can change under you — none of which is where the Mendix defects are. The new skill separates the two problems that get conflated (something must answer the request; the app must send the request there), and covers the mxcli-native endpoint swap that the proxy route is usually reached for instead: - `rest call` URLs are expressions, so `@Module.Constant + '/path'` plus `run/test --local --constant Name=value` repoints the app with no model change and nothing committed; `constant set --apply` flips an app that is already running, and `constant list` names the layer when the wrong value wins. - A REST client document's `BaseUrl` is a literal and cannot reference a constant, so that case is a one-line `create or modify` instead. - The forward proxy stays documented for a model you must not edit, with the HTTPS/CA caveat that makes it the last resort, plus the per-service proxy fields consumed OData carries as data. Prism's non-obvious behaviour is recorded symptoms-first: root path mounting, `Prefer: code=404` to drive error handlers, `-d` to catch a mapping that depends on one fixed payload, spec `security` producing real 401s, and why a vendor's full contract never starts. Also corrects rest-client.md, which stated flatly that an omitted `BaseUrl` falls back to `servers[0].url` — true only when that URL is absolute. A relative one is warned about and skipped, and the client then fails at call time rather than at import time. Registered in the skills README, the generated project CLAUDE.md table, and cross-linked from rest-client.md and test-app.md (a REST app's verification prerequisite is a reachable endpoint). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
…bs#948) Issue mendixlabs#948 asks for a runtime-observability section, on the grounds that the workflow skill concedes its own failure modes are invisible to every static gate and then offers no way to look at a running instance. The project skill it was raised against fills the gap by hand: base64 an admin password pulled from `ps eww`, register a log subscriber, POST preview_execute_oql with curl. None of that is necessary. mxcli already ships every piece as a skill -- verify-with-oql, write-oql-queries, analyze-runtime, system-module, test-app, run-local, runtime-admin-api -- and all seven sync to user projects. What was missing is any way to find them from where a workflow author stands: write-workflows.md referenced none of them. So this is cross-links, not new content, per the documentation rule in CLAUDE.md -- link to the canonical home rather than restate what lives there. Some of the hand-rolled recipes are also strictly worse than what ships: reading `<projectDir>/.mxcli/runtime.log`, which `run --local` tees automatically, beats registering a log subscriber. Two runtime traps are stated inline because they are workflow-specific and have no other home: the declared return type not being what the runtime checks (caught by MDL004 -- so do not reach for --no-check to get past it), and a parked instance not being a failed one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s-redo docs(skill): point the workflow skill at the runtime skills (mendixlabs#948)
docs(skills): add mock-rest-apis, the missing REST-mocking guidance
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.
32 commits since #942. Two feature areas — rules as a first-class document type, and the test-annotation framework — plus a run of workflow fixes where the common thread is silent data loss that no static gate reported.
Rules — a new document type, end to end
LIST RULES/DESCRIBE RULE, with the document shape pinned against Studio Pro before anything was written.CREATE/DROP/MOVE RULE, plus the restriction validator. A rule is handled like a nanoflow — settled by measurement, not analogy.show references tosees it.Workflows — five fixes, all silent data loss
CREATE OR REPLACEdeleted boundary events (Workflow feedback + issues #948). Measured 1 → 0, reported as success, exit 0. Root cause: the default engine's workflow reader had no boundary-event support (0 mentions) while the legacy parser has 17 — and both engines write them, so a boundary event mxcli had just written read back as absent. Reader fixed; rewrites now refuse to drop what they cannot restate (guard-don't-drop, ADR-0005). A describer clause-order bug the reader fix exposed is fixed too.check --referencesand was written byexecwith exit 0 — onlymx checkcaught it, as CE1613. Every reference kind was unvalidated: call microflow/workflow targets, user-task pages, targeting microflows, the context entity, and the module itself.ALTER WORKFLOWhad no validation case at all.REPLACE ACTIVITYrenamed the activity it replaced (ALTER WORKFLOW ... REPLACE ACTIVITY silently renames the replaced activity even on a same-name/no-op replace #944), compounding on every re-run —X→X_2→X_2_2. The dedup pool still contained the activity being removed.PersistentIdwas re-minted on every write (Workflow activity PersistentId is re-minted on every write (never preserved, never read back) #949), so no workflow write could ever be elided and every no-op rewrite produced a version-control diff. Carried incanon, which covers both engines and every nested type at once.ped_check_errorsreads Studio Pro's background error list, which lags the write in both directions (measured 77-128ms, 26 samples) — so an immediate check both missed real errors and reported cleared ones. Paced at the single choke point.Test framework
@setupdid nothing — the annotation was parsed and ignored. Now implemented.count()assertions were not evaluated; an annotation was read even when it did not open its line;@expecton a test expecting an exception is now refused.Domain model, pages, security
DELETE_BEHAVIOR PREVENTwas stored as keep-references (DELETE_BEHAVIOR PREVENT reports success and writes DELETE_BUT_KEEP_REFERENCES, destroying a stored DELETE_CASCADE #901) — the opposite of what was asked for.DROP ATTRIBUTEleft orphaned validation rules behind, giving CE1613.show_pagerefuses an argument that is not the context object.ALTER PROJECT SECURITY GUEST ACCESSadded;AdminUserRole/GuestUserRolewere bound under their SDK names rather than their storage names