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/mdl-executor.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -513,3 +513,4 @@
{"area": "mdl/executor", "date": "2026-09-04", "symptom": "`ACTIONBUTTON \u2026 (Action: SIGN_OUT)` is refused by the default engine \u2014 \"client action *pages.SignOutClientAction not yet supported by the modelsdk engine \u2014 rerun with MXCLI_ENGINE=legacy\" \u2014 and the suggested workaround SILENTLY produces a dead button: on legacy the action is written as `Forms$NoAction`, so it renders, says \"Sign out\", and does nothing, with `mxcli check`, `exec` and `mx check` all clean.", "cause": "Neither engine had a case for the action. modelsdk's clientActionToGen ended in a loud default; sdk/mpr's serializeClientAction ended in a QUIET one that returns Forms$NoAction for anything unmatched. Added the case to both. The document is two keys \u2014 `Forms$SignOutClientAction` + `DisabledDuringExecution: true` \u2014 pinned against a Studio Pro-authored button in ako/TestApp, plus `sign_out` in the DESCRIBE renderer so it round-trips. Both engines now emit byte-identical documents; mx check 0 errors on each. OPEN_LINK is still unwritten by both (gen calls it OpenLinkClientAction and its Address is an element, not a string) \u2014 the syntax topic now says so instead of listing it as available.", "file": "`mdl/backend/modelsdk/widget_write.go` (clientActionToGen), `sdk/mpr/writer_widgets_action.go` (serializeClientAction), `mdl/executor/cmd_pages_describe_output.go` (renderClientActionMDL), `cmd/mxcli/syntax/features_page.go`; example `mdl-examples/bug-tests/captrack-10-sign-out-action.mdl`", "insight": "When one engine refuses something and points at the other, CHECK THE OTHER before repeating the advice \u2014 the refusal is visible and the fallback is not, so the recommended escape hatch can be the strictly worse path. The structural tell is the shape of the default branch: modelsdk's raises, legacy's returns Forms$NoAction, and a silent default in a serializer converts every unimplemented type into data loss rather than an error. Grep for the fallthrough before trusting a switch. Note the control this needs: a test that SIGN_OUT is no longer NoAction can pass because someone softened the default, so pin the fallback separately with a type that is still unimplemented (OPEN_LINK). Reported as CapTrackV2 FINDINGS \u00a710."}
{"area": "mdl/executor", "date": "2026-09-04", "symptom": "A navigation menu's LOG-OUT item could not be authored and did not survive a round trip. MDL's `menu item` took PAGE or MICROFLOW only, so there was no spelling for it; and ako/TestApp's sign-out menu item read back as a plain `menu item 'Item 5';`, so DESCRIBE -> exec turned a working log-out entry into a dead one \u2014 silently, with `mx check` clean.", "cause": "A menu item's action goes through FOUR places that share no code with the button path: menuActionToGen (menu document, modelsdk), navMenuAction (navigation profile, raw BSON), resolveMenuAction (modelsdk read) and parseNavMenuItem (legacy read). Both writers ended in a NoAction default and both readers left the type name unmapped. Added SIGN_OUT to navMenuItemDef in the grammar (it consumes no qualifiedName, so it is read separately from the PAGE/MICROFLOW switch or an ICON after it is mis-assigned), carried it as ActionType \"SignOutAction\" / NavMenuItemSpec.SignOut, and wired all four. Studio Pro stores the same Forms$SignOutClientAction a button carries: DisabledDuringExecution true, nothing else.", "file": "`mdl/grammar/MDLParser.g4` (navMenuItemDef), `mdl/ast/ast_navigation.go`, `mdl/visitor/visitor_navigation.go`, `mdl/executor/cmd_menus.go` + `cmd_navigation.go` (conversion + printMenuMDL + the show summary), `mdl/types/navigation.go`, `mdl/backend/modelsdk/menu_write.go` + `navigation_write.go` + `navigation_read.go`, `sdk/mpr/parser_misc.go`; example `mdl-examples/bug-tests/captrack-10-sign-out-menu-item.mdl`", "insight": "A round trip closes only if the READER produces the exact string the WRITER consumes \u2014 here both readers had a raw-type-name fallback that looked like it preserved information (ActionType became \"Forms$SignOutClientAction\") while breaking the round trip, because DESCRIBE and the writers key on \"SignOutAction\". A fallback that stores the raw name is not the same as handling the case, and it hides the gap better than a NoAction default would. Also: the same logical action reaches storage through four unrelated switches (two writers x two constructs, two readers), so fixing the button path proved nothing about the menu path \u2014 grep for every switch on the action before calling such a fix complete. Controlled by neutralising both readers and re-reading TestApp: `Item 5 -> sign out` goes back to `Item 5`."}
{"area": "mdl/executor", "date": "2026-09-04", "symptom": "`ACTIONBUTTON \u2026 (Action: OPEN_LINK 'https://\u2026')` was written by neither engine: modelsdk refused it, legacy fell through to its quiet default and wrote Forms$NoAction, so the button rendered and did nothing with check, exec and mx check all clean.", "cause": "Same missing-case defect as SIGN_OUT, but with two traps a reference settled and reasoning would not. (1) The STORAGE NAME is Forms$OpenLinkClientAction, while the semantic type is LinkClientAction and the executor stamped `Forms$LinkClientAction` \u2014 a wrong $Type that never reached disk only because nothing could write the action. (2) The address is not a string field but a nested Forms$StaticOrDynamicString. Pinned against 31 Studio Pro link buttons (ako/TestApp, FeedbackModule): exactly five keys, LinkType \"Web\" in all 31, and 6 of 31 DYNAMIC (IsDynamic true + AttributeRef + empty Value). MDL authors the static form only, so DESCRIBE flags a dynamic one instead of printing its address as a literal.", "file": "`mdl/backend/modelsdk/widget_write.go` (clientActionToGen + staticAddressToGen), `sdk/mpr/writer_widgets_action.go`, `mdl/executor/cmd_pages_builder_v3.go` ($Type), `mdl/executor/cmd_pages_describe_output.go`, `cmd/mxcli/syntax/features_page.go`; example `mdl-examples/bug-tests/captrack-10-open-link-action.mdl`", "insight": "gen declares a fourth property on Forms$StaticOrDynamicString \u2014 `Attribute` \u2014 that not one of the 31 stored documents carries. Writing it would be the 'never invent a key' failure: a document mxbuild accepts and Studio Pro cannot open. When gen offers more properties than the references show, the references win. Second lesson, about controls: the SIGN_OUT commit used LinkClientAction as its 'still unimplemented' control, and implementing OPEN_LINK silently invalidated it \u2014 the test then failed for a good reason, but a control naming a specific unimplemented feature has a shelf life. Point it at something structurally unwritable instead (ShowHomePageClientAction: no gen type, no metamodel counterpart, no MDL statement that builds one)."}
{"area": "mdl/executor", "date": "2026-09-04", "symptom": "A generated domain model opens in Studio Pro as ONE horizontal line of entities, boxes touching, unreadable at any zoom. Reported on a 40-entity model (ako/CapTrackV2, Mendix 11.13).", "cause": "The default position for a CREATE ENTITY with no `@Position` was `model.Point{X: 100 + len(dm.Entities)*150, Y: 100}` \u2014 same y for every entity ever created, x stepping by 150. 40 entities = a 6,950px row; and 150px is narrower than an entity box, so they also overlapped. Replaced with a wrapping grid in the new `mdl/dmlayout` package, and added `mxcli layout` for a real layered layout off the association graph.", "file": "`mdl/executor/cmd_entities.go` (the default), `mdl/dmlayout/dmlayout.go` (new: GridSlot + Plan), `cmd/mxcli/cmd_layout.go` (new command)", "insight": "The default could not have been much better than a grid, and that is the design point: the first entity of a script is placed before the last one exists, so no create-time rule can see the graph. Layout needs the whole model, so it belongs in a separate pass, not as a side effect of authoring \u2014 and because it necessarily overwrites hand-arranged positions it has to be opt-in with a dry run. Two constraints that are easy to miss: an entity stores only Location and NO Size (Studio Pro derives the box when it draws), so spacing must be estimated from name length and attribute count; and a Mendix position is the box's CENTRE, not its top-left, so placement adds half a box. Determinism is load-bearing rather than cosmetic \u2014 an unsorted walk gives a different diagram every run, which rewrites the unit every time and is exactly the churn ADR-0008 exists to prevent (the test catches it on run 0)."}
33 changes: 26 additions & 7 deletions .claude/skills/mendix/generate-domain-model/reference/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,34 @@ identifiers; a value whose name is a reserved word can't be targeted by `alter`.

