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.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ to the symptom table below, so the next similar issue costs fewer reads.
| `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 |
| 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
21 changes: 21 additions & 0 deletions .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 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
30 changes: 30 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,36 @@ The reserved-word lists live in `mdl/executor/cmd_enumerations.go` (`mendixReser

A microflow wired as the project's **after-startup** microflow must return `Boolean` — Mendix build fails with **CE0142** on a void (no-return) microflow. A common trip-up: a seed/demo-data microflow wired to after-startup will not build until it ends with a `return true` (Boolean). This is a Mendix platform rule, not an mxcli check.

### Overlay Writes: Never Invent a Key, Branch on `$Type`

When a write overlays fields onto preserved BSON (`mdl/settingsoverlay`, and any
future storage that follows ADR-0005 guard-don't-drop), two rules are load-bearing.
Breaking either produces a document `mx check` accepts and **Studio Pro cannot
open**: it resolves every stored property against the type's property list and
throws `System.InvalidOperationException: Sequence contains no matching element`
at `MprProperty.cs`. mxbuild's deserializer tolerates unknown properties, so the
build is not a safety net here.

1. **Write only keys the document already carries.** Property names are
version-specific — Mendix renamed `JavaVersion` (`"Java21"`) to
`JavaMajorVersion` (`"21"`) and `Tracing` to `OpenTelemetry` between 11.6 and
11.12. Read the key off the stored document and write back to that same key;
when neither is present, write neither (an absent optional property is filled
in on load). See `settingsoverlay.JavaVersionKey` (#759).
2. **A polymorphic child must be dispatched on `$Type` before any field
assignment.** Variants can differ in *arity*, not just field values:
`Settings$SharedValue` carries a `Value`, while `Settings$PrivateValue` is a
bare marker with no properties at all (the value lives on the developer's
workstation). Assigning `Value` to whichever node is there corrupts the marker.

The same reasoning bans authoring what the model does not own: mxcli preserves a
constant override's shared/private choice and refuses statements that would flip
it, rather than silently converting one to the other.

Enum-valued properties are the sibling trap: validate against
`generated/metamodel` (e.g. `SettingsDatabaseType` is `Hsqldb`, never `HSQLDB`)
rather than passing a user string through.

### Association Parent/Child Pointer Semantics (Counter-Intuitive)

**CRITICAL**: Mendix BSON uses inverted naming for association pointers:
Expand Down
13 changes: 12 additions & 1 deletion cmd/mxcli/syntax/features_domain_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,18 @@ func init() {
"show constants", "constant values", "modify constant",
"string constant", "integer constant", "boolean constant",
},
Syntax: "CREATE CONSTANT Module.Name\n TYPE String|Integer|Long|Decimal|Boolean|DateTime\n DEFAULT value\n [COMMENT 'description'];\n\nCREATE OR MODIFY CONSTANT Module.Name\n TYPE DataType DEFAULT value [COMMENT 'text'];\n\nSHOW CONSTANTS;\nSHOW CONSTANTS IN <module>;\nSHOW CONSTANT VALUES;\nDESCRIBE CONSTANT Module.Name;\nDROP CONSTANT Module.Name;\n\nRemove override:\n ALTER SETTINGS DROP CONSTANT 'Module.Name' IN CONFIGURATION 'cfg';",
Syntax: "CREATE CONSTANT Module.Name\n TYPE String|Integer|Long|Decimal|Boolean|DateTime\n DEFAULT value\n [COMMENT 'description'];\n\nCREATE OR MODIFY CONSTANT Module.Name\n TYPE DataType DEFAULT value [COMMENT 'text'];\n\nSHOW CONSTANTS;\nSHOW CONSTANTS IN <module>;\nSHOW CONSTANT VALUES;\nDESCRIBE CONSTANT Module.Name;\nDROP CONSTANT Module.Name;\n\nRemove override:\n ALTER SETTINGS DROP CONSTANT 'Module.Name' IN CONFIGURATION 'cfg';\n\n" +
"Shared vs private values:\n" +
" A per-configuration override holds either a SHARED value (stored in the\n" +
" model, so in version control — every developer gets it) or a PRIVATE one\n" +
" (stored on the developer's own workstation, deliberately out of the repo;\n" +
" the usual choice for development API tokens).\n\n" +
" MDL preserves that choice but never changes it. ALTER SETTINGS CONSTANT\n" +
" applies to shared values only — on a private override it is refused, since\n" +
" setting a value would publish a deliberately-local one into version control.\n" +
" SHOW CONSTANT VALUES reports it as (private); DESCRIBE SETTINGS reports it\n" +
" as a comment, not a re-executable statement. DROP CONSTANT still works.\n" +
" Change a constant to a shared value in Studio Pro.",
Example: "CREATE CONSTANT MyModule.ApiBaseUrl\n TYPE String\n DEFAULT 'https://api.example.com/v1';\n\nCREATE CONSTANT MyModule.MaxRetries\n TYPE Integer DEFAULT 3\n COMMENT 'Maximum number of API retry attempts';\n\nCREATE CONSTANT MyModule.EnableDebug\n TYPE Boolean DEFAULT false;\n\nCREATE OR MODIFY CONSTANT MyModule.ApiBaseUrl\n TYPE String\n DEFAULT 'https://api.staging.example.com/v2';",
SeeAlso: []string{"domain-model.constant"},
})
Expand Down
22 changes: 22 additions & 0 deletions docs-site/src/reference/domain-model/create-constant.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,28 @@ CREATE CONSTANT MyModule.ApiBaseUrl TYPE String DEFAULT 'https://api.example.com
ALTER SETTINGS CONSTANT 'MyModule.ApiBaseUrl' VALUE 'https://staging.example.com' IN CONFIGURATION 'Staging';
```

### Shared and private values

An override holds its value one of two ways:

- **Shared** — stored in the model, so it travels with the project in version
control and every developer gets it.
- **Private** — stored on the developer's own workstation and deliberately kept
out of the repository. This is the answer for a development secret: the
constant and the override are shared, the value is not.

MDL **preserves that choice but never changes it** — the shared/private decision
belongs to the constant, and configurations just respect it:

| Statement | On a private override |
|-----------|----------------------|
| `ALTER SETTINGS CONSTANT … VALUE …` | **refused** — setting a value would convert it to shared and publish a deliberately-local value into version control |
| `ALTER SETTINGS DROP CONSTANT …` | allowed — removes the whole override, which is what was asked for |
| `SHOW CONSTANT VALUES` | reports `(private)` rather than a blank cell |
| `DESCRIBE SETTINGS` | emits a comment, not a re-executable statement |

To make a private value shared (or the reverse), change it in Studio Pro.

## See Also

[CREATE ENTITY](create-entity.md), [CREATE ENUMERATION](create-enumeration.md)
12 changes: 12 additions & 0 deletions docs-site/src/reference/settings/alter-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ ALTER SETTINGS CONFIGURATION 'production' DatabaseUrl = 'jdbc:postgresql://dbhos
ALTER SETTINGS CONSTANT 'MyModule.ApiBaseUrl' VALUE 'https://api.staging.example.com' IN CONFIGURATION 'staging';
```

