Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-10-15", "symptom": "`mxcli check script.mdl -p app.mpr` printed \"Check passed!\" for a script that `mxcli exec` on the same project then refused for the project's Mendix version, after running the statements before the refused one (on a 9.24 project: `create view entity` -> \"create view entity requires Mendix 10.18.0+\"; the issue's own StableId/CaseValues refusal was the write-time property floor, already cured by #1410)", "cause": "Every version gate exec applies lived inline in its handler as a checkFeature(ctx, area, name, …) call — about two dozen across 17 files, several conditional on the statement's content (send email in the body, a page with parameters, SupportsGraphQL, a timer event sub-process). check -p and exec's pre-flight never run a handler, so neither could see a gate: the project version was consulted only when the handler ran.", "file": "`mdl/executor/version_gates.go` (statementVersionGates: the one statement-to-feature table; checkVersionGates for the handlers; Executor.CheckVersionGates -> MDL-VERSION01 for `cmd/mxcli/cmd_check.go` and `cmd/mxcli/exec_preflight.go`); the handlers in cmd_*.go now call checkVersionGates(ctx, stmt)", "insight": "A refusal only a handler can raise is a check/exec gap by construction (duplicate-resolver-drift, check versus exec): move the decision into a pure function of the statement that the handler and the pre-flight both call, rather than giving check a second copy. TestCheckFeatureIsOnlyCalledFromTheGateTable fails on any new direct checkFeature call, so a future gate cannot bypass the table. Two kinds stay exec-only: gates that depend on stored state (drop/alter of a message definition document that is not there) and the write-time property floor (canon.StripUndeclaredProperties), which needs the encoded BSON and is cured by the writer emitting the version's shape (#1410), not by a check rule. Fixture for any Mendix 9 check: modelsdk/mpr/testdata/v1-project/App.mpr is 9.24.30.", "refs": ["mendixlabs/mxcli#1434", "mendixlabs/mxcli#1410"]}
2 changes: 1 addition & 1 deletion .claude/skills/mendix/check-syntax/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pages, microflows and icons — against that project. It reaches inside stored
documents where a name can only be answered there: an `ALTER PAGE … SET` is
dry-run against the page it edits, so a widget the page does not have, or a
property the stored widget does not declare, is reported here rather than
stopping the script partway through `exec`. What else it resolves, and what a pass still does not prove: [reference/references-resolved.md](reference/references-resolved.md).
stopping the script partway through `exec`. What else it resolves (including the project version's gates, MDL-VERSION01), and what a pass still does not prove: [reference/references-resolved.md](reference/references-resolved.md).

```bash
mxcli check script.mdl # syntax + model-free rules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ Beyond the page, microflow, entity and icon names a statement names directly,
- `Module.Enum.Key` in an expression;
- a view entity's FROM / JOIN entities.

## The project's Mendix version

`check -p` applies the version gates `exec` refuses with — the same
statement-to-feature table, the floors in `sdk/versions/*.yaml` (`show features`)
— as **MDL-VERSION01** (mendixlabs/mxcli#1434):

```
statement 5: create view entity requires Mendix 10.18.0+ (project is 9.24.30.51827) [MDL-VERSION01]
```

`exec` reports the same line in its pre-flight and writes nothing. Without a
project there is no version, so nothing is reported. Still exec-only: a gate that
depends on what the project stores (a `drop`/`alter message definition` of a
document that is not there), and the write-time property floor (`this Mendix X
project cannot store …`), which needs the encoded document.

## Not yet resolved

A passing check does not prove these; `mxcli docker check` is still the gate:
Expand Down
148 changes: 148 additions & 0 deletions cmd/mxcli/check_version_gates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: Apache-2.0

package main

import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"testing"

"github.com/mendixlabs/mxcli/mdl/ast"
"github.com/mendixlabs/mxcli/mdl/backend"
modelsdkbackend "github.com/mendixlabs/mxcli/mdl/backend/modelsdk"
"github.com/mendixlabs/mxcli/mdl/deprecation"
"github.com/mendixlabs/mxcli/mdl/executor"
"github.com/mendixlabs/mxcli/mdl/visitor"
)

// The issue's own script (mendixlabs/mxcli#1434): a module and flows on a
// Mendix 9.24 project. The decision exercises the two properties the reported
// refusal named — Microflow.StableId (10.2+) and SequenceFlow.CaseValues
// (10.15+) — which the flow writers no longer emit below those versions (#1410).
const issue1434Flows = `create module R1434;
create entity R1434.Item ( "Count": integer );
create microflow R1434.MF_Empty () begin return; end;
create microflow R1434.MF_Decide ($Item: R1434.Item) returns boolean
begin
declare $Result boolean = false;
if $Item/"Count" > 10 then
set $Result = true;
end if;
return $Result;
end;
`

// The same script plus a statement the project's version cannot store: exec's
// handler refuses `create view entity` below 10.18.
const issue1434Gated = issue1434Flows + `create view entity R1434.ItemView (Name: String(200)) as select cast('a' as String) as Name;
`

// copyV9Project copies the Mendix 9.24 v1 fixture the modelsdk/mpr tests use.
func copyV9Project(t *testing.T) (dir, mpr string) {
t.Helper()
src := filepath.Join("..", "..", "modelsdk", "mpr", "testdata", "v1-project", "App.mpr")
raw, err := os.ReadFile(src)
if err != nil {
t.Skipf("Mendix 9 fixture not found: %v", err)
}
dir = t.TempDir()
mpr = filepath.Join(dir, "App.mpr")
if err := os.WriteFile(mpr, raw, 0o644); err != nil {
t.Fatal(err)
}
return dir, mpr
}

// mendixlabs/mxcli#1434: `mxcli check -p` printed "Check passed!" for a script
// that exec, on the same project, refused for the project's Mendix version —
// after the statements before the refused one had run. check -p now applies
// the gates exec's handlers apply (executor.CheckVersionGates), from the same
// statement-to-feature table.
//
// Controls: the gate-free script still passes, and check leaves the project
// as it was.
func TestCheck_RefusesWhatTheProjectVersionCannotStore(t *testing.T) {
dir, mpr := copyV9Project(t)
_ = checkCmd.InheritedFlags()
_ = rootCmd.PersistentFlags().Set("project", mpr)
defer func() {
_ = rootCmd.PersistentFlags().Set("project", "")
rootCmd.PersistentFlags().Lookup("project").Changed = false
}()
if f := checkCmd.Flags().Lookup("format"); f != nil {
_ = f.Value.Set(f.DefValue)
}
before := treeDigest(t, dir)
scripts := t.TempDir()

var code int
gated := writeScript(t, scripts, "gated.mdl", issue1434Gated)
out := captureStd(t, func() { code = runCheckFile(checkCmd, gated) })
if code == 0 || strings.Contains(out, "Check passed!") {
t.Errorf("check passed a statement the 9.24 project cannot store (exit %d):\n%s", code, out)
}
for _, want := range []string{"statement 5: create view entity requires Mendix 10.18.0+", "MDL-VERSION01"} {
if !strings.Contains(out, want) {
t.Errorf("check output must name %q:\n%s", want, out)
}
}

// Control: the flows alone are storable on 9.24 and pass.
flows := writeScript(t, scripts, "flows.mdl", issue1434Flows)
out = captureStd(t, func() { code = runCheckFile(checkCmd, flows) })
if code != 0 || strings.Contains(out, "MDL-VERSION01") {
t.Errorf("control: a script the 9.24 project can store failed its check (exit %d):\n%s", code, out)
}

if n := countDiff(before, treeDigest(t, dir)); n > 0 {
t.Errorf("check wrote to the project: %d file(s) changed", n)
}
}

// The other half of #1434: exec agrees with check on the same project. The
// gated script is refused by the pre-flight before anything is written (it used
// to run statements 1-4 and fail on the fifth); the flows alone — the
// statements the reported StableId / CaseValues refusal came from — pass the
// pre-flight and are written.
func TestExecPreflight_AgreesWithCheckOnVersionGates(t *testing.T) {
_, mpr := copyV9Project(t)
exec := executor.New(io.Discard)
exec.SetBackendFactory(func() backend.FullBackend { return modelsdkbackend.New() })
if err := exec.Execute(&ast.ConnectStmt{Path: mpr}); err != nil {
t.Fatalf("connect: %v", err)
}
t.Cleanup(func() { _ = exec.Execute(&ast.DisconnectStmt{}) })

build := func(script string) *ast.Program {
t.Helper()
prog, errs := visitor.Build(script)
if len(errs) > 0 {
t.Fatal(errs[0])
}
return prog
}
preflight := func(prog *ast.Program) (string, string) {
var out bytes.Buffer
refusal := execPreflight(exec, prog, mpr, "s.mdl", false, false, false, deprecation.Warn, &out, false)
return refusal, out.String()
}

refusal, out := preflight(build(issue1434Gated))
if refusal == "" || !strings.Contains(refusal, "Nothing was written") {
t.Errorf("want the pre-flight to refuse the version-gated script, got %q\n%s", refusal, out)
}
if !strings.Contains(out, "create view entity requires Mendix 10.18.0+") {
t.Errorf("pre-flight output must name the gate:\n%s", out)
}

prog := build(issue1434Flows)
if refusal, out := preflight(prog); refusal != "" {
t.Fatalf("control: the flows were refused by the pre-flight: %s\n%s", refusal, out)
}
if err := exec.ExecuteProgram(prog); err != nil {
t.Errorf("exec refused the flows check passes on Mendix 9.24: %v", err)
}
}
10 changes: 10 additions & 0 deletions cmd/mxcli/cmd_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ return in an inserted fragment, ...) is refused by exec under "mdl 1;", and
check reports it as an MDL-V1-REBUILD error with exec's message. Under mdl 0
exec rebuilds the whole flow instead, and check reports the MDL-V1-REBUILD
warning exec prints. An "alter microflow|nanoflow" exec would refuse is MDL090.

Given a project it also applies exec's version gates: a statement the project's
Mendix version cannot store (create view entity below 10.18, send email below
11.13, ...) is MDL-VERSION01, with the message exec refuses it with. See
"show features" for the floors.
This is the verdict "mxcli diff" reports as "Refused:", computed by the same
code. A statement on a flow an earlier statement of the script changes, or
whose flow does not build until an earlier statement has run, is not predicted.
Expand Down Expand Up @@ -324,6 +329,11 @@ func runCheckFileAgainst(cmd *cobra.Command, filePath, against string) int {
// already stored that way: re-applying it changes nothing.
violations = executor.SettleStoredVoidReturnValues(violations, prog, executor.NewStoredVoidReturns(b))
violations = append(violations, executor.StoredTaskClaimViolations(prog, b)...)
// MDL-VERSION01: a statement the project's Mendix version cannot
// store. The gates are the ones exec's handlers refuse with, from
// one table (executor.statementVersionGates), so check -p no
// longer passes a script exec refuses for the version (#1434).
violations = append(violations, exec.CheckVersionGates(prog)...)
}
}
violations = executor.ApplyDeprecationPolicy(violations, depPolicy)
Expand Down
4 changes: 4 additions & 0 deletions cmd/mxcli/exec_preflight.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func execPreflight(exec *executor.Executor, prog *ast.Program, projectPath, scri
// project for MDL-WORKFLOW10 (ako/mxcli#943).
violations = append(violations, executor.StoredTaskClaimViolations(prog, b)...)
}
// A statement the project's Mendix version cannot store
// (MDL-VERSION01): the gate its handler applies when reached,
// after the statements before it (mendixlabs/mxcli#1434).
violations = append(violations, exec.CheckVersionGates(prog)...)
}
printPreflightViolations(violations, script, showInfo, w, color)
if summary := linter.Summarize(violations); summary.Errors > 0 {
Expand Down
1 change: 1 addition & 0 deletions docs-site/src/tutorial/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ This catches everything Level 1 catches, plus:
- Page layouts that don't exist in the project
- Association endpoints pointing to missing entities
- A plain `create` whose name already exists in the project
- A statement the project's Mendix version cannot store (`MDL-VERSION01`, e.g. `create view entity` below 10.18) — the same version gates `exec` refuses with

This is the check you should run before executing a script. It's fast (reads the project but doesn't modify it) and catches most mistakes.

Expand Down
38 changes: 38 additions & 0 deletions mdl-examples/bug-tests/1434-check-version-gates.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
mdl 1;
-- mendixlabs/mxcli#1434 — `mxcli check -p` passed a script that `mxcli exec`
-- then refused for the project's Mendix version, after the statements before
-- the refused one had run. check -p now applies exec's version gates (the same
-- statement-to-feature table, executor.statementVersionGates) as MDL-VERSION01,
-- and exec's pre-flight refuses the script before anything is written.
--
-- Verify (on a copy of a Mendix 9.x project, e.g. modelsdk/mpr/testdata/v1-project):
-- mxcli check 1434-check-version-gates.mdl -p app.mpr
-- -> statement 5: create view entity requires Mendix 10.18.0+ ... [MDL-VERSION01]
-- mxcli exec 1434-check-version-gates.mdl -p app.mpr
-- -> the same line, "Refusing to execute ... Nothing was written."
-- On a 10.18+ project both pass. Without the view entity, both pass on 9.24:
-- the flows are what the reported StableId / CaseValues refusal came from (#1410).

create module R1434;

create entity R1434.Item (
"Count": integer
);

create microflow R1434.MF_Empty ()
begin
return;
end;

create microflow R1434.MF_Decide ($Item: R1434.Item) returns boolean
begin
declare $Result boolean = false;
if $Item/"Count" > 10 then
set $Result = true;
end if;
return $Result;
end;

create view entity R1434.ItemView (
Name: String(200)
) as select cast('a' as String) as Name;
4 changes: 1 addition & 3 deletions mdl/executor/cmd_agenteditor_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,7 @@ func execCreateAgentEditorModel(ctx *ExecContext, s *ast.CreateModelStmt) error
// module. Nothing downstream catches an older project: the documents are
// custom blobs, so mxbuild does not validate them and the build stays green
// while Studio Pro cannot open the result.
if err := checkFeature(ctx, "agent_documents", "agent_model",
"create model",
"upgrade your project to Mendix 11.9+ and install the AgentEditorCommons module"); err != nil {
if err := checkVersionGates(ctx, s); err != nil {
return err
}

Expand Down
12 changes: 3 additions & 9 deletions mdl/executor/cmd_agenteditor_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ func execCreateConsumedMCPService(ctx *ExecContext, s *ast.CreateConsumedMCPServ
// module. Nothing downstream catches an older project: the documents are
// custom blobs, so mxbuild does not validate them and the build stays green
// while Studio Pro cannot open the result.
if err := checkFeature(ctx, "agent_documents", "agent_consumed_mcp_service",
"create consumed mcp service",
"upgrade your project to Mendix 11.9+ and install the AgentEditorCommons module"); err != nil {
if err := checkVersionGates(ctx, s); err != nil {
return err
}

Expand Down Expand Up @@ -116,9 +114,7 @@ func execCreateKnowledgeBase(ctx *ExecContext, s *ast.CreateKnowledgeBaseStmt) e
// module. Nothing downstream catches an older project: the documents are
// custom blobs, so mxbuild does not validate them and the build stays green
// while Studio Pro cannot open the result.
if err := checkFeature(ctx, "agent_documents", "agent_knowledge_base",
"create knowledge base",
"upgrade your project to Mendix 11.9+ and install the AgentEditorCommons module"); err != nil {
if err := checkVersionGates(ctx, s); err != nil {
return err
}

Expand Down Expand Up @@ -223,9 +219,7 @@ func execCreateAgent(ctx *ExecContext, s *ast.CreateAgentStmt) error {
// module. Nothing downstream catches an older project: the documents are
// custom blobs, so mxbuild does not validate them and the build stays green
// while Studio Pro cannot open the result.
if err := checkFeature(ctx, "agent_documents", "agent",
"create agent",
"upgrade your project to Mendix 11.9+ and install the AgentEditorCommons module"); err != nil {
if err := checkVersionGates(ctx, s); err != nil {
return err
}

Expand Down
19 changes: 3 additions & 16 deletions mdl/executor/cmd_alter_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,15 @@ func execAlterWorkflow(ctx *ExecContext, s *ast.AlterWorkflowStmt) error {
return mdlerrors.NewNotConnectedWrite()
}

// Version pre-check: workflows require Mendix 9.12+
if err := checkFeature(ctx, "workflows", "basic",
"alter workflow",
"upgrade your project to Mendix 9.12+ to use workflows"); err != nil {
// Version pre-check: workflows require Mendix 9.12+, and the added
// activities need their own features (statementVersionGates).
if err := checkVersionGates(ctx, s); err != nil {
return err
}

if vs := completionRuleViolations(alterWorkflowAddedActivities(s), workflowLocation(s.Name)); len(vs) > 0 {
return mdlerrors.NewValidationf("%s\n → %s", vs[0].Message, vs[0].Suggestion)
}
if workflowUsesAgentTask(alterWorkflowAddedActivities(s)) {
if err := checkFeature(ctx, "workflows", "ai_agent_task", "call agent microflow",
"AI agent tasks need Mendix 11.9 or later — use `call microflow` on older projects"); err != nil {
return err
}
}
if workflowUsesNotificationEvents(alterWorkflowAddedActivities(s)) {
if err := checkFeature(ctx, "workflows", "notification_events", "notification activities and notification boundary events",
"these need Mendix 11.11 or later — use `wait for notification` on older projects"); err != nil {
return err
}
}

// Same exec-side guard as CREATE WORKFLOW: ALTER had no reference validation
// at all, so an inserted activity could name a microflow that exists nowhere
Expand Down
4 changes: 1 addition & 3 deletions mdl/executor/cmd_datatransformer.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,7 @@ func execCreateDataTransformer(ctx *ExecContext, s *ast.CreateDataTransformerStm
return mdlerrors.NewNotConnectedWrite()
}

if err := checkFeature(ctx, "integration", "data_transformer",
"create data transformer",
"upgrade your project to 11.9+"); err != nil {
if err := checkVersionGates(ctx, s); err != nil {
return err
}

Expand Down
4 changes: 1 addition & 3 deletions mdl/executor/cmd_entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -942,9 +942,7 @@ func execCreateViewEntity(ctx *ExecContext, s *ast.CreateViewEntityStmt) error {
}

// Version pre-check
if err := checkFeature(ctx, "domain_model", "view_entities",
"create view entity",
"upgrade your project to 10.18+ or use a regular entity with a microflow data source"); err != nil {
if err := checkVersionGates(ctx, s); err != nil {
return err
}

Expand Down
9 changes: 1 addition & 8 deletions mdl/executor/cmd_messagedefinition_documents.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,6 @@ func messageCollectionsRemovedError(ctx *ExecContext, statement string) error {
"definition, and a mapping names one as Module.Name", statement, version))
}

// requireMessageDefinitionDocuments refuses the per-document form below 11.15.
func requireMessageDefinitionDocuments(ctx *ExecContext, statement string) error {
return checkFeature(ctx, "integration", "message_definition_document", statement,
"below Mendix 11.15 a message definition is an entry of a collection: "+
"create message definition collection Module.Name { definition Name for Module.Entity { … } };")
}

// projectStoresMessageDocuments reports whether the connected project stores
// message definitions as MessageDefinition2 documents.
func projectStoresMessageDocuments(ctx *ExecContext) bool {
Expand Down Expand Up @@ -123,7 +116,7 @@ func execCreateMessageDefinition(ctx *ExecContext, s *ast.CreateMessageDefinitio
if !ctx.ConnectedForWrite() {
return mdlerrors.NewNotConnectedWrite()
}
if err := requireMessageDefinitionDocuments(ctx, "create message definition"); err != nil {
if err := checkVersionGates(ctx, s); err != nil {
return err
}
existing := findMessageDefinitionDocument(ctx, s.Name.Module, s.Name.Name)
Expand Down
Loading
Loading