### Entities

**IMPORTANT: All entities MUST have @Position annotation**
**Positions are optional. Prefer `mxcli layout` over hand-placing them.**

The `@position(x, y)` annotation specifies where the entity appears in the domain model diagram. Without it, entities appear at (0,0) or random locations.
`@position(x, y)` sets where the entity sits in the domain-model diagram. An
entity without one is NOT lost — it takes the next slot in a wrapping grid — but
a grid is a default, not a layout: it knows nothing about which entities are
related, so association lines still cross the diagram.

**Position Guidelines:**
- Use increments of 50 or 100 for spacing (e.g., 100, 200, 300)
- Leave space between entities (at least 200 pixels)
- Organize related entities in logical groups
- Example layout: Categories at y=100, Transactions at y=300, Reports at y=500
The better answer for a generated domain model is to write no positions at all
and arrange the module once the script has run:

```bash
mxcli layout -p app.mpr --module MyModule --dry-run # see the moves
mxcli layout -p app.mpr --module MyModule # apply
```

That lays entities out from the association graph — lookups on the left, each
entity one column past the furthest thing it references — so the lines mostly
run one way. It is idempotent (a second run moves nothing) and local (adding an
entity later moves a handful, not the model). It REPLACES positions you set by
hand in the module it touches, which is the reason it is a separate command
rather than something `exec` does on its own.

Write `@position` when you want explicit control of a particular entity, and
remember the coordinate is the box's **CENTRE**, not its top-left corner:

- 250+ apart horizontally, 250+ vertically, to clear a typical box
- group related entities, and keep the lookups together
- `alter entity Mod.Name set position (x, y)` moves one without restating it

**Association line anchors** — where the connector attaches to each entity box —
are set with `@anchor`, as a **percentage of the box** (0..100, whole numbers):
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ go build -o bin/mxcli ./cmd/mxcli
| **Connector gen** | `sql <alias> generate connector into <module> [tables (...)] [views (...)] [exec]` | Auto-generate Database Connector MDL from discovered schema |
| **Marketplace drift** | `mxcli marketplace diff <id> -p app.mpr [--to V] [--json]` | Which elements of an installed marketplace module have been edited locally, and what an upgrade would overwrite |
| **Model repair** | `mxcli fix widgets`, `mxcli fix design-properties` | Runs `mx update-widgets` / `mx rename-design-properties` and **persists** the result without their MPR v2 → v1 collapse (harvest: let the tool convert, read the units back, restore v2, write the changed ones through mxcli's writer). Clears CE0463 / CE6087 after a headless install — measured 203 → 0 errors on a vanilla 11.12.1 app |
| **Domain-model layout** | `mxcli layout -p app.mpr [--module M] [--dry-run]` | Arranges entities from the **association graph**: an entity referencing nothing is a lookup and goes left, everything else one column past the furthest thing it references, so lines run one way instead of crossing. Unconnected entities (non-persistent helpers) go in a band below rather than among the lookups. Positions are a function of the model, so a second run moves nothing. Replaces hand-arranged positions in the modules it touches — hence opt-in, with `--dry-run`; Marketplace modules and System are skipped. The **default** for an entity with no `@Position` is a wrapping grid (`mdl/dmlayout`), not the single 6,000px row it used to be |
| **Diagnostics** | `mxcli diag [--bundle]` | Session logs, version info, bug report bundles |
| **Project brain** | `mxcli brain init\|capture\|staged\|promote\|drop\|check\|show\|plan\|resolve` | Opt-in store in `docs/brain/` for what mxcli **cannot** compute (why a pattern was chosen here, which marketplace version broke what). Sharded by module — an entry's first anchor names its file — so a session loads `project.md` plus the modules it is touching, not the whole store. Also holds the **plan**: requirements grouped into slices, whose anchors point *forward*, so `brain plan` reports progress **derived from the model** rather than from a status column. An agent captures to a git-ignored queue; a person promotes |
| **New project** | `mxcli new <name> --version X.Y.Z [--output-dir dir] [--theme none] [--layout none]` | Downloads mxbuild, creates blank project, applies default styling, scaffolds a project-owned layout, runs init, installs Linux mxcli for devcontainer |
Expand Down
Loading
Loading