Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c8f6bc4
test(docker): gate the MPRv2 Build guard on Mendix >= 11.6.1
claude Aug 1, 2026
eff48f9
docs(web): note hub credentials belong on the environment, never in t…
claude Aug 1, 2026
d4bf786
Merge pull request #73 from ako/claude/fix-nightly-1024-build-test
ako Aug 1, 2026
20cd6aa
Merge pull request #74 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 1, 2026
4f7e7c0
fix(pages): honour DataView FormOrientation and showFooter (#762, #813)
claude Aug 1, 2026
b736c86
fix(pages): stop wrapping layout placeholders in a synthetic containe…
claude Aug 1, 2026
d0e3824
fix: settings writes stored properties Mendix cannot resolve (#759)
claude Aug 1, 2026
2445f3e
Merge pull request #75 from ako/claude/dataview-762-813
ako Aug 1, 2026
d258faa
fix: settings writes corrupted private constant overrides
claude Aug 1, 2026
86060d2
docs: document shared vs private constant values; mark them in SHOW
claude Aug 1, 2026
ce41a25
Merge remote-tracking branch 'origin/main' into claude/layout-placeho…
claude Aug 1, 2026
96b320c
Merge pull request #76 from ako/claude/layout-placeholder-wrapper-760
ako Aug 1, 2026
bb556ee
Merge remote-tracking branch 'origin/main' into claude/settings-confi…
claude Aug 1, 2026
4f09b52
Merge remote-tracking branch 'origin/main' into claude/private-consta…
claude Aug 1, 2026
87b7472
feat(tunnel-hub): group previews by Claude Code session + retain past…
claude Aug 1, 2026
6dda0c8
Merge remote-tracking branch 'origin/main' into claude/settings-confi…
claude Aug 1, 2026
5afc5ba
build: resolve fix-issue symptom-table collisions with a union merge …
claude Aug 1, 2026
228bc48
build: restore the .gitattributes rules the union commit clobbered
claude Aug 1, 2026
9c97cae
Merge remote-tracking branch 'origin/main' into claude/private-consta…
claude Aug 1, 2026
d1fe240
build: give fix-issue.md a union merge driver
claude Aug 1, 2026
3e6c740
Merge pull request #77 from ako/claude/private-constant-values
ako Aug 1, 2026
e0cad96
Merge pull request #79 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 1, 2026
6da781f
Merge remote-tracking branch 'origin/main' into claude/settings-confi…
claude Aug 1, 2026
cba1f76
docs: correct the symptom-table conflict guidance
claude Aug 1, 2026
9d1022c
Merge pull request #78 from ako/claude/settings-configuration-759
ako Aug 1, 2026
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
38 changes: 28 additions & 10 deletions .claude/skills/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,32 @@ to the symptom table below, so the next similar issue costs fewer reads.
6. After the fix: **add a new row** to the table if the symptom is not already covered.
**Append it at the END of the table, never at the top.**

> **Why the end matters.** Every bug fix touches this one file, and for a long time
> new rows went in directly under the header. Two branches fixing two unrelated bugs
> therefore inserted at the *same line*, and git cannot merge that — it is a conflict
> by construction, not by bad luck. It cost five separate resolution rounds in one
> week, and each round risks dropping someone's row.
> **Conflicts are handled by git, not by where you insert.** Every bug fix touches
> this one file, so two branches fixing unrelated bugs write to the same place and git
> raises a conflict. That cost five resolution rounds in one week when rows went in
> under the header — and moving them to the end did **not** fix it: both sides still
> append to the same line, so the collision moved with the convention. PRs #76, #77
> and #78 each hit it again afterwards.
>
> Appending puts each branch's insert at a different offset, which git merges without
> help. The table is unordered — it is looked up by matching a symptom, not by
> reading top to bottom — so position carries no meaning and appending costs nothing.
> The actual fix is in `.gitattributes`:
>
> The rows above pre-date this convention and are left as they are; reordering them
> would conflict with every open branch at once, which is the problem, not the fix.
> ```
> .claude/skills/fix-issue.md merge=union
> ```
>
> git's built-in `union` driver keeps **both** sides of a conflicting hunk instead of
> raising a conflict. Two fixes that each append a row now merge with no intervention
> and both rows present — verified by merging a simulated later fix into an open
> branch. Where you insert no longer affects merging at all.
>
> Append at the end anyway, for a human reason: it keeps a fix's diff readable and the
> table roughly chronological. The table is looked up by matching a symptom, not read
> top to bottom, so position carries no meaning.
>
> **One caveat.** `union` applies to the whole file, so two branches editing the same
> *prose* line would silently keep both rather than conflict — a visible duplicated
> line, not corruption. If that starts happening, split the table into its own file so
> `union` covers only append-only content.

---

Expand Down Expand Up @@ -262,6 +276,10 @@ to the symptom table below, so the next similar issue costs fewer reads.
| A datagrid **column** with an explicit empty caption (`Caption: ''`) passes `mxcli check` but `mx check` rejects the page with **CE0463** "The definition of this widget has changed" on the Data grid 2 (error points at the widget version, not the caption). Omitting the caption, or a non-empty string, both build clean | The pluggable widget engine's column-header fallback treated a **present-but-empty** header property as "has header" and skipped the attribute-name default — so `Caption: ''` emitted an empty header (which Studio Pro rejects) while an **absent** caption got the fallback. The keyword datagrid path already handled it (`if caption == "" { caption = col.Attribute }` in `datagrid_column.go`) | `mdl/executor/widget_engine.go` (`applyColumnHeaderFallback`) | **Write-path fix.** Detect an empty header (a `texttemplate` op with empty `TextTemplate` and no `Parameters`) and treat it like an absent one: fill it **in place** with the bound attribute's leaf name (not appended — that would duplicate the `header` prop). A header WITH params (`Caption: '{1}'`) is left untouched. Result: `Caption: ''` now behaves like omitting it. **Round 2 (custom-content columns):** the first fix left a column with **no bound attribute** (an action/custom-content column) untouched — it had nothing to derive a header from, so an empty OR absent caption still tripped CE0463 (a custom-content column requires a non-empty header). Fixed by falling back to the **column's own name** when there's no attribute, and **gating the whole fallback on the item template having a `header` slot** (`mapping.ItemProperties`) so header-less object-list items (chart series, accordion groups) are never given a spurious header. `applyColumnHeaderFallback(spec, columnName, hasHeaderSlot)`. Test `TestApplyColumnHeaderFallback` (cases 1–8); examples `ledger-54-empty-column-caption.mdl` + custom-content verified via exec. **Verified: `mx check` → 0 errors on 11.12.1 for attribute columns AND custom-content columns (empty + absent caption)** (previously CE0463). Ledger finding #54 (custom-content columns) |
| Follow-up to round 2 (#25 re-test): the subscriber is registered but `runtime.log` **still** holds only the 4 JVM-banner lines — a probe microflow's `log info/warning/error` and a forced runtime exception produce **zero** lines. Isolation test: boot+subscriber → 0 probe lines; then call `start_logging` → 6 lines | A standalone runtime boots with logging **not started**, so a registered subscriber sits **inert** — nothing is delivered until the logging subsystem is activated. `create_log_subscriber` alone is necessary but not sufficient | `cmd/mxcli/docker/runtime_controller.go` (`configureRuntimeLogging` — renamed from `attachFileLogSubscriber`; now also calls `start_logging`) | After `create_log_subscriber`, call `CallM2EE(c.opts, "start_logging", nil)` in the same step (order: create subscriber → start_logging). Treat an "already started" response as success (`start` re-runs on the DB-update retry / restart paths on a still-running JVM). Still best-effort (warn to Stdout, never fail an up runtime). Test `TestStart_StartLoggingAlreadyStartedIsSuccess`; `TestStart_AttachesLogSubscriber` now asserts the `[start, create_log_subscriber, start_logging]` sequence. **Diagnosis pattern**: when a registered sink receives nothing, check whether the subsystem that feeds it is even *running* — registration ≠ activation. Findings #25 (round 3) |
| `mxcli new --version X` prints "Resolving MxBuild X..." and then produces a project at a **different** Mendix version — silently. Every later step (init, mxbuild, runtime, `run --local`) follows the wrong version | `ResolveMxForNewProject` delegated to `ResolveMxForVersion`, whose last resort is `AnyCachedMxPath()` — *any* cached mx, of any version. That fallback is fine when the project already exists and its version is a preference; for `new` the requested version **is** the output, because `mx create-project` stamps the project with the version of the binary that ran it | `cmd/mxcli/docker/check.go` (`localMxForVersion`, `ResolveMxForNewProject`) + `cmd/mxcli/cmd_new.go` (postcondition) | Resolve **exactly** the requested version for `new` (exact Studio Pro install → exact versioned install path → exact download cache; **not** PATH, which carries no version guarantee), and download otherwise. Then check the postcondition: reopen the created `.mpr`, compare `ProductVersion` to `--version`, and fail loudly on a mismatch — resolution bugs are invisible without it. **Generalisable**: when a flag names the version/identity of the artifact being produced, a "close enough" local substitute is never valid, and the produced artifact should be verified against the request rather than the resolution trusted. Found while reproducing #812 in a browser — cost a full project rebuild before it was noticed |
| A **DataView** property parses, passes `mxcli check`, and has no effect — `FormOrientation: Vertical` (#762) or `showFooter: true` (#813). `FormOrientation` works under `--engine legacy` | Two different causes that look identical. (a) `FormOrientation` has no BSON field: Studio Pro's radio **is** `LabelWidth` (0=Vertical, 3=Horizontal default). Only the legacy writer translated it; the modelsdk writer emitted `LabelWidth` solely when set explicitly, so the orientation was read into the model and dropped — the #812 shape, a model field no active-engine writer reads. (b) `ShowFooter` was only ever set implicitly by a `footer { … }` block; the property sat in the validator allow-list, so it parsed and was discarded | `sdk/pages/pages_widgets_data.go` (`ResolvedLabelWidth`), `mdl/backend/modelsdk/widget_write.go`, `sdk/mpr/writer_widgets_display.go`, `mdl/executor/cmd_pages_builder_v3_widgets.go` | Put the derivation **on the model** (`ResolvedLabelWidth`) so both writers share one definition instead of one owning it, and emit `LabelWidth` unconditionally. For the property, read it explicitly and let it win over the implicit block in both directions. **Trap**: `WidgetV3.GetBoolProp` is case-SENSITIVE and accepts only a real `bool`, unlike `GetStringProp` — so `showFooter: true` read as `false` even after the key was found. Coerce from the looked-up value and refuse a nonsense one instead of defaulting to false. Repro `mdl-examples/bug-tests/762-813-dataview-properties.mdl`. Issues #762, #813 |
| Every mxcli-authored page carries a container nobody asked for — a `Forms$DivContainer` named `conditionalVisibilityWidget<N>` wrapping the page's top-level widgets. Creating a single button yields a button **and** a container | The builder wrapped each non-empty layout placeholder, because `pages.LayoutCallArgument` declared a **single** `Widget` field while the BSON `Forms$FormCallArgument` carries a **`Widgets` array**. The wrapper existed only to squeeze N widgets through a 1-widget field — never a BSON requirement | `sdk/pages/pages_parameters.go` (`LayoutCallArgument.Widgets`), `mdl/executor/cmd_pages_builder_v3.go`, `sdk/mpr/writer_pages.go`, `mdl/backend/modelsdk/page_write.go`, `mdl/backend/mcp/page.go` | Make the field a list and place widgets directly. **Check the claim against Mendix's own output before believing a comment**: ours said the wrapper is what "mxcli (and Studio Pro) adds", but `Administration.Account_Overview` in a `mx create-project` app has *two* top-level widgets in one placeholder and zero wrappers — same reasoned-by-analogy error as #812/#295. Corroborating signal that a construct is wrong: DESCRIBE already unwrapped it as a "phantom CONTAINER" and the catalog skipped it as "transparent" — three places working around something that should not be created. **Keep those readers**: projects authored before the fix still contain wrappers. Repro `mdl-examples/bug-tests/760-no-placeholder-wrapper.mdl`. Issue #760 |
| `CREATE CONFIGURATION` (or any `ALTER SETTINGS`) reports success and `mx check` passes, but Studio Pro throws `System.InvalidOperationException: Sequence contains no matching element` at `MprProperty.cs:25` when the changed unit is opened (e.g. from the version-control status grid). Silently, the same write also resets **HttpPortNumber/ServerPortNumber to 0** on every *existing* configuration | Three storage-name/enum defects in the settings write, all invisible to mxbuild (its deserializer tolerates unknown properties; Studio Pro resolves each stored property against the type's property list and throws when there is no match). (1) `createConfiguration` hardcoded `DatabaseType: "HSQLDB"` — the enum member is `Hsqldb`. (2) The gen `Configuration` binds the ports as `RuntimePortNumber`/`AdminPortNumber` (SDK names) while Studio Pro stores `HttpPortNumber`/`ServerPortNumber`, so the read returned 0 and the overlay wrote that 0 back. (3) Mendix renamed the runtime Java version property between 11.6 (`JavaVersion` = `"Java21"`) and 11.12 (`JavaMajorVersion` = `"21"`); mxcli wrote the 11.6 name unconditionally, leaving `JavaMajorVersion` stale and adding a property 11.12 does not define | `mdl/executor/cmd_settings.go` (`settingsDatabaseType`, `createConfiguration` defaults), `mdl/backend/modelsdk/settings_read.go` (`rawInt`, `javaVersionOf`), `mdl/settingsoverlay/settingsoverlay.go` (`JavaVersionKey`/`SetJavaVersion`, `newServerConfiguration`), `sdk/mpr/parser_settings.go` + `writer_settings.go` | Canonicalise enum-valued settings against `generated/metamodel` and reject the rest (executor **and** `mxcli check`, via a `settingsKind*` entry so the drift guard covers it). Read version-renamed properties off the stored document and write them back to the key they came from — **never invent a key the document does not already have** (the same reasoning removed the hardcoded `Tracing: nil` from the no-sibling fallback: 11.12 spells it `OpenTelemetry`). **Diagnose without Studio Pro**: dump the `Settings$ProjectSettings` unit before and after the command and diff key-by-key against the project `mx create-project` produced — the write must be purely additive. A "no matching element" *property* lookup means a key Mendix does not know; an enum member mismatch is a different exception. Repro: `create configuration 'X'` on an 11.12 project. Issue #759 |
| Any `ALTER SETTINGS` / `CREATE CONFIGURATION` corrupts a **private** constant override: the stored `Settings$PrivateValue` comes back carrying `"Value": ""`. Studio Pro then throws `System.InvalidOperationException: Sequence contains no matching element` at `MprProperty.cs:25` on open. `describe settings` separately renders the override as `value ''`, so replaying describe's own output converts it to a *shared* empty override | A constant override's value is either a `Settings$SharedValue` (carries `Value`, lives in the shared model) or a `Settings$PrivateValue` — a **marker type with no properties at all**, meaning the value is on the developer's workstation and deliberately out of version control. The overlay assumed SharedValue and wrote `cv.Value` (always `""` for a private override) into whichever node it found; the read type-asserted to `*SharedValue`, failed, and returned `""` with no way to distinguish private from empty | `mdl/settingsoverlay/settingsoverlay.go` (`constantValue`, `PrivateValueType`), `mdl/backend/modelsdk/settings_read.go` (`isPrivateConstantValue`), `sdk/mpr/parser_settings.go` (`parseConstantValue`), `mdl/executor/cmd_settings.go` (`describeSettings`, `alterSettingsConstant`) | Carry the distinction in the model (`model.ConstantValue.IsPrivate`) and **preserve, never author**: leave a PrivateValue node byte-identical, have `describe` emit a comment instead of a re-executable statement, and refuse an `alter settings constant` that would flip private→shared (drop is still allowed — it discards the whole override, which is what was asked). **Generalisable**: a polymorphic child whose variants differ in *arity* (one carries a value, one is a bare marker) cannot be overlaid by field assignment — branch on `$Type` first. Blast radius is wider than it looks: configurations are shared in version control, so one developer's unrelated edit corrupts every developer's private overrides and pushes the result. Found from a user describing their workflow, not from a filed issue |

---

Expand Down
13 changes: 13 additions & 0 deletions .claude/skills/mendix/create-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,19 @@ dataview dv (datasource: $Customer, LabelWidth: 4) -- explicit, 0

`LabelWidth: 0` ⇔ `FormOrientation: Vertical`. If both are given, `LabelWidth` wins.

**Footer (`showFooter`):** a `footer { … }` block turns the footer on by itself, so
the property is only needed when the two would disagree:

```sql
dataview dv (datasource: $Customer, showFooter: true) -- empty footer, shown
dataview dv (datasource: $Customer, showFooter: false) { -- widgets declared, hidden
footer f { dynamictext t (content: 'hidden') }
}
```

An explicit `showFooter` wins over the block in both directions, and hiding a footer
never discards its widgets.

### GALLERY Widget

Display items in card layout with selection and responsive columns:
Expand Down
28 changes: 27 additions & 1 deletion .claude/skills/mendix/project-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ alter settings constant 'MyModule.ApiKey' value 'abc123';
alter settings drop constant 'MyModule.ApiKey' in configuration 'Default';
```

#### Shared vs private values

A constant override's value is either **shared** — stored in the model and therefore
in version control, where every developer gets it — or **private**, stored on the
developer's own workstation and deliberately kept out of the repository. Development
API tokens are the usual reason to make one private.

MDL **preserves that choice but never changes it**. The two statements above operate
on shared values only:

- `show constant values` reports a private override as `(private)` rather than a blank
cell — the value is not in the project, so mxcli cannot show it.
- `describe settings` reports a private override as a comment, not as a re-executable
`alter settings constant` line — replaying that line would publish into the shared
model a value the developer chose to keep local.
- `alter settings constant ... value ...` on a private override is **refused**, with a
pointer to change it in Studio Pro first. Setting a value would convert it to a
shared one and break the developer's local binding.
- `alter settings drop constant ...` **is** allowed: it removes the whole override,
private marker included, which is what was asked for.

### Create / Drop Configurations

```sql
Expand All @@ -86,14 +107,19 @@ create configuration 'Staging';

-- Create with properties
create configuration 'Production'
DatabaseType = 'POSTGRESQL',
DatabaseType = 'PostgreSql',
DatabaseUrl = 'prod-db:5432',
HttpPortNumber = 8080;

-- Drop a configuration
drop configuration 'Staging';
```

`DatabaseType` must name a Mendix database type — `Db2`, `Hsqldb`, `MySql`,
`Oracle`, `PostgreSql`, `SapHana` or `SqlServer` — matched case-insensitively and
stored in that spelling. Any other value is rejected; a configuration stored with
one Mendix does not recognise cannot be opened in Studio Pro.

### Language and Workflow Settings

```sql
Expand Down
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,16 @@ Dockerfile text eol=lf
*.vsix binary
*.cdx.json binary
bun.lock binary

# Append-only knowledge files use git's union merge driver.
#
# Every bug fix appends a row to the symptom table in fix-issue.md, so two
# concurrent fixes always collide on the same line — five resolution rounds in
# one week. Moving the insertion point from the top of the table to the bottom
# did not help: both sides still append to the same place, so the collision
# moved with it.
#
# "union" tells git to keep BOTH sides of a conflicting hunk instead of raising
# a conflict. That is exactly right for a file that is only ever appended to and
# is looked up by matching a symptom, so row order carries no meaning.
.claude/skills/fix-issue.md merge=union
Loading
Loading