Skip to content

Commit e66222a

Browse files
committed
fix: alter page insert/drop/replace of pluggable object-list entries
A PDS Dropdown Menu's `dropdownitem` entries could not be targeted by ALTER PAGE: insert failed check with MDL-WIDGET25, drop and replace found no widget. Entries are nameless WidgetObjects in the owner's object list, typed by the owner's Type, so they are now built as children of a throwaway donor widget and repointed at the owner's stored Type (refused before writing if any pointer cannot be mapped). New PageMutator methods resolve, insert and drop entries; DROP resolves every ref before removing any, and REPLACE inserts before it drops. The whole-menu replace workaround hit CE0463 from three drifts, all fixed: a PropertyType key index flattened across levels in augment.go, PDS's unconditional hidePropertiesIn not extracted (now OperatorAlways), and unset visible item TextTemplates written null instead of an empty ClientTemplate. WidgetDefGeneratorVersion is bumped to 18. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 90f6ca4 commit e66222a

30 files changed

Lines changed: 2130 additions & 204 deletions

‎.claude/skills/fix-issue/findings/mdl-backend.jsonl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,4 @@
126126
{"area":"mdl/backend","date":"2026-09-22","symptom":"modelsdk/mpr/version.ProjectVersion declared its own struct with the same seven fields as mdl/types.ProjectVersion instead of aliasing it, so a *version.ProjectVersion could not be passed where a *types.ProjectVersion was wanted and vice versa — two unrelated Go types that both print as 'ProjectVersion'.","cause":"The deleted sdk/mpr/version aliased the canonical type (`type ProjectVersion = types.ProjectVersion`); this copy declared a duplicate. CLAUDE.md's shared-types rule asks for the alias, and nothing enforced it. The duplication survived the legacy-engine retirement because it compiles perfectly — the two declarations are field-for-field identical, so only an assignment ACROSS the boundary reveals them as different types.","file":"modelsdk/mpr/version/version.go","fix":"Made it an alias. The four methods it redeclared (IsAtLeast, IsAtLeastFull, String, IsMPRv2) were verified semantically identical to types' first — IsAtLeast differed only in early-return style, same truth table — and now come from types. IsSupported/SupportsFeature could not survive as methods on an aliased type and had ZERO callers anywhere (measured), so they went with Feature, MinVersion, featureVersions and SupportedVersionRange; that map called itself 'the fallback when the YAML registry is unavailable' and the live registry is sdk/versions/mendix-{9,10,11}.yaml via checkFeature.","insight":"A same-shape duplicate type is invisible to every signal except an assignment across the package boundary: it compiles, tests pass, and the error it eventually produces names the same type on both sides of 'want'. So the guard is a COMPILE-TIME assertion, not a runtime test — `var _ *types.ProjectVersion = (*version.ProjectVersion)(nil)` builds only under an alias and fails to build under a duplicate, which is strictly stronger than anything a test body can assert. Write it before the fix and watch it fail to compile; that failure IS the reproduction. Two measurements that made the cleanup safe rather than brave: diff the method BODIES before assuming the redeclarations are redundant (identical behaviour, different style, is the common case and the dangerous one is the near-miss), and count callers of anything the alias forces you to drop — here six exported symbols had zero. Unrelated trap hit while verifying: four cmd/mxcli tests that read skill files failed once in a full `go test ./...` interleaved with `make check-mdl`, which runs sync-skills (rsync --delete into cmd/mxcli/skills/). They pass in isolation, on clean main, and in an uninterleaved full run — do not attribute a skills-reading test failure to your change without re-running it alone."}
127127
{"area": "mdl/backend", "date": "2026-09-22", "symptom": "`create workflow … overview page X` reports `Created workflow` and exit 0 and stores NOTHING — the written unit carries no page reference and not even the page's qualified name as a string. `mx check` passes (a workflow with no overview page is valid) and `describe workflow` omits the clause, so nothing reveals the loss. Running `alter workflow … set overview page X` afterwards DOES write it, which is what makes the split visible", "cause": "Two fields for one concept, never joined: the executor set semantic `Workflow.OverviewPage` (`cmd_workflows_write.go:170`) and `workflowToGen` only ever read `Workflow.AdminPage`, which nothing set. The READ half was wrong in the mirror direction — `workflowFromGen` took `g.OverviewPageQualifiedName()`, so even the correctly-written ALTER read back empty and the catalog's overview-page reference edge never fired", "file": "`sdk/workflows/workflow.go` (the two fields collapsed to one), `mdl/backend/modelsdk/workflow_write.go` (`workflowToGen`), `mdl/backend/modelsdk/workflow_read.go` (`workflowOverviewPageName`)", "insight": "**The Model SDK's StructureVersionInfo settles which of two rival property names is real, in one grep**: `npm pack mendixmodelsdk` then `src/gen/workflows.js` gives `overviewPage: {deleted: \"9.11.0\"}` and `adminPage: {introduced: \"9.11.0\"}` — so AdminPage (a `Workflows$PageReference` CHILD, not a by-name string) is the stored property, and `generated/metamodel` agrees by declaring AdminPage and no OverviewPage. `modelsdk/gen` declares BOTH, which is how a reader and a writer ended up on opposite sides of a 9.11 rename inside one package. **The version branch CLAUDE.md's overlay rule would demand is dead here, and that is a measurement not an assumption**: `workflowToGen` writes `WorkflowV2`, introduced in 11.1.0, unconditionally — so no reachable project wants the pre-9.11 key. Write one spelling, READ both (a read fallback invents nothing). **The differential that proves it on a real build**: same script, same project, only the write suppressed — control 0 errors, fixed `CE7410 \"The selected page 'Overview' should accept a parameter of type 'Workflow'\"` on mxbuild 11.6.6. mxbuild can only validate a page it can see, so the error IS the evidence; with a valid overview page both variants are 0 errors, which is the usual weak-signal trap. Useful side-finding: an overview page takes **System.Workflow**, while a user task's page takes **System.WorkflowUserTask** — two pages, two parameters. NOT fixed: no check rule for CE7410 yet, and `WorkflowV2` being written unconditionally is questionable for a 10.x project. Same shape as the `create … comment 'text'` bug (findings/mdl-grammar.jsonl 2026-08-25): grep for `stmt.X = …` / `wf.X = …` with no matching read. Tests `mdl/backend/modelsdk/workflow_overview_page_test.go`; repro `mdl-examples/bug-tests/workflow-586b-overview-page-dropped.mdl`", "refs": ["ako/mxcli#586"], "ce": ["CE7410"]}
128128
{"area": "mdl/backend", "date": "2026-09-23", "symptom": "`describe java action` prints `ContextObject: entity <>` for a parameter declared `entity <pEntity> not null`; a bare type-parameter reference (`Obj: pEntity`, `returns pEntity`) reads back nameless too. The description no longer round-trips", "cause": "The stored parameter type (`CodeActions$EntityTypeParameterType` / `ParameterizedEntityType`) holds only a BY_ID pointer to the `CodeActions$TypeParameter`. `javaActionFromGen` carried the ID into the semantic type but never resolved it to the name, and the name is all the describer prints. `javascript_read.go` had always done this resolution pass; the Java reader was ported without it", "file": "`mdl/backend/modelsdk/java_read.go` (`resolveJavaActionTypeParameterNames`)", "insight": "The executor's `entity <>` fallback in `formatJavaActionType` is the tell: an empty name at DESCRIBE means the *reader* dropped a by-ID resolution, not that the writer lost it \u2014 the write path sets both ID and name, so a create\u2192read unit test in the backend reproduces it without any project. When a JS and a Java reader cover the same `CodeActions$` shapes, diff their post-processing first; any pass one has and the other lacks is a candidate. The write was never wrong (replaying the fixed description into a fresh project describes identically), so no `mx check` run is needed. Issue mendixlabs/mxcli#1034", "refs": ["mendixlabs/mxcli#1034"]}
129+
{"area": "mdl/backend", "date": "2026-09-23", "symptom": "`alter page P { insert after dropdownitem1 { dropdownitem … } }` on a PDS Dropdown Menu failed `mxcli check` with MDL-WIDGET25 '`dropdownitem` is not a widget in this project — did you mean `dropdownfilter`?'; exec failed 'unsupported widget type: dropdownitem'; `drop widget dropdownitem2` found no widget; `replace dropdownitem3` likewise. DESCRIBE prints those names, so they looked addressable.", "cause": "An object-list entry is not a widget: it is a nameless CustomWidgets$WidgetObject in its owner's Object.Properties[].Value.Objects, typed by TypePointers into the owner's Type. The ALTER validator gave a body's top level no parent context, so the entry keyword was looked up as a widget; the page mutator only ever located widgets by Name, and DESCRIBE's `dropdownitemN` names are synthesised positions, not stored.", "file": "`mdl/backend/pagemutator/objectlist_items.go` (ResolveObjectListItem / InsertObjectListItems / DropObjectListItems), `mdl/executor/cmd_alter_page_objectlist.go` (routing), `mdl/executor/validate_widgets.go` (alterObjectListMappings) + `validate_widget_kind.go`", "insight": "Build new entries as children of a throwaway donor widget of the owner's widget id (same builder as CREATE PAGE, so every property, default and visibility rule matches), then lift them out and repoint their TypePointers at the owner's STORED Type by walking both Types in parallel on PropertyKey — the donor's Type ids are fresh, and a leaked one is the classic CE0463. Refuse before writing if any pointer is unmapped. Positional names renumber on every change, so a DROP resolves all refs first, then removes per list with sorted indexes; REPLACE inserts after then drops, so a failed build leaves the list intact. Verified on Marketplace UserGroups.GroupDetails: insert after/into, drop and replace each left mx check at the exact pre-existing baseline, no CE0463. The embedded widget registry declares no object lists, so executor tests must build a registry in memory.", "ce": ["CE0463"], "rules": ["MDL-WIDGET25", "MDL-WIDGET26"]}

