diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl b/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl index b6fb95babc..f78279461d 100644 --- a/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl +++ b/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl @@ -168,3 +168,6 @@ {"area": "cmd/mxcli", "date": "2026-10-05", "symptom": "`mxcli run --local --watch`: a model change made while the app is still booting is never built — the app keeps serving the model from before it, `--watch` logs no `Change detected`, and a waiter on the change times out. Re-running the exec does nothing (byte-idempotent: it writes nothing, so nothing re-triggers the watcher)", "cause": "watchAndApply took its baseline `last := sourceMTime(...)` after the boot finished, so an edit landing between the boot build's model read and the watch loop's start was already folded into the baseline", "file": "`cmd/mxcli/docker/runlocal.go` (`RunLocal` bootSource, `watchAndApply`)", "insight": "A watcher's baseline must be the source time the last build was MADE FROM, taken before that build — not 'now' when the watcher starts. Same class as the earlier fix that stopped moving the baseline to time.Now() after an apply. Measured with the run-lifecycle integration test: exec a page while the detached run's state says the boot build is in progress; with the old baseline `run wait` times out after 2m (no build #2), with the fix it reports `applied: build #2 via reload`. Exposed by making `run wait` race-free on source time: a generation-counting wait could not have told the change was lost.", "refs": ["cmd_run_lifecycle_integration_test.go"]} {"area": "cmd/mxcli", "date": "2026-10-06", "symptom": "`mxcli run --local --watch` on Mendix 10.24 / 11.6 dies after 5 minutes with `starting web client bundler: web client watcher timed out after 5m0s`, while the bundler's own log says `Bundling finished in 8999 milliseconds` — the bundle was done in 9s", "cause": "`parseBundlerStatus` (`cmd/mxcli/docker/webclient_watch.go`) only understood the modern-web-bundler protocol (`{\"protocol\":\"mx-modern-web-bundler\",\"type\":\"status\",\"payload\":{\"kind\":…}}`). The rollup-runner.mjs shipped with 10.24 and 11.6 predates it and writes `{\"code\":\"START\"|\"SUCCESS\"|\"ERROR\",\"payload\":…}` (ERROR payload an object with `message`, or a bare string when the config fails to load). Every status line was dropped as plain logging, so the first-build wait never saw a success", "file": "cmd/mxcli/docker/webclient_watch.go", "insight": "--watch had never worked on these versions: the parser has accepted only the new protocol since it was written (#349). Nothing exercised the watcher below 11.12 until the run-lifecycle integration test reached the nightly matrix — and it reached it only because ResolveMxForVersion substitutes ANY cached mxbuild for the test's default 11.13.0, so each matrix leg ran it against its own version. The runner's protocol is part of the mxbuild version contract: read tools/node/rollup-runner.mjs of the oldest supported version before assuming a stdout shape. Control: the lifecycle test on 11.6.8 with the parser reverted fails with the nightly's exact timeout (MXCLI_WEB_CLIENT_TIMEOUT=60s makes it fail in a minute); with the fix it passes", "refs": ["nightly 2026-10-06 (ako/mxcli run 37437089065, mendixlabs/mxcli run 37437605368)"]} {"area": "cmd/mxcli", "date": "2026-10-06", "symptom": "`mxcli syntax rename` printed `Unknown topic: rename` and `mxcli syntax --json` had rename only for attributes, values and definitions, while `RENAME MICROFLOW TO ;` passed `mxcli check` and `mxcli help rename` documented it; an agent concluded MDL could not rename a microflow and copied, re-pointed and deleted it by hand", "cause": "The renameStatement rule (MDLParser.g4) and its executor (mdl/executor/cmd_rename.go) never got a SyntaxFeature in cmd/mxcli/syntax/; the only discovery path was the cobra `rename` subcommand's Long text, which `syntax` does not read. BySegmentMatch could not rescue it because no registered path has a `rename` segment", "file": "cmd/mxcli/syntax/features_misc.go", "insight": "Same class as the TABCONTAINER gap (widget_keywords_drift_test.go): an agent treats absence from `mxcli syntax` as absence from the language, so a statement the grammar accepts but the registry omits is effectively missing. Fixed with a `rename` topic plus a guard (rename_topic_test.go) that reads the renameTarget rule from the .g4 and requires every alternative in the topic, and see_also links from microflow/page/entity/move. The CLI subcommand and the MDL statement diverge (MDL also takes JAVA ACTION and WORKFLOW); the topic says so rather than implying parity. Control: with the topic reverted the built binary prints `Unknown topic: rename` verbatim and TestSyntaxRenameTopic fails; dropping the RENAME WORKFLOW line fails the grammar guard. A sweep for other top-level statements with no topic would be the cheap next step", "refs": ["mendixlabs/mxcli#1318"]} +{"area": "cmd/mxcli", "date": "2026-10-08", "symptom": "Main's build-and-test fails intermittently in the run-lifecycle tests, several at once in one run and green on the next: TestRunStop_LeavesNoOrphanBehind (\"fake run has 0 session members, want the leader and its child\"), TestRunStop_ReapsTheLeftoversOfAKilledRun, TestSessionMembers_FindsOrphanedGrandchild (\"members … = [], want the leader and its sleep\"). Not reproducible locally (20/20 green). In real use the same path makes `run stop` / `run status` miss a live run's processes.", "cause": "docker.SessionMembers' pid-reuse guard rebuilt a process's start time as btime + starttime/USER_HZ and dropped it if that fell more than 1s before notBefore. btime in /proc/stat is whole seconds (up to 1s early) and starttime whole 10ms ticks (up to 10ms early), so the rebuild can be ~1.01s early: a process started a few ms after notBefore was dropped. frac(btime) is fixed for a machine's uptime, so a CI VM booted late in a second failed every such test in the run, and others never did; a process started a few ms later (the leader's sleep) survived.", "file": "`cmd/mxcli/docker/session_linux.go` (`startedBefore`, `startSlack` = 2s)", "insight": "A flake that fails a whole family of tests in one run and none in the next points at per-machine state, not timing within the test: here the fractional second of the VM's boot. Make the comparison a pure function and pin the worst case (boot at X.9999, start 9.9ms into a tick, 1ms after notBefore); the test then fails deterministically against the old slack. Rebuilding a wall-clock time from btime + ticks loses up to 1s + 1 tick; any slack must exceed both.", "refs": ["https://github.com/ako/mxcli/actions/runs/37733157296", "https://github.com/ako/mxcli/actions/runs/37688181946"]} +{"area": "cmd/mxcli", "date": "2026-10-08", "symptom": "`mxcli test --local` had no --db-type, so on a machine without PostgreSQL it stopped with \"Error: ensuring database: no local PostgreSQL superuser available to create the role/database\" while `run --local --db-type hsqldb` served the same project. Passing the type through naively then failed with \"database not reachable at 127.0.0.1:5432\".", "cause": "The headless boot (docker.LocalAppOptions) never learned the file database: applyDefaults filled PostgreSQL's host/user/password into every config, StartLocalApp pinged that host, and testrunner hardcoded EnsureDB=true. run --local had the file-database branches in its own applyDatabaseDefaults/StartLocalRun, a second boot path that the test runner does not use. DBConfig.IsFileBased keys on the runtime spelling \"HSQLDB\", so the canonical \"hsqldb\" passed straight through also silently skipped every file-based branch.", "file": "`cmd/mxcli/docker/localapp.go` (`applyDefaults`, `prepareLocalAppDatabase`), `cmd/mxcli/testrunner/localapp_options.go` (`dbConfig`, `ResolveTestDBType`), `cmd/mxcli/cmd_test_run.go`", "insight": "Two boot paths (StartLocalRun for run --local, StartLocalApp for test --local) each own their database defaults, so a capability added to one is absent from the other until someone asks; grep both when adding a database option. Keep the _test scratch suffix for HSQLDB as well: the name selects the database files under deployment/data/database/hsqldb/, and the unsuffixed ones are the dev loop's (shared data + HSQLDB file lock). A test that asserts EnsureDB is refused must match the refusal's message: EnsureDatabase itself fails on a host without PostgreSQL, which passes an err != nil check for the wrong reason.", "refs": ["https://github.com/mendixlabs/mxcli/pull/1274", "https://github.com/mendixlabs/mxcli/issues/1292"]} +{"area": "cmd/mxcli", "date": "2026-10-08", "symptom": "Studio Pro (11.12/11.13) shows conflicts the change pane does not show, history entries with BranchName/ModelerVersion \"(unknown)\", and crashes on open (\"Unable to find 'system' property in 'system'\") after an agent committed and branched with plain git on a Team Server project", "cause": "Studio Pro keeps per-commit metadata in git notes under refs/notes/mx_metadata (written on every Studio Pro commit, pushed with it); git commits had none, so Studio Pro 11.13 back-fills a placeholder (HasModelerVersion:false) for each after every background fetch (~3 min), and that fetch force-replaces the local notes ref, discarding notes an agent wrote but did not push. Branches made with git checkout -b have no upstream (ako/mxcli#972).", "file": "cmd/mxcli/gitnote.go", "fix": "`mxcli git note` computes the note from the commit (the .mpr Unit index + mprcontents at commit and parent, canon.Equal to skip $ID-only churn, a deleted module listed once, Moved from a container change, the project root listed on a version change); the teamserver-git skill teaches server branches and pushing refs/notes/mx_metadata with the branch.", "insight": "The ground truth was in the user's own repository, not in any doc: `git log refs/notes/mx_metadata` separates Studio Pro's notes (libgit2 'git_note_create', or the git CLI in 11.13 within a second of a host-timezone commit) from agent ones, and the notes reflog shows the 3-minute fetch-and-backfill cycle. Comparing a generator against those notes commit by commit settled every rule; an ordinary commit must match exactly, and the only residual difference is upgrade commits, where Studio Pro also records names from its editing session (a rename before commit) that no diff can recover.", "refs": ["mendixlabs/mxcli#1337", "ako/mxcli#972"]} diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index 5cdf70b25d..c4b36932f1 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -884,3 +884,9 @@ {"area": "mdl/executor", "date": "2026-10-07", "symptom": "A Microflow-typed Java action argument with `)` on the next line — `call java action M.DoSomething(Flow = M.SUB_Target⏎ );` — reported on 0.23/0.24 as passing check/exec and then failing mx check with CE1613 \"The selected microflow 'MyModule.SUB_Target⏎' no longer exists\"; on current main the same script is REFUSED by both `check --references` and exec as \"parameter Flow is of type Microflow, which takes a microflow name ('Module.Microflow'), not MyModule.SUB_Target\" — the message names a valid microflow name. `Flow = empty⏎)` was refused too (or, without the guard, stored the text `empty` as a microflow reference).", "cause": "buildCallArgumentList keeps the whitespace between an argument's last token and `)` by wrapping the value in ast.SourceExpr (expression round-trip). The Microflow-typed path type-switched on the wrapper: microflowParamArgRefusal (#1210) and isEmptyJavaActionArgument saw a SourceExpr, not a QualifiedNameExpr / LiteralExpr. The original stored-newline form had already been removed by #898 (mendixexpr.String trims a SourceExpr's trailing whitespace), which is why the reported symptom no longer reproduces and the bug now surfaces as a false refusal instead.", "file": "`mdl/executor/javaaction_flow_param.go` (`javaActionArgumentValue`, `microflowParamArgRefusal`), `mdl/executor/cmd_microflows_builder_calls.go` (`isEmptyJavaActionArgument`, Microflow-typed mapping), `mdl/executor/javaaction_flow_param_whitespace_test.go`", "insight": "Any executor decision that type-switches on a call/member argument (`arg.Value.(type)`) must unwrap ast.SourceExpr first: the visitor wraps the value only when whitespace follows it, so the hand-built QualifiedNameExpr/LiteralExpr in existing unit tests never exercises the wrapped form and every such switch passes its tests. Build test arguments with visitor.Build from real MDL, with and without a line break before `)`. Wrong turn to skip: the issue blames the lexer/name text — the name is parsed cleanly; the trailing whitespace is a deliberate SourceExpr suffix, and stripping it in the visitor would break describe round-trip layout (#898). Control: reverting the fix with #1210's guard stubbed no longer reproduces the newline on this tree (#898 trims it), so the live regression is the refusal, which the reverted fix reproduces in both the builder test and the check/exec agreement test.", "refs": ["mendixlabs/mxcli#1282", "mendixlabs/mxcli#1210", "#898"], "ce": ["CE1613"]} {"area": "mdl/executor", "date": "2026-10-07", "symptom": "`mxcli check script.mdl -p app.mpr --references` printed \"Check passed!\" for `grant read * on entity System.Nope to M.R`, for a grant to `M.NopeRole`, and for every other GRANT/REVOKE form naming a missing entity, document, member or role; also for `create user role R ( ModuleRoles: (M.NopeRole) )`, `alter user role … add module roles`, and a demo user with an unknown user role or entity (measured on Evora Factory Management, 10.24.15). exec refused the grants after the earlier statements were written; the user-role and demo-user shapes it wrote unresolved (CE1613 at build).", "cause": "No validate path resolved a security statement's names. validateWithContext's switch had no case for any of them (only the role-free validateCrossModuleGrant ran), and the user-role/demo-user executors store module and user role names verbatim without resolving them, so neither check nor exec stood between a typo and the model.", "file": "`mdl/executor/validate_grant_refs.go` (`validateGrantReferences`), wired into `validateProgramWithWarnings` in `validate.go`; test `check_grant_references_pedapp_test.go`", "insight": "Two things made the obvious version wrong. (1) What check refuses has to be what exec refuses, form by form: exec refuses an unknown role on a GRANT and on an entity REVOKE, but a document REVOKE (and `alter user role … drop module roles`) of an unknown role is a reported no-op, which keeps cleanup scripts re-runnable after the role is dropped — refusing it would make check louder than exec. Likewise every System entity is refused by exec (refuseSystemEntityGrant), so `grant … on entity System.User` must be refused by check too, while System MODULE roles in a user role must pass. (2) A script's own effects are not only its CREATE statements: creating a microflow/nanoflow/page in a module with no module roles auto-creates `.User` (defaultDocumentAccessRoles), and doctype-tests/02b grants to exactly that role — the first version flagged 10 statements there. Walking the program in statement order (not the whole-program scriptContext) gives the forward-reference hint for free. Performance trap: reading every module's security costs ~5s on Evora; read only the modules a statement names (~0.1s).", "refs": ["mdl-examples/bug-tests/check-grant-unknown-reference-refused.mdl", "mdl-examples/bug-tests/check-grant-unknown-reference.mdl", "ako/mxcli#1020"]} {"area": "mdl/executor", "date": "2026-10-07", "symptom": "A DataGrid 2 column's `Visible:` expression is stored as `true` (always visible) for every spelling but the old quoted one: `Visible: $showPrices`, `Visible: if … then … else …`, `visible: not(…)`, `Visible: [cond]`. check clean, exec reports \"Created page\", describe shows no Visible. `alter page … set (Visible: ) on grid column(…)` is refused as \"column property VisibleIf not found\"; `insert` of such a column drops it, and drops `Visible: false` too.", "cause": "The visitor lowers every expression spelling of Visible to the key VisibleIf (the page-widget conditional-visibility key) and keeps only plain values under Visible. The column writers read only visible/Visible: the widget engine by schema key + types.ItemPropertyAliases (no VisibleIf alias), the ALTER mutator's resolveColumnPropertyKey the same table, and widgetobj.BuildDataGrid2Column (ALTER insert/replace via buildColumnSpecFromAST) the exact key \"Visible\" as a string only, so a bool false fell to the default too. The mirror image of widget-visible-expression.mdl, where page widgets read VisibleIf and dropped Visible.", "file": "`mdl/types/widget_item_aliases.go` (`visible` ← `VisibleIf`), `mdl/executor/widget_engine.go` (WidgetDefGeneratorVersion 18), `mdl/executor/cmd_pages_builder_v3_widgets.go` (`columnSpecProperties`), `mdl/executor/cmd_pages_describe_output.go` (column Visible via widgetConditionMDL), `mdl/executor/validate_column_visible_scope.go` (MDL-WIDGET43)", "insight": "When the visitor lowers one MDL property to two AST keys by value shape, every consumer must read both — grep the consumers of the key the visitor writes, not of the property name. Making the value persist exposed what the drop had hidden: a column's visible expression has no row object (no dataSource in the widget schema, unlike columnClass), so every $currentObject example became CE0117 at build — measured on 11.14.0 — and needed a check rule (MDL-WIDGET43) in the same change. The widget def's per-item `dataSource` field is the signal for whether $currentObject exists.", "refs": ["mdl-examples/bug-tests/datagrid-column-visible-expression.mdl", "mdl-examples/bug-tests/widget-visible-expression.mdl"]} +{"area": "mdl/executor", "date": "2026-10-08", "symptom": "`mxcli check` refuses a call argument written the way Studio Pro writes an associated object \u2014 `$PageHelper/AgentCommons.PageHelper_Agent/AgentCommons.Agent` \u2014 with MDL049 \"passes an association path \u2026 CE0117\". 42 untouched Evora Factory Management microflows (Mendix 10.24, 0 errors under mx check) were refused, so exec would not re-apply their own describe output without --no-check", "cause": "exprIsAssociationObjectPath flagged ANY path whose last segment is module-qualified. Mendix paths alternate association / entity, so a qualified last segment after an association is the ENTITY step that names the object \u2014 a valid object value, not a bare association. The rule's premise (ledger #43/#44) was measured only on the bare `$obj/Mod.Assoc` form", "file": "`mdl/executor/validate_microflow.go` (`checkAssociationObjectArgs`/`exprIsAssociationObjectPath`, MDL049)", "insight": "Before trusting a syntactic rule, collect the shapes the real corpus stores: Studio Pro never writes the bare form the rule was built on, it writes `Assoc/Entity`. Measured on mxbuild 10.24.15 and 11.13.0, one microflow per argument: `$A/M.A_B` CE0117 (still refused), `$A/M.A_B/M.B` 0, ref set `$A/M.A_Bs/M.B` into a List param 0, two hops with entity steps 0, `$A/M.A_B/M.B/M.B_C` 0; reverse traversal and ref set into an object param are CE0117 too but need the domain model to tell apart, so they are left to mxbuild. The old Fix: line (`retrieve $x from /Entity`) also handed back a path the retrieve could not take", "refs": ["mdl-examples/bug-tests/check-fp-assoc-entity-path.mdl", "mdl-examples/bug-tests/ledger-44-assoc-path-as-value.fail.mdl"], "ce": ["CE0117"], "rules": ["MDL049"]} +{"area": "mdl/executor", "date": "2026-10-08", "symptom": "`describe microflow` prints `call microflow M.F(…, TraceID = , …)` / `(…, ID = )`, which does not parse (`no viable alternative at input 'TraceID=,'`) — 10 of 1,875 Evora flows, all calls into marketplace modules (GenAICommons, ConversationalUI) whose callee gained a parameter after the caller was written", "cause": "Studio Pro keeps a parameter mapping per callee parameter and stores an argument field left blank as `Argument: \"\"`. The call microflow / call nanoflow printers ran it through describeExpr and printed nothing after `=`. No MDL spelling existed for it: `Param = empty` stores the Mendix value \"empty\" (a different document Studio Pro also writes — `SystemPrompt = empty` sits in the same call) and omitting the argument writes no mapping, so neither round-trips; `Param = nothing` silently stored the text \"nothing\"", "file": "mdl/executor/cmd_microflows_format_action.go", "insight": "An empty stored expression is a third state, distinct from the value `empty` and from absence, and a describer that conflates it with either cannot be Unchanged under ADR-0008 — check the stored bytes of each candidate spelling before choosing one. It is also an ERROR state: mx check 10.24 reports CE0127 \"Missing argument\" for it, which is why the doctype example lists CE0127 as known. The spelling is `Param = nothing` (grammar alternative in callArgument, ast.BlankExpr, renders \"\"), refused by the visitor outside call microflow / call nanoflow. Java actions keep their older, lossy convention (\"\" describes as `empty`, which re-stores \"empty\" when the action resolves) — not changed here", "refs": ["mdl/executor/call_argument_blank_test.go", "mdl-examples/bug-tests/describe-call-blank-argument.mdl"]} +{"area": "mdl/executor", "date": "2026-10-08", "symptom": "`describe microflow AgentCommons.PromptToUse_ApplyVariable` (Evora Factory Management, MPR v1, Mendix 10.24) runs past a 5-minute timeout, while the app's other 1,800+ flows — including ones with 4x its McCabe and 5x its activities — describe in seconds. The flow is small: 3 decisions, 2 merges, no loops", "cause": "Not graph complexity. Two of its decisions call a rule (`if AgentCommons.Rule_PromptToUse_ContainsVariable(...)`), and every build asks backend.IsRule for each `if Module.Name(...)`. The canonical describe rebuilds the flow once per derived-layout round (9 here), and the layout read cache did not memoise IsRule. modelsdk IsRule resolved the module of EVERY rule before comparing names; each resolution (moduleNameFor) listed the whole project once, then the modules once per folder level. On MPR v1 every listing reads every unit's contents from SQLite (137 MB), so one IsRule took 53 s on a 29-rule app: 2 splits x 9 rounds x 53 s", "file": "`mdl/backend/modelsdk/microflow.go` (IsRule: compare bare name before resolving the module), `mdl/backend/modelsdk/domainmodel_write.go` (moduleNameFor: list modules once, not per ancestor; moduleNameListings counter), `mdl/executor/cmd_microflows_derived_layout_backend.go` (layoutCheckBackend.IsRule memo); tests TestIsRule_ResolvesOneModule, TestDescribe_DerivedLayoutAsksIsRuleOncePerName; repro `mdl-examples/bug-tests/describe-rule-split-timeout.mdl`", "insight": "When one document is pathologically slow, compare it with the LARGEST documents before theorising about its graph: this one was among the smallest, so the cost had to be a per-construct backend lookup multiplied by rounds, not a traversal. A goroutine dump (SIGQUIT) at a few points showed the same IsRule frame every time, and timing a single call (53 s) settled it before any profile. The multiplier chain is the lesson: per-statement lookup x rebuild rounds x per-candidate project listing x per-ancestor listing — each factor harmless alone, and the derived-layout read cache must memoise EVERY backend read the builder makes, not just the ones the first profile showed. Filter by the cheap key (bare name) before the expensive one (module resolution). Tests count calls/listings, not wall time; reverting gives 32 IsRule calls over 8 rounds (want 1) and 12 project listings for one lookup (want 2). Measured: >5 min (killed) -> 6-10 s, byte-identical output; describe -> exec -> Unchanged, MPR byte-identical"} +{"area":"mdl/executor","date":"2026-10-08","symptom":"`mxcli check --references` printed \"Check passed!\" (exit 0) for `grant create on entity MyModule.NoSuchEntity to MyModule.User` and a grant to `MyModule.NoSuchRole`; `exec` wrote the valid grant above them, then failed with \"Error: entity not found: MyModule.NoSuchEntity\" / \"module role not found: MyModule.NoSuchRole\" (reported on v0.25.0, Mendix 11.12.4). Issue mendixlabs/mxcli#1333","cause":"Already fixed before the report reached main: 6731d9df (validateGrantReferences) landed the day the issue was filed, after v0.25.0 was cut. Nothing in the executor changed for #1333.","file":"`mdl/executor/validate_grant_refs.go` (fix, 6731d9df); test added `cmd/mxcli/exec_preflight_grant_refs_test.go`","insight":"**Run the reported script on current main before touching code** — and, when it passes, build the parent of the suspected fixing commit and run it there too: that one control turns \"cannot reproduce\" into \"fixed by \", and `git tag --contains ` tells the reporter which release carries it. The pre-fix binary reproduced the report verbatim (Check passed, exit 0; exec writes grant 1, then entity not found). What was still missing was the issue's actual harm — a valid statement written before the bad one — pinned at the layer it lives in: the existing tests drive the reference check through `agreeCheck`, not `execPreflight`, so nothing asserted exec refuses the MIXED script before writing. The new test does, and fails when the `validateGrantReferences` line in `validate.go` is commented out. Repro `mdl-examples/bug-tests/1333-grant-missing-entity-or-role.mdl`.","refs":["mdl-examples/bug-tests/1333-grant-missing-entity-or-role.mdl","cmd/mxcli/exec_preflight_grant_refs_test.go"]} +{"area":"mdl/executor","date":"2026-10-08","symptom":"Published OData entity exposing a CROSS-MODULE association by bare name writes a `ODataPublish$PublishedAttribute` (Attribute 'Od.Child.Child_Parent') instead of a `PublishedAssociationEnd`; `mxcli check`/`exec` pass, `mx check` reports CE1613 \"The selected attribute 'Od.Child.Child_Parent' no longer exists.\" Same-module associations fine","cause":"`lookupEntityMembers` read only `dm.Associations` of the published entity's module; a cross-module association is a `DomainModels$CrossAssociation` in `dm.CrossAssociations` (FROM entity's module, TO entity by name), so the member fell through to the attribute fallback. From the TO side it is in ANOTHER module's domain model, and the writer's `qualifyAssociationName` prefixed the published entity's module (Op.Child_Parent -> CE1613 \"selected association\")","file":"`mdl/executor/cmd_odata.go` (`lookupEntityMembers`, new `addCrossAssociationsTo`, `assocMembership.QN`, `astEntityDefToModel` sets `member.Name` to the qualified association)","insight":"**Any resolver that walks `dm.Associations` is blind to cross-module ones — grep for it, not for the symptom.** `cmd_associations.go` loops both lists everywhere; a new resolver that loops one is this bug. Two halves, each proven separately on a real 11.14.0 app: no cross lookup -> 2x CE1613 'selected attribute' (both sides); cross lookup but writer-side qualification -> 1x CE1613 'selected association Op.Child_Parent' from the TO side. **The writer cannot qualify an association from the published entity** — the association's module is the FROM entity's, so qualify in the executor where the domain model is known. Guard `TestPublishCrossModuleAssociation_IsAssociationEnd` (same-module control passes on unfixed code); repro `mdl-examples/bug-tests/1335-odata-cross-module-association.mdl`","refs":["mendixlabs/mxcli#1335","#50","#1209"],"ce":["CE1613"]} +{"area": "mdl/executor", "date": "2026-10-08", "symptom": "A DataGrid 2 column bound to `changedBy/Name` (entity declares `changedBy: AutoChangedBy`) passed `check --references` and `exec`; `describe` echoed it back; mxbuild failed `[CE1613] \"The selected attribute 'MyFirstModule.Item.changedBy/Name' no longer exists.\"` Same for `owner/Name`. Issue mendixlabs/mxcli#1338 (11.12.5)", "cause": "changedBy and owner are the ASSOCIATIONS System.changedBy / System.owner to System.User, and no domain model lists them — an entity carries them only as the HasChangedBy / HasOwner flags on its generalization root. resolveAssociationAttributePath qualified the hop as `MyFirstModule.changedBy`, associationEndpoints found nothing, and every caller (column, input widget, dynamic-text param) fell back to the flat attribute path", "file": "`mdl/executor/cmd_pages_builder_v3.go` (systemMemberAssociation, tried after the modelled lookup in resolveAssociationAttributePath)", "insight": "The XPath checker already knew this (`xpathSystemAssociations` in validate_widget_member_refs.go) — the page resolver was a second, uninformed resolver for the same names (see duplicate-resolver-drift). Try the modelled association FIRST so a user association that happens to be named `owner` still wins, and gate on systemMemberStoredOnChain so an entity without AutoChangedBy gets no invented hop. The stored form is an ordinary IndirectEntityRef step {System.changedBy → System.User} with attribute System.User.Name: measured on 11.12.5, fixed build `mx check` clean, pre-fix binary reproduces the two CE1613s verbatim, and describe → exec reports `Unchanged`. Still open, separately: `check --references` validates NO association hop in a column path, so `Nope_Assoc/Name` also passes check and writes a flat dead path.", "refs": ["mdl-examples/bug-tests/1338-datagrid-column-system-association.mdl", "mdl/executor/cmd_pages_builder_system_assoc_test.go"]} diff --git a/.claude/skills/fix-issue/findings/mdl-grammar.jsonl b/.claude/skills/fix-issue/findings/mdl-grammar.jsonl index e416bd9187..2d5cccf58e 100644 --- a/.claude/skills/fix-issue/findings/mdl-grammar.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-grammar.jsonl @@ -73,3 +73,4 @@ {"date": "2026-10-02", "area": "mdl/grammar", "symptom": "ako/mxcli#533, #569: a positional data-source argument `listview lv (datasource: microflow M.DS($Value))` reports `no viable alternative at input 'datasource'` at the property keyword, plus two cascade errors \u2014 not at the argument, and naming nothing; the create-page skill (widgets.md) and `syntax page.datasource` taught the positional form", "cause": "A widget property is predicted with full-LL lookahead over the whole `datasource: \u2026` alternative, so a failure anywhere inside it is reported at the property's first token. No argument rule had a positional alternative", "file": "mdl/grammar/domains/MDLPage.g4 microflowArgV3; mdl/grammar/domains/MDLMicroflow.g4 callArgument, showPageArg; mdl/visitor/visitor_argument_binding.go refusePositionalArgument", "fix": "Each argument rule gets a last `| expression` alternative that the visitor always refuses, at the argument's own line:column, naming `Param = ` and the syntax topic. Last so the ambiguity with `Param = e` (`=` is equality in an expression) resolves to the named alternative. call workflow's `(callArgumentList | VARIABLE)` reordered to `(VARIABLE | callArgumentList)` so `call workflow M.W($ctx)` keeps meaning the context variable. Docs corrected to `M.GetData(Param = $Param)`", "insight": "To move an ANTLR error to where the mistake is, accept the mistake in the grammar and refuse it in the visitor \u2014 the error-alternative pattern. A malformed named argument (`Param = )`) still reports at `datasource`: the general prediction-level case is not fixed by this", "test": "mdl/visitor/visitor_argument_binding_test.go TestPositionalArgumentIsRefusedAtTheArgument"} {"date": "2026-10-05", "area": "mdl/grammar", "symptom": "mendixlabs/mxcli#1288: `add $Row to $Material/MyFirstModule.SkuRowsOfMaterial;` -> `Parse error: line 9:23 mismatched input '/' expecting ';'`. The only spelling left was `change $Material (Assoc = $Row)`, which ASSIGNS the reference set: in a loop of three appends two rows were orphaned with 0 errors from check, mx check, runtime.log and a boolean-return test. Read side had the same defect: DESCRIBE of a Studio Pro Add/Remove member printed `change $Obj (Assoc = $X)`, so a describe -> exec round trip turned an append (and even a Remove) into a set assignment", "cause": "addToListStatement/removeFromListStatement only took `TO VARIABLE`, so the Change object activity's Add/Remove member types (MemberChangeTypeAdd/Remove, already in sdk/microflows and round-tripped by the codec) had no writer; formatAction ignored MemberChange.Type", "file": "mdl/grammar/domains/MDLMicroflow.g4 associationMemberTarget; mdl/visitor/visitor_microflow_actions.go; mdl/executor/cmd_microflows_builder_actions.go addAssociationMemberChange; mdl/executor/cmd_microflows_format_action.go associationMemberChangeStatement; cmd_microflows_builder_graph.go collect{List,Object}InputVariables; validate_microflow_retrieve_single.go listUseOf", "fix": "ADD expr TO $Obj/Module.Assoc and REMOVE $X FROM $Obj/Module.Assoc [commit] [refresh] -> AddToListStmt/RemoveFromListStmt with Association set -> ChangeObjectAction with one MemberChange{Type: Add|Remove}, built through addChangeObjectAction so the member resolves exactly like a CHANGE member. Refused on an attribute or a plain Reference. DESCRIBE prints a sole Add/Remove member back in that form; an Add/Remove mixed with other members gets a trailing `-- WARNING` instead of a silent `=`. The association form's $Obj is an object input, not a list input (else MDL-RETRIEVE01 and the retrieve-typing walkers misfire)", "insight": "Measured on 11.12.2 (mx check): Add on a ReferenceSet = 0 errors; Add on an attribute AND on a plain Reference = CE0033 \"The 'Type' property of this member cannot be 'Add'.\" The issue's claim that Reference was equally affected is about CHANGE set semantics, not about Add being valid there. When an AST statement gains a second meaning for an existing field (List = object variable here), grep every type-switch on the statement: the list-input walker and the single-object rule both read List as a list. And whenever a writer gains an enum value, check the describer prints it — the codec round-tripped Type all along; only formatAction dropped it, which is the silent half of this bug", "test": "mdl/visitor/visitor_add_to_association_test.go; mdl/executor/cmd_microflows_builder_add_to_association_test.go; cmd_microflows_format_action_test.go TestFormatAction_ChangeObject_AssociationAddRemove; mdl-examples/bug-tests/1288-add-remove-association-member.mdl", "refs": ["mendixlabs/mxcli#1288"], "ce": ["CE0033"]} {"date": "2026-10-07", "area": "mdl/grammar", "symptom": "mendixlabs/mxcli#1281: two predicates on one XPath step fail to parse. `retrieve $x from M.Location where [M.A/M.TenantUser[Status = 'Active'][AnonymizedAt = empty]/M.B = '[%CurrentUser%]']` -> `mismatched input '/' expecting ';'`; ending the path on the step -> `missing ']' at '['`. One predicate with `and` passed, and Mendix accepts the consecutive form (reporter: access rules, mx check 0 errors on 11.12.4).", "cause": "xpathStep was `xpathStepValue (LBRACKET xpathExpr RBRACKET)?` and ast.XPathStep had a single Predicate field, so every consumer (serializers, the rename rewriter, the widget/retrieve member-ref walkers) assumed one.", "file": "mdl/grammar/domains/MDLPage.g4 xpathStep; mdl/ast/ast_expression.go XPathStep.Predicates; mdl/visitor/visitor_xpath.go buildXPathStep; mdl/visitor/visitor_page_v3.go xpathPathToString; mdl/executor/cmd_microflows_helpers.go; mdl/executor/validate_microflow.go; mdl/executor/validate_widget_member_refs.go; mdl/xpathrefs/rewrite.go", "fix": "`*` on the predicate group; Predicates []Expression in source order; every consumer loops. Kept separate rather than folded into one `and`: `[reversed()]` is also a step predicate and is not a condition, and folding would rewrite the user's constraint on describe.", "insight": "The stored-constraint reader (ParseXPathConstraint) did NOT report this as a failure: ANTLR recovery consumed the stray `[b]` up to EOF, so the EOF check from #772 passed and the round trip silently returned `Entity[a]` — dropping the second predicate. A grammar-level parse error with error listeners removed can still look like a full parse; test via visitor.Build (errors surfaced) as well as the listener-free helper. The ast field rename made the compiler enumerate every consumer — cheaper than grepping for walkers. mx check control: the scaffold's own `= '[%CurrentUser%]'` against a non-User entity gave CE0161 for BOTH the two-predicate and the `and` form; only the paired run showed it was the scaffold.", "test": "mdl/visitor/visitor_xpath_test.go TestXPath_ConsecutiveStepPredicates; mdl-examples/bug-tests/1281-consecutive-xpath-step-predicates.mdl"} +{"date": "2026-10-08", "area": "mdl/grammar", "symptom": "`describe microflow` output fails to parse with `missing '(' at ')'` / `missing '(' at '$X'` on an arithmetic expression: `offset $Pagination/PageSize*($Pagination/Offset-1)` (Viewer3D.__GetMxModelDocumentList) and `(($I/OEE-$B/OEE) div $B/OEE)*100` (FactoryManagement.ACT_Scenario_Compare), 2 of 1,875 Evora flows. Looks like a `div` / `offset` / parenthesis problem; it is neither", "cause": "The lexer's HYPHENATED_ID (`ID_START (ID_BODY* '-')+ ID_BODY*`, there for `starts-with` and Atlas icon names) matched `Offset-1` (digit after the hyphen) and `OEE-` (trailing hyphen, nothing after it), so the member and the minus became one name token, which the parser then read as a function name and wanted a `(` after. Studio Pro stores expressions as typed, so an unspaced minus is ordinary stored text and describe prints it verbatim", "file": "mdl/grammar/MDLLexer.g4", "fix": "Every hyphen in HYPHENATED_ID must be followed by a letter or underscore: `ID_START ID_BODY* ('-' ID_START ID_BODY*)+`. A hyphen before a digit, `$`, space or nothing is now MINUS", "insight": "Bisect a parse error to the TOKEN before reasoning about the rule it names: `$P/Name-1` vs `$P/Name -1` differing is the whole diagnosis, and the error text (`missing '('`, at a `div` or inside `offset`) pointed at three wrong places. Verbatim describe output is exactly where storage form meets the lexer, so any lexer rule that can swallow an operator is a describe round-trip bug waiting for an unspaced expression. Not fixed: a hyphen followed by a LETTER (`$a/Total-round($x)`, `$a/X-Y`) still lexes as one name; no Evora flow has one", "test": "mdl/executor/expression_unspaced_minus_test.go (asserts the stored text is byte-identical, not just that it parses); mdl-examples/bug-tests/describe-unspaced-minus-expression.mdl"} diff --git a/.claude/skills/fix-issue/findings/modelsdk.jsonl b/.claude/skills/fix-issue/findings/modelsdk.jsonl index 3cdc21513f..ec6b8b5ce2 100644 --- a/.claude/skills/fix-issue/findings/modelsdk.jsonl +++ b/.claude/skills/fix-issue/findings/modelsdk.jsonl @@ -25,3 +25,4 @@ {"area": "modelsdk/widgets", "date": "2026-09-26", "symptom": "CE0463 \"The definition of this widget has changed\" on every page carrying a pluggable widget built from its .mpk whose action properties declare `` (Signature 2.1.0, Calendar 2.6.0 on 11.12.2) — even with no action configured. `mx update-widgets` clears it", "cause": "The .mpk parser had no field for `` (modelsdk/widgets/mpk/mpk.go xmlProperty/PropertyDef), and createDefaultValueType hardcoded `ActionVariables: [2]`; reconcileValueTypesFromMPK never touched the list. The typed gen class (CustomWidgets$WidgetActionVariable) existed but the map-based template pipeline never fed it", "file": "modelsdk/widgets/mpk/mpk.go (ActionVariable, toActionVariables), modelsdk/widgets/augment.go (buildActionVariablesArray, actionVariablesMatch, createDefaultValueType, reconcileValueTypesFromMPK); tests actionvariables_test.go in both packages; example mdl-examples/bug-tests/1200-mpk-action-variables.mdl", "insight": "Third instance of the same shape after #716 (onChange) and #956 (defaultType): a widget.xml attribute/element that is part of the DEFINITION, never parsed, written as its empty default. Cheapest audit: diff every key Studio Pro stores on a WidgetValueType in the embedded templates against what createDefaultValueType derives from the .mpk — the embedded combobox.json already held the correct ActionVariables entry, i.e. the oracle was in the repo. `sdk/widgets/augment.go` has no importers; the live BSON path is modelsdk/widgets. When reconciling a list from the .mpk, rewrite only on disagreement, or an agreeing template's entry $IDs churn — the Combobox augment test is the no-change control (it fails if the rewrite is unconditional).", "refs": ["mendixlabs/mxcli#1200", "mendixlabs/mxcli#956", "#716"], "ce": ["CE0463"]} {"area": "modelsdk/canon", "date": "2026-09-26", "symptom": "A page rewrite still loses translations despite CarryTranslations: an empty caption's en_US '' vanishes (Texts$Text with no items), and a label's nl_NL 'Gebruikers' is dropped because its English 'Account Overview' is also the page title's", "cause": "Positional pairing needs the whole document's text paths unchanged — one DataGrid2 rebuilt from its template breaks that. Source pairing keys on (language, text): (en_US, '') is ambiguous on any real page, a shared English source is ambiguous, and a rebuilt EMPTY text has no translation to look up by at all", "file": "modelsdk/canon/translations.go", "insight": "Address a text by the named element that owns it — ($Type, Name, path from the element) — because a widget Name is unique per document. Exact only where the path from the element crosses no list index (a rebuilt pluggable widget reorders Properties; pairing there moves a translation onto the wrong property) and where the address occurs once in each document. Order: positional when the shape is unchanged, then owning element, then source", "refs": ["ako/mxcli#705"]} {"date": "2026-10-01", "area": "modelsdk", "symptom": "mendixlabs/mxcli#849: `mxcli exec` writes the .mpr while Studio Pro has the project open; the write succeeds, mx check passes, and Studio Pro's next save silently discards it. No warning, no error.", "cause": "No write path looked for Studio Pro at all; the rule 'close Studio Pro first' existed only in prose (README, docs-site, skills).", "fix": "modelsdk/mpr/studiopro_lock.go: Writer.guardWrite refuses every write that would reach storage (updateUnit and WriteTransaction.WriteUnit after no-op elision, insertUnit, deleteUnit, MoveUnit, UpdateUnitContainer) with StudioProOpenError while `.mpr.lock` (matched case-insensitively) is beside the .mpr; `exec --force` / MXCLI_ALLOW_STUDIO_PRO_OPEN=1 override. Reads and elided no-op writes are never refused.", "insight": "Studio Pro's own generated .gitignore is the evidence for the signal and its spelling: it lists `testapp.mpr.lock` (lower-cased) beside `TestApp.mpr`, plus `mprcontents/mprjournal*`. Guarding at the storage layer after elision, not at command level, keeps twice-exec a no-op instead of an error and covers every command that writes.", "issue": "mendixlabs/mxcli#849", "file": "modelsdk/mpr/studiopro_lock.go"} +{"date": "2026-10-08", "area": "modelsdk/mpr", "symptom": "On an MPR v1 project (single SQLite .mpr) every command is slow in proportion to the file, not the work: a plain `describe microflow` on a 140 MB v1 app (Evora Factory Management, Mendix 10.24) took 3-5 s, 30 s+ on a loaded machine, while the same flow on a v2 project is near-instant", "cause": "`listUnitsByTypeV1` ran `SELECT ... Contents FROM Unit` and decoded `$Type` from every blob before filtering, so each typed listing (ListModules, ListFolders, ListMicroflows, ListConsumedRestServices, ...) and ListUnits read the whole project. One describe did 14 of them (~150-450 ms each). v2 never paid this because `buildUnitCache` indexes types once and reads only matching files; v1 had no equivalent, and the Unit table has no type column", "fix": "v1 type index on the Reader (`reader_units_v1.go`): type peeked from `substr(Contents,1,256)` ($ID then $Type head every Studio Pro unit; full read as fallback), keyed by UnitID blob with the blob's `length(Contents)`. Each listing re-scans only ID/container/name/length (record header, no overflow pages, ~5 ms on 4,372 units), re-types new or resized units, then reads contents by primary key for matches only. ListUnits uses the index on both engines' storage formats (v1 index, v2 unitCache) and reads no contents", "insight": "The index cannot lean on InvalidateCache: v1 `updateUnit` writes the Unit table without calling it, and Studio Pro can change the file under a long-lived reader. Revalidating against `length(Contents)` per listing makes it self-correcting at near-zero cost, because SQLite answers length() from the record header without touching overflow pages, whereas substr() on a blob still pulls them in (measured 55 vs 176 ms per table scan). Counting blob reads per listing (Reader.blobReads) turns 'is it slow?' into an exact, load-independent assertion; wall-clock is useless on a shared machine. After the fix the remaining describe cost is decoding all ~1,770 microflows for ListMicroflows, 3x per describe — inherent to listing by decode, a separate memoisation question", "issue": "", "file": "modelsdk/mpr/reader_units_v1.go"} diff --git a/.claude/skills/fix-issue/findings/other.jsonl b/.claude/skills/fix-issue/findings/other.jsonl index 08773e43a4..ac9134c613 100644 --- a/.claude/skills/fix-issue/findings/other.jsonl +++ b/.claude/skills/fix-issue/findings/other.jsonl @@ -21,3 +21,4 @@ {"area": "examples/doctype-tests", "date": "2026-09-22", "symptom": "The nightly fails on Mendix 10.24 only, in `TestMxCheck_DoctypeScripts/14-project-settings-examples.mdl`, with `Execution error: alter settings workflows add group '' requires Mendix 11.2.0+ (project is 10.24.24.119349)`. Push CI (single, newer version) and `TestDoctypeScriptsParseAfterVersionFiltering` both pass", "cause": "The workflow-groups feature (832e9c80) added Examples 4.3-4.7 to the doctype script ungated, although its executor correctly refuses the statement below 11.2 (`Settings$WorkflowGroup` is 11.2 metamodel). Third time this class has landed (Atlas building block in 15c, `DecimalScale`, now workflow groups)", "file": "`mdl-examples/doctype-tests/14-project-settings-examples.mdl`", "insight": "Wrap the examples in `-- @version: 11.2+` ... `-- @version: any`, with the directive ABOVE the first `/** */` comment. The parse-only guard cannot catch this: it proves the filtered script parses, not that the executor accepts every statement on that version, so a version-refused statement only surfaces in the nightly's 10.24 job. When a feature adds a version check to the executor, gate its doctype example in the same change. Reproduce locally in about 30s: `mxcli setup mxbuild --version 10.24.24.119349`, then `go test -tags integration ./mdl/executor/ -run 'TestMxCheck_DoctypeScripts/