An override's value is either **shared** — stored in the model, and so in version
control — or **private**, stored on the developer's own workstation and kept out of
the repository (the usual choice for development API tokens).

MDL preserves that choice but never changes it. `ALTER SETTINGS CONSTANT ... VALUE`
applies to shared values only; on a private override it is **refused**, because
setting a value would convert it to a shared one, publish a deliberately-local value
into version control, and break the developer's local binding. Change the constant to
a shared value in Studio Pro first, or drop the override. `DESCRIBE SETTINGS` reports
a private override as a comment rather than a re-executable statement, for the same
reason.

### Set the default language

```sql
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/reference/settings/show-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The available settings categories are:
|----------|----------|
| `MODEL` | Application-level settings: AfterStartupMicroflow, BeforeShutdownMicroflow, HashAlgorithm, JavaVersion, etc. |
| `CONFIGURATION` | Runtime configurations: DatabaseType, DatabaseUrl, HttpPortNumber, etc. Each named configuration is listed separately. |
| `CONSTANT` | Constant value overrides per configuration. Shows which constants have non-default values in each configuration. |
| `CONSTANT` | Constant value overrides per configuration. Shows which constants have non-default values in each configuration. An override whose value is private — stored on the developer's workstation rather than in the shared model — is reported as `(private)`; its value is not in the project and mxcli cannot show it. |
| `LANGUAGE` | Localization settings: DefaultLanguageCode and available languages. |
| `WORKFLOWS` | Workflow engine settings: UserEntity, DefaultTaskParallelism, etc. |

Expand Down
7 changes: 7 additions & 0 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ alter entity Sales.Customer
| Create constant | `create [or modify] constant Module.Name type DataType default 'value';` | String, Integer, Boolean, etc. |
| Drop constant | `drop constant Module.Name;` | |

A per-configuration override holds either a **shared** value (in the model, so in
version control) or a **private** one (on the developer's own workstation, out of the
repo). MDL preserves that choice but never changes it: `alter settings constant … value`
is refused on a private override, `show constant values` reports it as `(private)`, and
`describe settings` emits a comment rather than a re-executable statement.
`alter settings drop constant` still works.

**Example:**
```sql
create constant MyModule.ApiBaseUrl type string default 'https://api.example.com';
Expand Down
Loading
Loading