Skip to content

Sync ako/main → upstream: MDL safety, fragments/building blocks, typed design properties, warm-loop debugging#785

Merged
ako merged 70 commits into
mendixlabs:mainfrom
ako:main
Jul 26, 2026
Merged

Sync ako/main → upstream: MDL safety, fragments/building blocks, typed design properties, warm-loop debugging#785
ako merged 70 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Brings the 39 commits on ako/main since the last upstream sync (#781). Clean
merge against main; make build && make test green.

Validation & DDL safety

  • create or modify entity data-loss guard + alter-path attribute checks
  • Idempotent ALTER ENTITY ADD/DROP ATTRIBUTE IF (NOT) EXISTS
  • MDL044: flag unknown expression functions + Decimal→Integer assignments at check time
  • Parser errors only flag real contraction leftovers as unescaped apostrophes
  • Syntax errors point at the fix, not just the location
  • Reject cross-module document-access grants (CE0148 guard)

Fragments & building blocks

  • SHOW / DESCRIBE BUILDING BLOCKS (read-only discovery) + building_blocks catalog table
  • USE BUILDING BLOCK v1 — deep-copy a block onto a page
  • Content-slot fragments — wrap arbitrary content in a reusable shell
  • Fragment parameter bindings + building-block rebind overrides (experimental)
  • Fix fragment/building-block/slot not expanding inside a container
  • CI-checked fragment slot/binding examples

Typed design properties / Atlas

  • Registry-driven value types + MDL-WIDGET11/12 check validation
  • Fix CE6084: ToggleButtonGroup design property value must be an option, not custom
  • Fix compound (nested) design properties silently dropped on write
  • atlas-design skill + Atlas-first prototype migration; use building block as the primary path

run --local / --hub warm loop

  • Runtime log to a file for debugging: JVM tee + file log subscriber + start_logging (findings TUI: yazi-style Miller columns, image collections, review fixes #25, end-to-end)
  • Re-serve the browser bundle after a structural --watch change
  • Reap child process groups so ports free on Ctrl-C
  • run --hub: re-register with the hub on heartbeat 404

Other

  • exec --continue-on-error so partially-applied scripts re-run
  • Docs/skills accuracy fixes surfaced by the sudoku test app; CI hygiene (gate bug-tests, rename misnamed fixture)

claude and others added 30 commits July 23, 2026 20:17
Record the multi-project-solution orchestration as slice 5: several apps of one
solution running side by side in a single container, each its own runtime +
database with auto-allocated ports, all registered under one --hub-solution and
grouped in the hub overview, with sibling URLs wired from the hub-assigned
subdomains via a mxcli.solution.yaml manifest driven by `mxcli run --solution`.

It's an orchestration layer over shipped primitives (per-project DB/deploy
isolation, port-triple flags, the slice-4 hub's solution grouping) — no new
runtime or hub mechanics. Also marks slices 3 & 4 verified-live and merged (PR #11).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The warm loop's three long-lived children — mxbuild --serve, the runtime JVM,
and the rollup web-client bundler — were each tracked by a single PID, and every
Stop() signalled only that direct child. Because mxbuild is a shell-script
wrapper that spawns a Temurin JVM grandchild, SIGTERM to the wrapper left the JVM
orphaned, still holding its port (6543/8080/8090). The next `mxcli run --local`
then refused to start with "port already in use".

Start each child in its own process group (Setpgid) and have Stop() signal the
whole group (negative-PID SIGTERM, then SIGKILL after the grace period), so
wrapper grandchildren are reaped too. Cross-platform via a Windows no-op stub
(no POSIX process groups there; the warm loop is a Linux-devcontainer feature).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Under `run --local --watch`, a structural model change (new/removed page, nav or
home change) went through a runtime restart, but the app then 404'd on
/dist/index.js and booted only the <noscript> shell until a clean full restart.
Cause: the serve Deploy build rewrites web/ source and clears web/dist, while the
incremental rollup bundler can report a "success" for an intermediate bundle that
does not match the final source. The watch-apply loop reported "build applied"
off that in-process signal with no check that the bundle was actually served.

Gate the "applied" report on the bundle being present on disk AND returning 200
from the app. When it isn't, recover with the reliable synchronous one-shot
BuildWebClient (the same path the non-watch boot uses) and re-probe before
reporting success. A pure model reload never touches web/dist, so it passes the
probe instantly — no cost on the common path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
docs(warm-loop): add slice 5 — multi-project solutions in one container
Land the atlas-design proposal — a taste + workflow skill layer that makes
mxcli-generated Mendix apps look designed by default (the Atlas-first method,
4-layer token architecture, recipe library, chart/dark-mode widget recipes, a
gotchas catalog, and a mandatory runtime-verify loop). It sits on top of the
page-styling, page-composition, building-blocks, and page-templates proposals
and consumes what they build rather than duplicating them.

Drafted from the Itinera / Atlas-MES build (test app ako/MxcliTest1). Two
corrections vs the original draft, verified against the codebase:
- all chart types (incl. Line/Bubble/Heatmap/TimeSeries) are MDL-authorable
  today; the "stick to the authorable four" note was stale.
- the two P0 warm-loop tooling bugs it listed are now fixed (PR #13), so those
  rows are marked done.

Regenerates the proposals README index.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Granting a page/microflow/nanoflow/published OData|REST service access to a
module role from a DIFFERENT module than the document passed `mxcli check` and
`exec`, then failed the Mendix build with an opaque CE0148 "reselect roles".
Document access (AllowedModuleRoles) may only reference the document's OWN
module roles — Studio Pro's role picker only offers those — so a cross-module
reference is invalid even though it is well-formed.

The grant handlers wrote `role.Module + "." + role.Name` verbatim, guarded only
by validateModuleRole (which checks the role exists in ITS module, not that it
matches the document's module). The MOVE path already enforced the same-module
rule by remapping (remapDocumentAccessRoles); GRANT had no equivalent.

Add checkDocumentAccessRolesSameModule and wire it into all five grant handlers
(page, microflow, nanoflow, OData service, published REST service). It rejects a
cross-module role with an actionable message that names the document's module and
suggests the own-module role — turning an opaque deploy-time failure into an
immediate, clear error. Reject (not silently remap): a GRANT is explicit, so a
wrong role or wrong document must not be substituted.

Tests: the pure guard (both branches) plus page/microflow/nanoflow handler tests
via MockBackend (cross-module rejected, no write; same-module still works).
Adds mdl-examples/bug-tests/ce0148-cross-module-grant.mdl and a fix-issue.md
symptom-table entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Add read-only discovery for Mendix Building Blocks (Pages$BuildingBlock),
mirroring the SHOW/DESCRIBE SNIPPET path. Building Blocks are the native
"reusable widget composition" library; this surfaces them so a project's
existing blocks can be discovered and inspected (the READ prerequisite for the
Atlas design-system recipe work — INSTANTIATE/AUTHOR remain separate).

- SHOW BUILDING BLOCKS [in module] — lists Qualified Name, Module, Name,
  Display Name, Platform, Category.
- DESCRIBE BUILDING BLOCK Module.Name — informational output (doc block +
  metadata comments + widget tree). Read-only: not a re-executable CREATE,
  since Building Blocks can't be authored via MDL.

Data layer: the storage $Type is Pages$BuildingBlock (per the generated
metamodel), not Forms$BuildingBlock as the reader previously assumed — so
ListBuildingBlocks now does a dual-type lookup (Pages$ first, Forms$ fallback),
fixing a latent empty-result bug. parseBuildingBlock reads DisplayName /
Platform / TemplateCategory scalars. The widget tree is read from the raw unit
(GetRawUnit) as a top-level Widgets[] array — the same shape and reader logic
as a Studio-Pro snippet — not from the typed parser.

Grammar: new BUILDING / BLOCK / BLOCKS lexer tokens (added to the keyword rule
so they remain usable as identifiers), plus showStatement / describeStatement
alternatives. Wired through AST, visitor, executor dispatch, and the CLI
describe registration (both $Type strings). Backend interface/impls unchanged
(ListBuildingBlocks + GetRawUnit already existed everywhere).

Tests: executor mock tests (list, module filter, describe-not-found) + a
check-parseable doctype example. Docs: MDL_QUICK_REFERENCE + a syntax feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Surface Building Blocks in the SQLite catalog so they are queryable via
`select ... from CATALOG.building_blocks` and listed by SHOW CATALOG TABLES.

- New building_blocks_data table + full-snapshot view (tables.go), registered
  in Catalog.Tables() as CATALOG.BUILDING_BLOCKS.
- buildBuildingBlocks() populates it (Id, Name, QualifiedName, Module, Folder,
  Description, DisplayName, Platform, Category, WidgetCount). WidgetCount is read
  from the raw unit's top-level Widgets array (full mode only).
- ListBuildingBlocks added to the CatalogReader interface (*mpr.Reader already
  implements it).

Deliberately NOT wired into widgets_data / the refs projection: a Building Block
is a read-only copy-template (deep-copied onto pages with no live link), so it
has no meaningful cross-reference edges — only a widget count. The SELECT path
needs no grammar change (catalogTableName already accepts IDENTIFIER).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The doctype integration harness (TestMxCheck_DoctypeScripts) execs each example
on both engines, and 36-building-block-examples.mdl failed two ways:

- modelsdk: `show building blocks` errored because the modelsdk backend's
  ListBuildingBlocks was an unimplemented stub. Implement it on *Backend
  (mprread over genPg.BuildingBlock + scalar metadata), mirroring ListSnippets,
  so SHOW/DESCRIBE BUILDING BLOCK work on the codec engine too. The widget tree
  still comes from the shared GetRawUnit, so read-only describe needs no more.
- legacy (and modelsdk): the example `describe building block MyModule.LoginForm`
  errored "building block not found" — a fresh test project ships no building
  blocks, and DESCRIBE of a missing block correctly errors. Make the example
  exec-safe: keep the `show building blocks` statements (empty, no error) and
  show DESCRIBE + the CATALOG.building_blocks query illustratively in comments.

Follow-up to the merged building-blocks feature (PR #16).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
fix(building-blocks): modelsdk ListBuildingBlocks + exec-safe doctype example
A user-facing skill (synced to Mendix projects via `mxcli init`) that makes a
generated app reach "designed product" quality, the way artifact-design does for
HTML and dataviz does for charts. It's the taste + workflow layer on top of
theme-styling (SCSS mechanics), create-page (widget syntax), fragments
(composition), and migrate-design-prototype (design handoff).

Grounded in the STANDARD Atlas design system every project already ships —
Atlas_Core classes + the 39 out-of-the-box Atlas_Web_Content building blocks —
rather than a bespoke per-app library, so it works in any project. Built on the
building-blocks READ capability (SHOW/DESCRIBE BUILDING BLOCK), and validated
against a real Atlas project (the inventory + the Card structure in the worked
example are real DESCRIBE output).

Covers: the Atlas-first thesis and "reach down the stack first"; the 4-layer
architecture (Atlas → brand tokens → identity SCSS → mandatory runtime verify);
the discover → inspect → mirror workflow over out-of-the-box blocks; the Atlas
class + typed-designproperties vocabulary; a Layer-1 brand-token scaffold and an
optional dark-mode widget-override scaffold (inline SCSS); the transparent-Plotly
chart theme; the run --local --watch --screenshot verify loop ("mx check is not
enough"); and the full gotchas catalog.

Honest about the current capability boundary: discovery works today, but
instantiation (USE BUILDING BLOCK) does not yet exist, so the skill teaches
mirroring a block's shape with classes + design properties + fragments, treating
the Atlas block as the canonical target — migrating to native instantiation when
USE BUILDING BLOCK lands.

Single self-contained file (the sync/embed only handles top-level skills/*.md).
Implements the atlas-design proposal (docs/11-proposals/PROPOSAL_atlas_design_system.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Mark the Building-Blocks proposal partial — READ (SHOW/DESCRIBE +
CATALOG.building_blocks) shipped in PR #16/#17 and is validated live against a
real Atlas app — and add the USE BUILDING BLOCK instantiate slice:

- Syntax mirroring `use fragment` (`use building block Mod.Name [as prefix_]`),
  a page-body element sourced from a persisted Pages$BuildingBlock.
- Deep-copy / no-live-link / prefix semantics.
- The configuration decision: afterwards via `alter page` (v1), an optional
  inline override block (v1.1), a possible `over database …` data-context
  shortcut — and explicitly NOT implicit slot inference.
- v1 implementation approach: reuse the fragment-expansion hook; convert the
  block's BSON widgets to []*ast.WidgetV3 by round-tripping through the DESCRIBE
  renderer + visitor.Build (verified re-parseable), not a hand-written converter.

Also corrects the doc's stale storage-type (Pages$BuildingBlock) and refreshes
the proposals README index.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Instantiate an out-of-the-box Atlas building block onto a page in one line,
instead of hand-mirroring its DESCRIBE output. Mirrors `use fragment`:

    use building block Atlas_Web_Content.Card
    use building block Atlas_Web_Content.Card as cust_

It's a page-body element (valid anywhere `use fragment` is), sourced from a
persisted Pages$BuildingBlock rather than a script-scoped `define fragment`. v1
is deep-copy only (+ optional `as <prefix>` rename); configure the copied widgets
afterwards with `alter page` (inline config is the v1.1 slice in the proposal).

Wiring mirrors use fragment: a `useBuildingBlockRef` grammar rule → a
USE_BUILDING_BLOCK sentinel WidgetV3 in the visitor → expansion in the same
expandIfFragment hook. The block's BSON widgets are turned into []*ast.WidgetV3
by round-tripping through the DESCRIBE renderer (render → `define fragment`
wrapper → visitor.Build → .Widgets) rather than a hand-written BSON→AST
converter, so it reuses the full widget parser and degrades to a clear parse
error. The prefix rename reuses prefixWidgetNames.

Verified end-to-end against the real Atlas_Web_Content.Card on a live 11.12.1
project: the container + dynamictext deep-copy with the cust_ prefix, preserving
the card-title class and the 'Card style' design property. Unit tests
(mock-backend) cover prefix/no-prefix/not-found and the parse.

Notes: requires MXCLI_ENGINE=legacy until the modelsdk ListBuildingBlocks lands
(PR #17). Compound/nested design-property values (e.g. Spacing: [...]) are a
pre-existing create-page writer gap (they drop on direct authoring too), out of
scope here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…rimary path

Now that USE BUILDING BLOCK v1 exists, the skill leads with the one-line
instantiation instead of the hand-mirror workaround:

- Workflow renamed discover → inspect → USE (was → mirror). Step 3 is now
  `use building block Mod.Name [as prefix_]` (deep-copy), step 4 configures the
  copied widgets with `alter page`. Mirroring is reframed as the explicit fallback
  (hand-tuning, or the modelsdk engine before its building-block support lands).
- Capability table: Instantiate → ✅ v1 (deep-copy; configure afterwards; legacy
  engine today). Capability note updated to "discovery AND instantiation both work".
- Validation checklist updated to prefer `use building block` + `alter page`.

All `use building block` / `alter page` examples are bare page-body lines, which
the skill-MDL checker auto-skips (first keyword not in its create/alter/drop
entity|page checkable set) — verified with a binary built WITHOUT the grammar, so
this PR's CI is green even before the USE BUILDING BLOCK feature (#19) merges.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…veat, refresh status

Two verified doc fixes from the atlas-design proposal's P2 list, plus a
status refresh of the proposal itself:

- create-page.md: correct the stale chart note — Line/Bubble/Heatmap/TimeSeries
  ARE MDL-authorable (via the line/scalecolor object-lists; see
  34-chart-widget-examples.mdl), not "use Studio Pro for those". Add the
  Slider/RangeSlider `showTooltip: false` caveat (React findDOMNode removed in
  MX 11 → "Could not render widget" on drag, a runtime crash mx check misses).
- PROPOSAL_atlas_design_system.md: mark the shipped work done in the
  "mxcli work required" table — CE0148 grant guard (PR #15), Building Blocks
  READ (PR #16/#17) and INSTANTIATE (PR #19), and the two P2 doc fixes above.
  Bump status proposed → partial (the skill shipped in PR #18); list related PRs.
  Refresh the proposals README index.

Remaining open proposal items: parameterized fragments, chart theme colourway,
CREATE BUILDING BLOCK (author), typed designproperties, lint rules, and
USE BUILDING BLOCK v1.1 (inline override + data-context shortcut).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
An inline compound design property on a widget — e.g. the Atlas Spacing
group `designproperties: ['Spacing': ['margin-bottom': 'Large', ...]]`,
and any block copied in via `use building block` — was silently dropped
by the legacy builder write path. `check`, `exec`, and even `mx check`
all passed, but the nested styling vanished from the .mpr; only flat
toggle/option props survived.

Root cause: serializeDesignProperties switched on p.ValueType with cases
for toggle/option/custom and a `default: continue` that dropped the
`compound` kind entirely. A compound value nests its sub-properties in a
Forms$CompoundDesignPropertyValue whose Properties list has the same
marker-prefixed Forms$DesignPropertyValue shape as the outer array.

Add a `case "compound"` that emits Forms$CompoundDesignPropertyValue and
recurses via serializeDesignProperties(p.Compound). The AST, visitor, and
builder already produced the nested model — only the serializer's
terminal switch dropped it. This is the CREATE write counterpart to the
describe read-back fix in mendixlabs#668.

Verified: mxcli docker check = 0 errors on 11.12.1 (authoritative), plus
a write->describe round-trip preserves Spacing on both engines.

Test TestSerializeDesignProperties_Compound; bug-test
mdl-examples/bug-tests/compound-designproperties.mdl; symptom table row
added to fix-issue.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
A plain fragment substitutes a fixed widget group. A content slot lets a
fragment WRAP arbitrary caller-supplied content — a reusable shell (an
Atlas card, panel, or section) whose body varies per use. Neither a plain
fragment nor a Mendix Snippet can express this (Snippets are context-entity
only, no content slot), so the design-recipe library previously needed
hand-written .mdl copy-and-fill for every content-varying block.

Syntax:
  define fragment Card as {
    container cardWrap (class: 'card', designproperties: ['Card style': on]) {
      slot content            -- caller's widgets land here
    }
  };
  use fragment Card {
    dynamictext cardHeading (content: 'Welcome', rendermode: H2)
    dynamictext cardText (content: 'Wrapped content')
  }

Semantics (v1):
  * slot name optional (defaults to "content"); one slot per fragment
  * use fragment with no payload leaves the slot empty (valid shell)
  * payload supplied to a slotless fragment is an error
  * payload is deep-cloned; `as prefix_` still renames the fragment's own
    widgets, not the caller's payload
  * resolves entirely at fragment-expansion time — no BSON changes; describe
    page shows the fully-expanded tree (slot marker gone)

Implementation is confined to the AST-expansion layer: a SLOT sentinel rides
in the fragment tree, the `use fragment X { … }` payload rides on the
USE_FRAGMENT sentinel's Children, and expandFragmentRef splices the payload
into the slot before buildWidgetV3 ever runs. Full stack: SLOT lexer token
(+ keyword rule so it stays a usable identifier), slotMarkerV3 +
useFragmentPayload grammar, visitor builders, executor splice + guards.

Verified: mxcli docker check = 0 errors on a real Mendix 11.12.1 project;
write->describe round-trip preserves the wrapped tree; all three edge cases
(no-slot-with-payload, bare slot, empty slot) behave. Tests in
visitor_fragment_test.go and fragment_test.go; example in
15-fragment-examples.test.mdl. Docs: fragments.md and atlas-design skills,
page-composition proposal (now leads with the content slot; scalar params
deferred to v1.1), atlas proposal open-items table, and syntax help
(fragment.slot topic).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…perimental)

Content slots vary a component's structure; this adds the data and behavior
axes. A fragment can now declare typed datasource/action parameters, and a
`use building block` can rebind the block's datasource and primary button —
turning a reusable shell into a real component bound to a specific entity and
microflow per use.

Fragment parameters:
  define fragment DataPanel($data: datasource, $onEdit: action) as {
    container panel (class: 'card') {
      listview lv (datasource: $data) {
        slot content
        actionbutton edit (caption: 'Edit', action: $onEdit, buttonstyle: primary)
      }
    }
  };
  use fragment DataPanel ($data: database Sales.Order, $onEdit: microflow Sales.Edit) {
    dynamictext heading (content: 'Orders', rendermode: H4)
  }

Building-block rebind overrides:
  use building block Atlas_Web_Content.List_Cards
    (datasource: database Sales.Order, action: microflow Sales.Open) as orders_;

Semantics: values substitute at fragment-expansion time (no BSON changes;
describe shows the concrete datasource/action, no $param). Every declared
parameter must be supplied; unknown args and type mismatches are errors. A
microflow/nanoflow value parses as a datasource (grammar overlap on those two
keywords) and is reinterpreted as a call action when the parameter's kind is
action. Building-block binding-point rule: datasource → first widget carrying a
datasource; action → first button widget (Atlas placeholder buttons ship with
no action, so the target is matched by widget type, not an existing Action).

Full stack: actionExprV3 gains a $var alt; fragmentParams / fragmentArgs /
blockOverrides grammar; AST FragmentParam + FragmentArg + param-action;
visitor builders; executor substituteFragmentParams (with validation) and
rebindFirst. Sentinel-internal keys (Args, DataSourceOverride, ActionOverride)
added to the MDL-WIDGET07 allowlist.

Verified end-to-end on a real Mendix 11.12.1 project: mx check = 0 errors for a
param-bound fragment page AND for datasource + action building-block rebinds;
describe round-trip shows the resolved bindings; four error cases (missing arg,
unknown arg, type mismatch, args-to-no-param-fragment) behave. Tests in
visitor_fragment_test.go and fragment_test.go; example LEVEL 5c/5d in
15-fragment-examples.test.mdl. Docs: fragments.md and atlas-design.md skills
(marked experimental), page-composition proposal (Parameter Bindings section
with open design points), and syntax help (fragment.params topic).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
A `use fragment` / `use building block` (or a content `slot`) nested inside a
container, layout column, or dataview — anywhere but the page-body top level —
failed exec with "unsupported widget type: USE_FRAGMENT". expandFragments only
ran on the top-level widget list; the layout/container builders build children
via buildWidgetV3 directly, which has no case for the sentinel types.

This crippled the natural usage — a reusable card/panel could not be placed
inside a layout column — and affected content slots and building blocks too.

Fix: expandFragments now recurses into every widget's children, so the tree is
fully expanded to concrete widgets before buildWidgetV3 runs (no builder needs
a sentinel case). Expansion is idempotent on concrete widgets, so re-traversing
an already-expanded slot payload is harmless.

Verified: nested cards inside a layoutgrid column exec + mx check = 0 on a real
11.12.1 project. Test TestExpandFragments_NestedInsideContainer; bug-test
mdl-examples/bug-tests/nested-fragment-expansion.mdl; symptom-table row added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…ing P004

CI (push-test.yml) runs `mxcli check` over doctype-tests/*.mdl but skips
*.test.mdl, so the slot/binding examples buried in 15-fragment-examples.test.mdl
had no CI coverage. Extract them into focused, self-contained plain .mdl files
that CI checks:

  * 15b-fragment-slots-examples.mdl    — DEFINE FRAGMENT … SLOT, USE FRAGMENT { … },
                                          empty slot, prefixed reuse, nested-in-layout
  * 15c-fragment-bindings-examples.mdl — ($data: datasource, $onEdit: action) params,
                                          USE BUILDING BLOCK (datasource:/action:) rebind

Both pass `mxcli check` and exec + mx check = 0 on a real 11.12.1 project. The
old .test.mdl levels are replaced with a pointer to the focused files.

Also fixes a pre-existing error in 15-fragment-examples.test.mdl: P004_Customer_New
bound a data view to a database source (MDL-WIDGET09 — a data view shows one
object). It now takes a page parameter, so the whole file passes `mxcli check`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
ako and others added 28 commits July 25, 2026 11:53
…a-loss guard

Two sudoku-app findings, both about entity DDL that passes `mxcli check`
but bites later:

#6 (alter path): the per-attribute checks (MDL021 reserved words, MDL022
AutoX rename, MDL023 autonumber seed) only ran on CREATE ENTITY, so an
attribute added via `alter entity ... add attribute` escaped all of them —
a seedless autonumber returned "Check passed!" then failed the build with
CE7247. Extract the CREATE loop body into validateEntityAttribute and add
ValidateAlterEntity for the ADD ATTRIBUTE path, wired into both `check`
and the LSP. The entity kind is unknown from ALTER, so persistent-only
MDL020 is skipped; the kind-independent checks all run.

#24 (data loss): `create or modify entity` rebuilds the entity from the
statement alone and replaces the stored one, so any attribute the
statement omits is silently dropped (36->2 in practice), later surfacing
as CE1613 on widgets still bound to them. Make it warn (non-blocking — the
user asked to modify) and list the dropped members, pointing at
`alter entity ... add attribute` for incremental edits. Also fix the
"already exists" message so it recommends `alter entity` for a member
change and reserves `create or modify` for a full replace.

Adds unit + mock tests and bug-test examples for both, plus fix-issue
symptom rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…trophes

looksLikeUnescapedApostrophe matched any 1-4 char lowercase token as a
contraction leftover, so a genuine parse error on a short lowercase MDL
keyword/identifier (mismatched input 'on'/'in'/'as'/'to'/'by') drew the
"unescaped apostrophe" hint and sent the user hunting for a quote that
isn't there (findings #4).

Restrict the match to the fixed contraction-suffix set (s/t/d/m/re/ve/ll —
the leftovers from it's, don't, he'd, I'm, you're, we've, you'll). Real
apostrophe errors still hint; short real keywords no longer do. Drop the
now-unused isLowerAlpha helper; add negative test cases for the keywords.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Findings #10: `exec` halted on the first error, so re-running a domain
script that is 90% already-applied (e.g. "attribute already exists")
applied none of the remaining 10%.

Add ExecuteProgramContinueOnError: it attempts every statement, reports
each failure prefixed with its statement number, and returns a summary;
the CLI exits non-zero when any statement failed. Unlike a blanket
error-swallow, every failure is still surfaced, so a real mistake stays
visible. `exit`/`quit` (ErrExit) still stop the run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Findings #10: with no way to re-apply a domain delta, re-running a script
that adds an attribute erroring "already exists" was a hard error.

Add IF NOT EXISTS to ADD ATTRIBUTE and IF EXISTS to DROP ATTRIBUTE
(grammar + AST + visitor + executor): when the attribute is already
present / already gone, the op is skipped with a notice instead of
erroring, so a domain script re-runs cleanly and converges. The plain
forms still error, so the guard is explicit and opt-in.

Includes visitor + executor tests, a bug-test example, and syntax/quick-
reference/symptom-table docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…ignments

Two sudoku-app findings about microflow expressions that pass `mxcli check`
but fail the build with CE0117:

#1 — a call to a name that is not a Mendix expression function (e.g. the
non-existent `randomInt()`) was ignored by the checker. Add
exprcheck.UnknownFunctionCalls: it walks the expression tree and flags any
CallExpr whose name is not in funcTable (which lists every built-in — a
bare name(...) in a Mendix expression is always a built-in call), with a
Levenshtein/prefix "did you mean random()?" hint. Surfaced as MDL044 from
the microflow validator (declare/set/return/if-condition).

#2 — a Decimal-returning function (random(), the duration *Between family)
assigned to an Integer/Long variable fails CE0117, but the check only
caught bare `div`. Add SourceRejectedForIntegerTarget (arithmetic Decimal
OR a non-rounding Decimal-returning built-in) and use it in MDL041. Also
correct the funcTable return kinds: secondsBetween/minutesBetween/
hoursBetween/daysBetween/weeksBetween are Decimal in Mendix (the calendar
variants stay Integer, millisecondsBetween stays Long).

Fixes 519-rest-mapping-as-list-of.mdl, which used a non-existent count()
in an expression (now returns the list directly). Adds exprcheck +
validator tests, two negative-test examples, and skill/symptom docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Findings #4: `index name on (cols)` — the SQL-like form documented for
entity indexes — failed with "extraneous input 'on' expecting '('",
because indexDefinition had no ON token. The bare `index name (cols)`
worked, so the documented ON form was the only broken variant.

Make ON optional in indexDefinition (IDENTIFIER? ON? LPAREN … RPAREN),
regenerate the grammar. It covers both CREATE (entityOption) and ALTER
ENTITY ADD INDEX (shared rule). buildIndex reads columns from
IndexAttributeList only, so ON can't be mistaken for a column and no
visitor change is needed; the bare form still parses.

Adds visitor tests, a bug-test example, and syntax/quick-reference/skill/
symptom-table docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`retrieve … where [Seq = $Game/MoveSeq + 1]` failed with a bare
`mismatched input '+' expecting ']'`, with no hint that Mendix XPath
constraints take a literal/token/variable/path on the value side — never
an arithmetic expression. This is a Mendix limitation, not an mxcli bug,
so adding grammar support would only let through XPath that mxbuild
rejects.

Add an error hint keyed on `mismatched input '<+|*|div|mod>' expecting
']'` (the `expecting ']'` shape only occurs inside a bracketed
constraint) that names the limitation and the workaround: compute the
value into a variable first, then compare against it. Documented in
xpath-constraints.md; visitor test + negative bug-test example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The migration skill was custom-SCSS-first — it had you hand-write .ss-*
component classes for cards/panels/buttons and demoted design properties to
a one-line aside, the exact "reinvent what Atlas gives you for free" pattern
atlas-design.md is written to prevent. The two skills also didn't link.

Reframe it Atlas-first without dropping the (valid) custom-SCSS identity
layer:
- Link atlas-design.md as the first related skill ("read first").
- Add an Atlas-first principle after the golden rule: prefer building blocks
  → Atlas classes / typed designproperties (spacing-*, flex-*/align-*,
  'Card style': on, …) → brand-token retune → custom .ss-* SCSS for identity
  only (layer 4).
- Step ② now leads with "check for an Atlas block/class first"; custom class
  is the fallback for shapes Atlas can't express.
- Widget map: card → Atlas Card / class:'card'; simple row/column → flex
  utilities (no layoutgrid); layoutgrid/display:grid reserved for real
  multi-column/fractional layouts.
- Design-property gotcha upgraded from "if you use it" to a recommendation,
  noting mxcli check -p validates keys/values (MDL-WIDGET11/12).

Docs only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Design properties were written fully free-form: a flat value always
serialized as an option, so a ColorPicker/ToggleButtonGroup property got
the wrong BSON $Type for Studio Pro's Appearance tab, and a typo'd key or
value (both case-sensitive) sailed through mxcli check.

Root cause of why the existing theme registry was unused: resolveDesignPropsKey
upper-cased the MDL keyword but the lookup map is lowercase-keyed, so
`container` never resolved to `DivContainer`. That left resolveDesignPropertyValueType
as dead code and also made `show design properties <widget>` report "not
found" for valid widgets. Fixed to lower-case the lookup.

- Write path: astDesignPropToValue now takes the widget's theme properties
  and resolves each flat value's type from metadata (ColorPicker /
  ToggleButtonGroup -> custom), falling back to option when no project /
  themesource (backward compatible).
- Check (-p only, when themesource defines properties): new
  ValidateDesignProperties walks page/snippet/alter-page widget trees and
  warns on MDL-WIDGET11 (unknown key, with a case-sensitivity hint / the
  list of valid keys) and MDL-WIDGET12 (invalid value, listing the allowed
  values). Warnings, not errors, for forward-compat with newer themes.
  Compound entries and widgets without type-specific metadata are skipped.
- LSP: caches the theme registry once per session (no per-keystroke I/O)
  and surfaces the same diagnostics.

Adds executor unit tests, an example, and theme-styling / symptom-table docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Findings #25: the warm loop prints its own progress but the Mendix
runtime's own stdout/stderr — server stack traces and microflow LOG
output — went only to an in-memory buffer surfaced solely on a startup
failure. So when a page action threw, the browser showed the generic
error dialog with nothing to correlate against, and a server-side bug
couldn't be told apart from a client one.

Tee the runtime JVM's stdout+stderr to <projectDir>/.mxcli/runtime.log
via io.MultiWriter(buffer, file): the in-memory buffer still backs
startup-error reporting, and the file makes the running app debuggable
(tail -f). Appended across restarts with a "=== runtime start … ==="
marker; the handle is closed on Stop and reopened on restart. On by
default; --runtime-log <path> relocates it and "-" disables it. The path
is printed at boot.

Adds TestOpenRuntimeLog and run/run-local docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…tured

The runtime-log tee added earlier writes the runtime JVM's stdout/stderr to
.mxcli/runtime.log, but a standalone runtime (launched via runtimelauncher.jar)
attaches no log subscriber by default — unlike a Studio Pro / m2ee run, which
calls create_log_subscriber after start. Mendix application logs flow to log
subscribers, not stdout, so microflow LOG output and server-side exception
stack traces never reached the file: a page-action error still showed only the
generic Mendix dialog with nothing to correlate against.

After a successful start (and on every restart's Start, since each fresh JVM
starts with no subscriber), attach a "file" log subscriber pointing at the same
runtime.log with autosubscribe INFO. max_rotate is 0 so the runtime never
renames the file out from under the JVM tee's open handle; the path is passed
absolute because the runtime's cwd is <install>/runtime, not mxcli's. The
attach is best-effort — a logging failure warns to stdout rather than failing
an otherwise-healthy start.

Tests: TestStart_AttachesLogSubscriber (params), TestStart_NoLogSubscriberWhenUnset,
TestStart_LogSubscriberFailureNonFatal. Docs: run-local skill, docs-site, and the
fix-issue symptom table (findings #25, round 2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…, not custom

The typed-design-properties write path mapped a design property's BSON value
type from its declared control type: ToggleButtonGroup and ColorPicker both
became "custom". That is wrong for ToggleButtonGroup — a toggle-button-group
selection picks one of a fixed option set, so Studio Pro stores it as an Option.
A Custom value type mismatches the declaration and mx check fails CE6084
("Expected design property Flex container / Column gap / … to be of type Toggle
button group, but found Custom") on any Atlas flex/spacing/typography property,
e.g. 'Column gap': 'Medium'.

This broke TestMxCheck_DoctypeScripts on 12-styling-examples,
15c-fragment-bindings-examples, and 31-pluggable-datagrid-gallery-v010-examples
(both engines) — a make test-integration failure that unit tests didn't catch.

The value type is decided by the value, not the control type. Make
resolveDesignPropertyValueType value-aware: a value that is one of the
property's declared options serializes as "option" for every control type
(Dropdown, ToggleButtonGroup, and a ColorPicker's predefined swatches);
only a ColorPicker's off-list value (a free-form hex/color) is "custom";
everything else stays "option"; no metadata falls back to "option". Reuses the
validator's themeOptionAllowed helper for consistency.

Verified: all three doctype examples pass mx check with 0 errors on both the
legacy and modelsdk engines. TestAstDesignPropToValue_Typed extended with the
'Column gap': 'Medium' regression case and ColorPicker swatch-vs-hex cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…livers

Registering a file log subscriber (findings #25, round 2) was necessary but not
sufficient: a standalone runtime boots with the logging subsystem NOT started,
so the subscriber sits inert and receives nothing. runtime.log still held only
the JVM banner — a probe microflow's LOG output and a forced runtime exception
produced zero lines. The isolation test in the findings doc pins it exactly:
boot+subscriber → 0 probe lines; then call start_logging → 6 lines.

After create_log_subscriber, call the start_logging admin action in the same
step (order: create subscriber → start_logging). An "already started" response
is treated as success, because Start re-runs on the DB-update retry and restart
paths against a still-running JVM. Still best-effort — a logging hiccup warns to
stdout and never fails an otherwise-healthy start.

Renamed attachFileLogSubscriber → configureRuntimeLogging since it now does
both. Tests: TestStart_StartLoggingAlreadyStartedIsSuccess; TestStart_Attaches-
LogSubscriber now asserts the [start, create_log_subscriber, start_logging]
sequence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
@ako
ako merged commit 3c9f45e into mendixlabs:main Jul 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants