From a4f882f6791a3b79f8e669e80b8f6765965f0f6a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 16:31:34 +0000 Subject: [PATCH 1/3] docs: fix skill inaccuracies found via the sudoku test app (findings #1,#3,#4,#6,#9,#18,#19,#20,#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine findings from an end-to-end app build were wholly or partly wrong/stale docs in the synced skills (shipped to user projects via `mxcli init`): - #1 write-microflows.md listed a non-existent `randomInt()` — removed; added a note to use `round(random()*N)` and that Decimal-returning funcs (random, div, *Between) need round/floor/ceil before an integer target. - #3 java-actions.md / patterns-data-processing.md showed bare `not $x` — Mendix requires `not(expr)`; corrected both (cheatsheet-errors.md already documents the rule). - #4 mdl-entities.md showed `index name on (cols)` — the real syntax has no `on` (`index name (cols)`); corrected. - #6 mdl-entities.md showed `autonumber` with no seed — it requires `default N` (else CE7247 at build); corrected examples + the quick-ref row. - #9 resolve-forward-references.md claimed `call microflow` forward refs "already work" — they don't at exec time (resolves against the project, not later same-script defs); corrected to say order the callee first. - #18 added the bucket-class idiom for computed dimensions (no inline computed Style; quantise to a bucket + SCSS @for). - #19 promoted the clickable-container-with-args idiom in create-page.md (actionbutton can't hold children or pass literals). - #20 added a domain-model note: widgets bind members, not expressions (model the wide form when the UI addresses parts individually). - #22 migrate-design-prototype.md claimed SCSS partials "cannot be created" — stale; they can, and are recommended for readability. Also hardens the CI gate that should have caught #4: check-skill-mdl.sh misclassified any DDL statement prefixed with a `/** */` doc comment as non-DDL and silently skipped it. Skip leading doc/line comments when finding the first keyword — the skills corpus now checks 179 blocks (was 159) and all pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4 --- .claude/skills/mendix/create-page.md | 20 +++++++++++ .../skills/mendix/generate-domain-model.md | 12 +++++++ .claude/skills/mendix/java-actions.md | 2 +- .claude/skills/mendix/mdl-entities.md | 13 ++++---- .../skills/mendix/migrate-design-prototype.md | 33 ++++++++++++++++--- .../skills/mendix/patterns-data-processing.md | 2 +- .../mendix/resolve-forward-references.md | 2 +- .claude/skills/mendix/write-microflows.md | 7 +++- scripts/check-skill-mdl.sh | 9 +++-- 9 files changed, 84 insertions(+), 16 deletions(-) diff --git a/.claude/skills/mendix/create-page.md b/.claude/skills/mendix/create-page.md index 9b5f48677..91332e9c3 100644 --- a/.claude/skills/mendix/create-page.md +++ b/.claude/skills/mendix/create-page.md @@ -766,6 +766,26 @@ container card1 (OnClick: microflow MyModule.ACT_OpenDetails, class: 'clickable- This maps to the Mendix Container's **On click** event (`Forms$DivContainer.OnClickAction`). A container with no `OnClick:`/`Action:` is non-clickable (a no-op action), exactly as in Studio Pro. +**Prefer a clickable container over an `actionbutton` when the trigger needs child +widgets or a context argument.** An `actionbutton` can't contain child widgets and +maps only page-context object parameters — so a tile showing a digit over a label, +or a card that opens a specific object, is best built as a `container` with +`OnClick:`. The container's `OnClick:` takes the same `(Param: …)` argument syntax +as an `actionbutton`'s `action:`: + +```sql +-- Rich, parameterised trigger: a card that opens the object it represents +container tileCard (OnClick: microflow MyModule.ACT_Open(Item: $currentObject), class: 'tile') { + dynamictext tileValue (content: '4') + dynamictext tileLabel (content: '4 LEFT', class: 'tile-label') +} +``` + +Note the Mendix limit this works around: a button (or `OnClick`) can map **object** +parameters from the page context but **cannot pass a literal argument**. To vary a +literal per trigger (e.g. a 1–9 number pad), make one real microflow and a thin +wrapper per value (`ACT_Set1`…`ACT_Set9`), each calling the shared implementation. + ### FOOTER Widget Container for form action buttons: diff --git a/.claude/skills/mendix/generate-domain-model.md b/.claude/skills/mendix/generate-domain-model.md index 6971e85d5..eda3bcb0d 100644 --- a/.claude/skills/mendix/generate-domain-model.md +++ b/.claude/skills/mendix/generate-domain-model.md @@ -554,6 +554,18 @@ create persistent entity Module.Address (...); create persistent entity Module.Order (...); ``` +## Modelling for the client: widgets bind members, not expressions + +A page widget binds an **attribute or association path** — never an expression. There +is no `substring(attr, i, 1)` or computed binding in a widget. So a value that must be +rendered per-part (each character of a code, each cell of a grid, each of N pencil +marks) has to be stored as **separate attributes**, not packed into one string and +indexed client-side. Model the wide form when the UI needs to address the parts +individually (e.g. `N1`…`N9` booleans rather than a packed `Notes` string); reach for +a computed/derived value only where a microflow or view-entity OQL produces it into a +real attribute. (Same reason the bucket-class idiom exists — see +`migrate-design-prototype.md`.) + ## Documentation Best Practices ### Entity Documentation diff --git a/.claude/skills/mendix/java-actions.md b/.claude/skills/mendix/java-actions.md index 68c085ead..ef78d855e 100644 --- a/.claude/skills/mendix/java-actions.md +++ b/.claude/skills/mendix/java-actions.md @@ -649,7 +649,7 @@ begin EmailAddress = $customer/Email ); - if not $isValid then + if not($isValid) then validation feedback $customer/Email message 'Please enter a valid email address'; end if; diff --git a/.claude/skills/mendix/mdl-entities.md b/.claude/skills/mendix/mdl-entities.md index 5419d4451..7555256d5 100644 --- a/.claude/skills/mendix/mdl-entities.md +++ b/.claude/skills/mendix/mdl-entities.md @@ -38,8 +38,9 @@ create persistent entity Module.Customer ( -- Enumeration status: Module.CustomerStatus default Active, - -- Auto-number - CustomerNumber: autonumber + -- Auto-number (REQUIRES a seed via `default N` — without it the build fails + -- CE7247 "Value cannot be empty") + CustomerNumber: autonumber default 1 ); / ``` @@ -73,7 +74,7 @@ create non-persistent entity Module.CustomerSearchParams ( | DateTime | `Name: datetime` | `CreatedAt: datetime` | | Date | `Name: date` | `BirthDate: date` | | Enumeration | `Name: Module.EnumName` | `status: Module.Status` | -| AutoNumber | `Name: autonumber` | `Code: autonumber` | +| AutoNumber | `Name: autonumber default 1` | `Code: autonumber default 1` (seed required) | | Binary | `Name: binary` | `FileData: binary` | | Hashed String | `Name: hashedstring` | `password: hashedstring` | @@ -253,8 +254,8 @@ create persistent entity Module.Product ( Category: string(50), Price: decimal ) -index idx_product_code on (Code) -index idx_product_category on (Category); +index idx_product_code (Code) +index idx_product_category (Category); / ``` @@ -293,7 +294,7 @@ create persistent entity Shop.Product ( -- Order entity create persistent entity Shop.Order ( - OrderNumber: autonumber, + OrderNumber: autonumber default 1, OrderDate: datetime not null, status: Shop.OrderStatus default Draft, TotalAmount: decimal, diff --git a/.claude/skills/mendix/migrate-design-prototype.md b/.claude/skills/mendix/migrate-design-prototype.md index ad15fd51a..2c6959875 100644 --- a/.claude/skills/mendix/migrate-design-prototype.md +++ b/.claude/skills/mendix/migrate-design-prototype.md @@ -45,10 +45,12 @@ spacing, font, component shapes. Do not invent styling the prototype doesn't sho ## Where the Theme Lives (read this first — it avoids the main friction) -- **Custom styles go inline in `theme/web/main.scss`, AFTER the `@import`s.** mxcli/the - agent generally **cannot create new SCSS partials** (only existing files are writable), - and styles placed after the imports win the cascade over Atlas defaults. Put all your - design tokens and component classes there. +- **Custom styles go in `theme/web/main.scss` AFTER the `@import`s, or in your own + partial.** Styles placed after the imports win the cascade over Atlas defaults. Once + `main.scss` grows, prefer splitting a partial out for readability: create + `theme/web/_.scss` and add `@import "";` after the Atlas imports (the same + cascade-order rule then applies within the partial). New partials **are** creatable — + keep the import order (custom after Atlas) and everything works. - Use a **project prefix** for every custom class and CSS variable so they never collide with Atlas or widget CSS. This project uses `ss-` (e.g. `.ss-panel`, `--ss-primary`). Pick one prefix and use it everywhere. @@ -416,6 +418,29 @@ container heatCell ( (Note the doubled single-quotes for string literals inside an MDL expression.) +### Computed dimensions — the bucket-class idiom + +A widget has **no computed inline style**: `Style:` is a static string and +`DynamicClasses:` returns *class names*, not CSS values. So anything with a +data-driven dimension — a progress bar width, a bar-chart height, a meter fill — +can't be `Style: 'width: {expr}%'`. The idiom is to **quantise the value into a +bucket and generate one class per bucket**: + +1. In a microflow, publish an integer bucket attribute (e.g. `0..20`): + `$obj/PctBucket = round($obj/Done / $obj/Total * 20)`. +2. Generate the classes once with an SCSS `@for` loop: + +```scss +@for $i from 0 through 20 { .ss-pb-#{$i} { width: $i * 5%; } } +``` + +3. Select the class from the bucket: + `DynamicClasses: '''ss-pb-'' + toString($currentObject/PctBucket)'`. + +Trade-off worth noting: this adds one bucket attribute per animated dimension to the +domain model. Pick a bucket count that matches the visual precision you need (20 → 5% +steps is usually plenty). + ### Adding classes to an existing page Use `alter page` to attach a class without rewriting the page (see `alter-page.md`): diff --git a/.claude/skills/mendix/patterns-data-processing.md b/.claude/skills/mendix/patterns-data-processing.md index 2425c4b0e..88b015521 100644 --- a/.claude/skills/mendix/patterns-data-processing.md +++ b/.claude/skills/mendix/patterns-data-processing.md @@ -269,7 +269,7 @@ begin -- Collect items to remove loop $item in $Items begin - if not $item/IsActive then + if not($item/IsActive) then add $item to $ToRemove; end if; end loop; diff --git a/.claude/skills/mendix/resolve-forward-references.md b/.claude/skills/mendix/resolve-forward-references.md index 354aef827..d5a3cc4d5 100644 --- a/.claude/skills/mendix/resolve-forward-references.md +++ b/.claude/skills/mendix/resolve-forward-references.md @@ -19,7 +19,7 @@ This applies to the following reference types: | `snippetcall` | page / snippet | snippet created after the page | | `show_page` in action | page / snippet | page created after the page that references it | | `SHOW PAGE` | microflow | page created after the microflow | -| `call microflow` | microflow | already works — microflow resolver checks in-session cache | +| `call microflow` | microflow | callee microflow created after the caller (in the same script, `exec` resolves the call against the project/backend, not later same-script definitions — so order the callee first) | > **Note:** `SHOW PAGE` inside a microflow body resolves the page reference at > microflow-creation time, not at invocation time. If the target page doesn't exist yet, diff --git a/.claude/skills/mendix/write-microflows.md b/.claude/skills/mendix/write-microflows.md index 690f6eb10..527152bfc 100644 --- a/.claude/skills/mendix/write-microflows.md +++ b/.claude/skills/mendix/write-microflows.md @@ -818,9 +818,14 @@ empty -- Null/empty value [%CurrentDateTime%] -- Current date/time [%CurrentUser%] -- Current user object toString($value) -- Convert to string -randomInt($max) -- Random integer ``` +> **No `randomInt`.** Mendix has no `randomInt` function. Use `random()` (returns a +> Decimal in [0,1)) and round to an integer range — e.g. a value in 0..8 is +> `round(random() * 8)`. Because `random()` is a Decimal, assigning it (or `div`, +> `secondsBetween()`, and the other `*Between` functions) directly to an `integer` +> variable fails the build with CE0117 — wrap it in `round()`/`floor()`/`ceil()`. + ## Complete Example ```mdl diff --git a/scripts/check-skill-mdl.sh b/scripts/check-skill-mdl.sh index 6787d2807..12eb98c34 100755 --- a/scripts/check-skill-mdl.sh +++ b/scripts/check-skill-mdl.sh @@ -126,7 +126,12 @@ while IFS= read -r md; do for stmt in "$WORK"/stmt_*.mdl; do [ -e "$stmt" ] || continue - first="$(grep -vE '^[[:space:]]*$' "$stmt" | head -1 | sed -E 's/^[[:space:]]+//' | tr 'A-Z' 'a-z')" + # First keyword = first line that is not blank, not a `/** … */` doc + # comment (the `create` statement's Documentation), and not a `--` line + # comment. Without skipping the doc comment, a `/** */`-prefixed DDL + # statement is misclassified as non-DDL and silently skipped — which let + # `index … on (…)` drift ship in mdl-entities.md (findings #4). + first="$(grep -vE '^[[:space:]]*($|/\*|\*|--)' "$stmt" | head -1 | sed -E 's/^[[:space:]]+//' | tr 'A-Z' 'a-z')" # Only in-scope statements: domain-model DDL, or complete CREATE/DROP of a # page or snippet. printf '%s' "$first" | grep -qE "$DDL_RE|$PAGE_RE" || { SKIPPED=$((SKIPPED + 1)); continue; } @@ -141,7 +146,7 @@ while IFS= read -r md; do # enforced here. if printf '%s' "$out" | grep -qiE 'Syntax errors found|mismatched input|no viable alternative|extraneous input|expecting'; then FAILED=1 - echo "FAIL: $md — $(grep -vE '^[[:space:]]*$' "$stmt" | head -1 | sed -E 's/^[[:space:]]+//')" + echo "FAIL: $md — $(grep -vE '^[[:space:]]*($|/\*|\*|--)' "$stmt" | head -1 | sed -E 's/^[[:space:]]+//')" printf '%s\n' "$out" | grep -iE 'mismatched|expecting|no viable|extraneous' | grep -iv 'vibe' | sed 's/^/ /' | head -2 fi done From b3de82d0b1521aebddedef9c76963c5b94aa2a4a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 16:39:09 +0000 Subject: [PATCH 2/3] docs: seed the autonumber example in generate-domain-model.md too (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Straggler for finding #6 — the audit-attributes example also showed `OrderNumber: autonumber` with no seed. Add `default 1` (an autonumber requires a seed or the build fails CE7247), matching the mdl-entities.md fix and the already-correct data-types row. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4 --- .claude/skills/mendix/generate-domain-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/mendix/generate-domain-model.md b/.claude/skills/mendix/generate-domain-model.md index eda3bcb0d..4bff018b0 100644 --- a/.claude/skills/mendix/generate-domain-model.md +++ b/.claude/skills/mendix/generate-domain-model.md @@ -226,7 +226,7 @@ Mendix supports four built-in auditing properties on persistent entities. Declar * Order with full audit trail */ create persistent entity Sales.Order ( - OrderNumber: autonumber, + OrderNumber: autonumber default 1, TotalAmount: decimal not null, status: enumeration(Sales.OrderStatus) not null, owner: autoowner, From 9165181a2ec603b63360edab37cab3046226b9f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 17:27:58 +0000 Subject: [PATCH 3/3] docs-site: fix invalid DELETE_CASCADE keyword surfaced by the check-skill hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hardened check-skill-mdl (which now classifies /** */-prefixed DDL blocks instead of skipping them) runs over docs-site/src too, and caught two `CREATE ASSOCIATION … DELETE_BEHAVIOR DELETE_CASCADE` blocks that fail `mxcli check` — `DELETE_CASCADE` is not a valid keyword. The real cascade behavior is `DELETE_AND_REFERENCES` (delete the referencing objects too). Replace DELETE_CASCADE → DELETE_AND_REFERENCES across the docs-site association pages (statement blocks, the behavior table, the CREATE ASSOCIATION synopsis, and the lexical-structure keyword list). docs-site now checks 209 blocks, all passing. This is exactly the drift the classifier fix is meant to catch. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4 --- docs-site/src/language/associations.md | 4 ++-- docs-site/src/language/lexical-structure.md | 2 +- docs-site/src/reference/domain-model/create-association.md | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs-site/src/language/associations.md b/docs-site/src/language/associations.md index 6125a5931..a39486b72 100644 --- a/docs-site/src/language/associations.md +++ b/docs-site/src/language/associations.md @@ -66,7 +66,7 @@ Controls what happens when an associated object is deleted. | Behavior | MDL Keyword | Description | |----------|-------------|-------------| | Keep references | `DELETE_BUT_KEEP_REFERENCES` | Delete the object, set references to null | -| Cascade | `DELETE_CASCADE` | Delete associated objects as well | +| Cascade | `DELETE_AND_REFERENCES` | Delete associated objects as well | ```sql /** Invoice must be deleted with Order */ @@ -74,7 +74,7 @@ CREATE ASSOCIATION Sales.Order_Invoice FROM Sales.Order TO Sales.Invoice TYPE Reference - DELETE_BEHAVIOR DELETE_CASCADE; + DELETE_BEHAVIOR DELETE_AND_REFERENCES; ``` ## Naming Convention diff --git a/docs-site/src/language/lexical-structure.md b/docs-site/src/language/lexical-structure.md index 18f1c5677..6b9e34604 100644 --- a/docs-site/src/language/lexical-structure.md +++ b/docs-site/src/language/lexical-structure.md @@ -13,7 +13,7 @@ BOOLEAN, BOTH, BUSINESS, BY, CALL, CANCEL, CAPTION, CASCADE, CATALOG, CHANGE, CHILD, CLOSE, COLUMN, COMBOBOX, COMMIT, CONNECT, CONFIGURATION, CONNECTOR, CONSTANT, CONSTRAINT, CONTAINER, CREATE, CRUD, DATAGRID, DATAVIEW, DATE, DATETIME, DECLARE, DEFAULT, DELETE, -DELETE_BEHAVIOR, DELETE_BUT_KEEP_REFERENCES, DELETE_CASCADE, DEMO, +DELETE_BEHAVIOR, DELETE_BUT_KEEP_REFERENCES, DELETE_AND_REFERENCES, DEMO, DEPTH, DESC, DESCENDING, DESCRIBE, DIFF, DISCONNECT, DROP, ELSE, EMPTY, END, ENTITY, ENUMERATION, ERROR, EVENT, EVENTS, EXECUTE, EXEC, EXIT, EXPORT, EXTENDS, EXTERNAL, FALSE, FOLDER, FOOTER, diff --git a/docs-site/src/reference/domain-model/create-association.md b/docs-site/src/reference/domain-model/create-association.md index 6339ff35b..4ddf5946f 100644 --- a/docs-site/src/reference/domain-model/create-association.md +++ b/docs-site/src/reference/domain-model/create-association.md @@ -7,7 +7,7 @@ TO to_module.to_entity TYPE { Reference | ReferenceSet } [ OWNER { Default | Both | Parent | Child } ] - [ DELETE_BEHAVIOR { DELETE_BUT_KEEP_REFERENCES | DELETE_CASCADE } ] + [ DELETE_BEHAVIOR { DELETE_BUT_KEEP_REFERENCES | DELETE_AND_REFERENCES } ] ## Description @@ -34,7 +34,7 @@ The `DELETE_BEHAVIOR` clause controls what happens when an object on the FROM si | Behavior | Description | |----------|-------------| | `DELETE_BUT_KEEP_REFERENCES` | Delete the object and set references to null | -| `DELETE_CASCADE` | Delete the object and all associated objects on the TO side | +| `DELETE_AND_REFERENCES` | Delete the object and all associated objects on the TO side | If `OR MODIFY` is specified, the statement is idempotent: if the association already exists, it is updated to match the new definition. @@ -94,7 +94,7 @@ CREATE ASSOCIATION Sales.Order_Invoice FROM Sales.Order TO Sales.Invoice TYPE Reference - DELETE_BEHAVIOR DELETE_CASCADE; + DELETE_BEHAVIOR DELETE_AND_REFERENCES; ``` ### Idempotent with OR MODIFY