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
1 change: 1 addition & 0 deletions .claude/skills/fix-issue/findings/modelsdk.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
{"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"]}
{"area": "modelsdk/canon", "date": "2026-09-23", "symptom": "The storage-GUID write guard (`canon.StorageGUIDChanges`) stopped refusing the MOVE ENTITY data loss it had exposed (ako/mxcli#503). With MoveEntity's carries removed, moving an association's TO side re-minted the in-place converted cross-association's GUID and the write went through silently, where the issue records a refusal.", "cause": "`sameMember` (added in 86927852 to stop the guard refusing transplant mis-pairings) required an equal `$Type` as well as an equal `Name`. MoveEntity converts `DomainModels$Association` to `DomainModels$CrossAssociation` IN PLACE, keeping `$ID` and `Name`, so the type clause made the guard skip the pair. The type clause excluded nothing the transplant can produce: `pairDoc` stops at a `$Type` mismatch (TestTransplantIgnoresMismatchedTypes).", "file": "`modelsdk/canon/storageguid.go` (`sameMember`, the note above `GUIDChange`)", "insight": "Before adding a clause to an identity test that sits on an approximate pairing, ask what error of THAT pairing the clause excludes. The transplant only mis-pairs same-type, different-name elements, so `$Type` excluded none of its errors. Its only effect was to exclude the one writer that keeps an `$ID` across a type change deliberately. Rule now: Name when both sides have one; `$Type` only when neither does; a pair with a name on one side only is not a match. How the gap was found: stub the three MoveEntity carries on main and run TestIssue503. The child-side case returned no error where the issue quotes a refusal. That mismatch between the recorded refusal and the observed silence was the tell. A guard's quiet is not evidence of a clean write, so a guard's comment must list every hole it leaves; this one listed only renames. Controls: (1) the new canon test fails on the old `sameMember` with `got 0 change(s)`; (2) with the MoveEntity carries stubbed the child-side move is refused again with the issue's exact message, and the parent-side move still goes through, because the moved element changes unit and pairs with nothing (a documented hole); (3) the 86927852 false positive does not return: `marketplace install --file mx-modules/BusinessEvents_3.12.0.mpk` into a copy of testdata/expr-checker, then `create or modify persistent entity BusinessEvents.PublishedBusinessEvent (EventId: long)` is accepted, while a build with an `$ID`-only rule refuses it (EventId paired with a removed attribute). The existing table case `DifferentType_NotAChange` pinned the wrong decision with the justification 'nothing authors this today', which was false the day it was written. Grep for the writers (`SetID(x.ID())` next to `New<OtherType>()`) before claiming nothing authors a shape.", "refs": ["ako/mxcli#503", "mendixlabs/mxcli#1119"]}
{"area":"modelsdk/codec","date":"2026-09-25","symptom":"A compound design property (Atlas `Spacing` → `margin-bottom`, or a multiSelect toggle group) writes its `Forms$CompoundDesignPropertyValue.Properties` list with BSON array marker 3 where Studio Pro writes 2. `check`, `exec` and `mx check` all pass; a describe → exec round trip of FeedbackModule.ShareFeedback (Feedback v4.0.2, 11.13.0) turned every nested marker-2 list into 3","cause":"The codec picks a PartList's marker from the CHILD element's `$Type` only (`partListMarker` → `lookupListMarker`). The nested list and the enclosing `Forms$Appearance.DesignProperties` list (marker 3) both hold `Forms$DesignPropertyValue`, so no `RegisterListMarker` on the child type could tell them apart and both fell to the default 3","file":"`modelsdk/codec/defaults.go` (`RegisterPropertyListMarker`), `modelsdk/codec/encoder.go` (`propertyListMarker`), `mdl/backend/modelsdk/widget_write.go` (init)","insight":"When one child `$Type` sits in two lists with different markers, the marker belongs to the owner+key, not the child: `RegisterPropertyListMarker(owner, key, m)` is consulted first, for an empty list too and in the selective-rebuild path. Establish the marker by counting Studio Pro-authored BSON before changing anything: walking every mxunit gave Compound.Properties 373/373 marker 2 and Appearance.DesignProperties 1821/1821 marker 3 across pages, layouts, building blocks and page templates. Count per (owner $Type, key) — a flat grep of `Properties [marker=2]` in ndsl also matches unrelated lists. Pages, snippets and layouts share `newAppearance`, so one registration covers all. Test `TestAppearanceCompoundDesignPropertyMarkers`; bug-test `mdl-examples/bug-tests/compound-design-property-marker.mdl`. Same class, not fixed: the selective-rebuild branch of `encodeEntry` still hard-codes 3 for lists with no owner registration, ignoring a child-type `RegisterListMarker`","refs":["#668"]}
{"area":"modelsdk/mpr","date":"2026-09-25","symptom":"`alter page FeedbackModule.ShareFeedback_Logo { insert after textBox1 { image zzImg (ImageType: imageUrl, ImageUrl: '{1}', ImageUrlParams: [{1} = ImageB64]) } }` (data view over a nanoflow the project lacks, 11.13.0) reported \"Altered page\"; `mxcli docker check` then could not LOAD the project: ArgumentNullException setting 'Attribute' of an Attribute in a Page","cause":"The bare-AttributeRef refusal (#678) lived in encodePage/encodeSnippet only. ALTER PAGE patches the stored BSON in pagemutator and saves via UpdateRawUnit, never passing the encoder; with no entity in scope the pluggable-widget template-parameter builder (widgetobj) writes the name as given, so DomainModels$AttributeRef{Attribute:\"ImageB64\"} reached disk","file":"modelsdk/canon/attributeref.go; modelsdk/mpr/writer_core.go (updateUnit, insertUnit)","insight":"A guard placed in one encoder covers one write path; the page family has at least four (encodePage/Snippet, pagemutator Save, widget sync apply, layout/template raw writes). Put an unloadable-shape refusal at the writer beside DuplicateElementIDError, as that one already argued. Measured before refusing stored refs too: 73 of 73 AttributeRefs across all 374 units of a stock 11.13 project are qualified (71 page, 1 snippet, 1 page template, none elsewhere) — a stored bare one cannot have come from Studio Pro, so refusing ALL bare refs (not only new ones) blocks nothing legitimate. The textbox path does NOT reproduce it: attributeRefToGen nulls a bare name (a silent binding drop instead); the pluggable/column template builders are the ones that write it verbatim. The test goes through the real mutator + writer on the expr-checker fixture (InsertColumns with a bare CaptionParams ref).","refs":["#678"]}
6 changes: 4 additions & 2 deletions docs-wiki/bug-patterns/unloadable-model-writes.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ project rather than one page, and because the diagnostic is a stack trace.
that points at the wrong thing. Mendix reconstructs each stored property into a
typed identifier as it loads, and a value that cannot be parsed into that type
takes the loader down. The shapes seen so far: a one-qualifier member name
written where an attribute reference is expected (an attribute is bare or
`Module.Entity.Attribute`, never `Module.Name`); an unqualified entity name in a
written where an attribute reference is expected (a stored
`DomainModels$AttributeRef` is `Module.Entity.Attribute` and nothing else — a
bare name fails to load as surely as `Module.Name`, and the writer now refuses
both for every unit, ALTER's raw patches included); an unqualified entity name in a
generalization; a literal string where the property is a `ConstantIdentifier`;
an empty `DestinationEntity`; an index column pointing at a GUID that no longer
exists; a sequence flow dangling from a `break`; an association whose `ParentPointer`
Expand Down
57 changes: 57 additions & 0 deletions mdl-examples/bug-tests/alter-page-bare-attributeref-refused.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
-- ============================================================================
-- ALTER PAGE refuses to store a bare attribute reference
-- ============================================================================
--
-- Symptom: on a copy of a real 11.13.0 project,
-- alter page FeedbackModule.ShareFeedback_Logo {
-- insert after textBox1 { image zzImg (ImageType: imageUrl, ImageUrl: '{1}',
-- ImageUrlParams: [{1} = ImageB64]) }
-- }
-- reported "Altered page", and `mxcli docker check` then could not LOAD the
-- project: ArgumentNullException setting 'Attribute' of an Attribute in a Page.
-- The data view is sourced by a nanoflow the module does not ship, so there is
-- no entity to qualify `ImageB64` against, and the image's template parameter
-- was written as DomainModels$AttributeRef { Attribute: "ImageB64" }.
--
-- Cause: CREATE PAGE refused a bare reference in its encoder (encodePage), but
-- ALTER PAGE patches the stored BSON tree in the page mutator and saves it with
-- UpdateRawUnit, which never passes that encoder.
--
-- Fix: the refusal moved to the write choke point (modelsdk/mpr updateUnit and
-- insertUnit, canon.BareAttributeRefError), beside the duplicate-$ID guard, so
-- every raw write of any unit is covered.
--
-- Verify: exec this script → "Altered page"; `mxcli docker check` → 0 errors.
-- Change the inserted image's parameter to `{1} = Subject` → the ALTER is
-- refused ("attribute reference not qualified as Module.Entity.Attribute …
-- "Subject" at …/imgQualified…"), and the stored page is unchanged.
-- ============================================================================

create entity MyFirstModule.AlterBareDraft (
Subject: String(200),
PictureUrl: String(400)
);
/

@excluded
create or modify page MyFirstModule.AlterBareDraft_Example
( Title: 'Draft (example)', Layout: Atlas_Core.Atlas_Default )
{
dataview dv (DataSource: nanoflow MyFirstModule.DS_MissingAlterBareDraft) {
textbox txtSubject (Label: 'Subject', Attribute: MyFirstModule.AlterBareDraft.Subject)
}
}
/

-- Inside the data view there is no resolvable entity: only a qualified
-- reference can be stored.
alter page MyFirstModule.AlterBareDraft_Example {
insert after txtSubject {
image imgQualified (
ImageType: imageUrl,
ImageUrl: '{1}',
ImageUrlParams: [{1} = MyFirstModule.AlterBareDraft.PictureUrl]
)
}
};
/
59 changes: 0 additions & 59 deletions mdl/backend/modelsdk/page_bare_attributeref.go

This file was deleted.

47 changes: 0 additions & 47 deletions mdl/backend/modelsdk/page_bare_attributeref_test.go

This file was deleted.

Loading
Loading