‎.claude/skills/fix-issue/findings/modelsdk.jsonl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@
1919
{"area": "modelsdk/meta", "date": "2026-09-22", "symptom": "A view entity selecting `u.Name` from System.User could not be declared in any way that both passed `mxcli check` and built: `String(100)` (the correct length) was refused, `String` (unlimited) passed check and then failed mxbuild with CE6770 \"View Entity is out of sync with the OQL Query\". `describe entity System.User` reported `Name: String(unlimited)`. Reported in ako/ChipCoV4 FINDINGS.md against Mendix 11.14.0 (ako/mxcli#584, with #585 the other half).", "cause": "meta.SystemAttrDef declared a Length field and NOT ONE of the 115 String attributes in modelsdk/meta/system_module.go populated it, so systemAttrType built every System string as StringAttributeType{Length: 0} — which mxcli reads as unlimited. Every length comparison against a System attribute was therefore made against 0. Fixed by measuring all 115 and populating them, with a golden table (modelsdk/meta/testdata/system_string_lengths.txt) and TestSystemStringLengths holding the two in step.", "file": "`modelsdk/meta/system_module.go` (SystemEntities, SystemAttrDef.Length); `modelsdk/meta/testdata/system_string_lengths.txt`; tests `modelsdk/meta/system_string_lengths_test.go`, `modelsdk/meta/system_string_lengths_measure_test.go`, `mdl/backend/modelsdk/system_module_read_test.go`", "insight": "The System module's attribute lengths are IN THE BUILD OUTPUT: `deployment/model/model.mdp` is a stream of BSON documents (each with its own 4-byte length prefix — unmarshalling the file whole fails with \"invalid document length\"), the System module arrives as a Projects$ModuleImpl carrying only a Name with its DomainModels$DomainModel immediately after, and every entity's attributes are there with their StringAttributeType.Length. That is ONE `mxbuild --target=deploy` for all 115, and it is the model the runtime builds the tables from, so it is the same number CE6770 is decided by. Two searches not worth repeating, both spent on this issue: the Mendix Model SDK does not carry them (its gen/ describes metamodel TYPES, so System.User.Name is not in it) and the modeler's own copy is inside Mendix.Modeler.Core.dll, i.e. a decompiler. The one-view-entity-per-attribute mxbuild probe works but is ~40s each. The version question answers itself the same way: building 10.24.4.77222 as well showed all 216 shared attributes identical in type AND length to 11.14.0, so one table serves every supported version instead of a per-version registry — measure the second version rather than reasoning about it, it is one more build. Finally, 0 is Mendix's own encoding of \"unlimited\" (46 of the 115), so populating the table does not make 0 safe to read as a length — what makes it safe is that the golden enumerates every String attribute, so 'unmeasured' cannot exist without failing a test."}
2020
{"area": "docs / modelsdk/mpr", "date": "2026-09-22", "symptom": "Four reference pages described an MPR v1 `UnitContents` table holding the BSON blobs, and a v1/v2 detection recipe that probes for it. No .mpr has ever had that table: a v1 file has exactly `Unit` and `_MetaData`, and contents are the `Unit.Contents` blob. `grep -rn UnitContents --include=*.go` is 0 hits. Reported by an outside reader building an independent format reader (mendixlabs/mxcli#1072).", "cause": "Never-measured prose. The pages also invented `UnitType` and `Name` columns on `Unit` (there are seven columns and neither is among them — type and name come out of the BSON `$Type`/`Name`), and drew `mprcontents/` flat when it is sharded `<XX>/<YY>/<swapped-uuid>.mxunit`. Fixed by rewriting the four pages from the SQLite catalogs of two real fixtures, and adding modelsdk/mpr/docs_schema_test.go to hold them there.", "file": "`docs-site/src/internals/mpr-format.md`, `docs-site/src/internals/mpr-v1-v2.md`, `docs-site/src/appendixes/version-compatibility.md`, `docs/05-mdl-specification/10-bson-mapping.md`; test `modelsdk/mpr/docs_schema_test.go`", "insight": "Prose cannot be type-checked but the IDENTIFIERS in it can, and the rule that makes it zero-maintenance is a prefix rule, not an allowlist: check only names BEGINNING with a real table name (`Unit`, `_MetaData`, `_Transaction`) against the union of the fixtures' tables and columns. `UnitContents` and `UnitType` are caught; the catalog tables these same pages mention (`REFS` and friends) never start with a real .mpr table name, so they need no exemption and no one has to maintain a list. The page set is discovered by content (any .md under docs-site/src or docs/05-mdl-specification mentioning `.mpr`/`mprcontents`), so a page added later is covered without anyone remembering. One consequence worth stating in the docs themselves: a page that wants to say a column does NOT exist must say it in PROSE — the first fix wrote \"there is no `UnitType` column\" and the test flagged its own remedy, which is correct, because the old pages' \"no `UnitContents`\" at mpr-v1-v2.md:35 read as a v1/v2 difference rather than as a fiction and an exemption for denials would have masked it. The control is cheap and exact here: `git stash` the doc edits with the test file kept, and the failures reproduce the reporter's line list verbatim (version-compatibility.md:31, mpr-format.md:21,23, mpr-v1-v2.md:12,35,69,73,74,84,94, 10-bson-mapping.md:30) plus the two they had not found. No Mendix tool runs in this fix's argument — the claims are about SQLite schema and are read straight off `sqlite_master`/`PRAGMA table_info`, which is the primary source, so the usual 'build two apps' rule does not apply.", "refs": ["mendixlabs/mxcli#1072"]}
2121
{"area": "docs / modelsdk/mpr", "date": "2026-09-22", "symptom": "The MPR reference pages' \"Unit Types\" tables mapped BSON `$Type` to document kinds, and 15 of the rows named a spelling no unit carries: `Pages$Page`/`Pages$Layout`/`Pages$Snippet`/`Pages$BuildingBlock` (real units say `Forms$*`), and docs/05-mdl-specification/10-bson-mapping.md lowercased eleven more (`microflows$microflow`, `pages$page`, `security$ProjectSecurity`…). It also listed `CustomWidgets$customwidget` as a document type. Found while fixing mendixlabs/mxcli#1072, filed and fixed separately.", "cause": "The tables were written from the TypeScript SDK's QUALIFIED names rather than the storage names Mendix writes — the same split CLAUDE.md documents for `ShowPageAction`/`ShowFormAction`, never applied here. `CustomWidgets$CustomWidget` is a widget element inside a page's tree (mdl/catalog/builder_widget_refs.go), never a unit, so that row was removed rather than corrected.", "file": "`docs-site/src/internals/mpr-format.md`, `docs/05-mdl-specification/10-bson-mapping.md`; test `modelsdk/mpr/docs_schema_test.go` (TestDocumentedUnitTypesUseStorageNames)", "insight": "Measuring the real set is one command and settles the whole table at once: decode every `mprcontents/*/*/*.mxunit` (and every v1 `Unit.Contents` blob) and count `$Type` — 28 distinct values across a blank 11.6.6 app and a 9.24.30 one. Do NOT try to verify rows one at a time against gen, which carries BOTH spellings: `model/types.go` defines `DocumentTypePage = \"Pages$Page\"` and mdl/catalog/builder_xpath.go defensively matches `Forms$Page` AND `Pages$Page`, so grepping the codebase 'confirms' the wrong name. The fixture is the arbiter; the codebase is not. The test rule that makes this checkable without a maintenance burden keys on the LOCAL name after the `$`, case-insensitively: a fixture cannot prove a type ABSENT (a blank project has no business-event service), so demanding every documented type be present would fail correct rows — but when the fixture has a type with the same local name, the documented row must equal it exactly. That catches all four `Pages$` rows and all eleven lowercase ones with zero false positives. Its stated limit is real and cost a manual fix: a row whose local name appears nowhere in the fixtures is not checked at all, which is how `CustomWidgets$customwidget` slipped past and had to be removed by hand. One editing trap, not a Mendix one: anchoring a section replacement on `'---'` matches a markdown TABLE SEPARATOR (`|---|---|`) long before the horizontal rule you meant — the edit silently no-ops on the table you were replacing. Anchor on `'\\n---\\n'`.", "refs": ["mendixlabs/mxcli#1072"]}
22+
{"area": "modelsdk", "date": "2026-09-23", "symptom": "`alter page … replace pDSDropdownMenu1 with { pdsdropdownmenu pDSDropdownMenu1 (…) { dropdownitem … x5 } }` passed check and exec, then mx check failed CE0463 'The definition of this widget has changed' on the rebuilt PDS Dropdown Menu.", "cause": "Three independent drifts from what Studio Pro writes: (1) the reconcile passes in modelsdk/widgets/augment.go indexed properties by key flattened across levels, so the nested `caption` of a dropdown item overwrote the top-level `caption` PropertyType's Category; (2) PDS getProperties opens with an UNCONDITIONAL `hidePropertiesIn(...,[\"ariaLabelCaption\",\"moreOptionsCaption\"])`, which the editorConfig extractor ignored (it only recognised guarded hides), so mxcli wrote those TextTemplates populated where Studio Pro stores null; (3) visible-but-unset TextTemplate sub-properties of object-list items were left null where Studio Pro writes an empty ClientTemplate.", "file": "`modelsdk/widgets/augment.go` (scoped key index), `mdl/executor/editorconfig_extract.go` (OperatorAlways rule), `mdl/backend/widgetobj/builder.go` (empty ClientTemplate for items), `mdl/executor/widget_engine.go` (WidgetDefGeneratorVersion 18 so cached .def.json regenerate)", "insight": "Diff the rebuilt widget's BSON against the stored one property by property (Type AND Object) — each of the three shows up as a single differing field, and fixing any two still leaves CE0463. Bump WidgetDefGeneratorVersion whenever def extraction changes, or projects keep serving the stale .def.json and the fix appears not to work. The legacy sdk/widgets/augment.go carries the same flattened-key index and was not changed here. Evidence: replace on a fresh copy of the Marketplace project matched the pre-existing mx check baseline exactly (no CE0463); one intermediate run lost an nl_NL translation of an item caption, so check translations when rebuilding TextTemplates.", "ce": ["CE0463"]}

0 commit comments

Comments
 (0)