fix: settings writes corrupted private constant overrides - #77
Merged
Conversation
A constant override's value is stored one of two ways. Settings$SharedValue
carries a "Value" and lives in the model, so it is in version control and every
developer gets it. Settings$PrivateValue is a marker type with no properties at
all: the value is on the developer's own workstation, deliberately kept out of
the repository (the usual choice for development API tokens).
The overlay assumed every stored SharedOrPrivateValue was a SharedValue and
assigned cv.Value into it. For a private override cv.Value is always "" — the
value is not in the model — so any settings write produced:
{"$Type": "Settings$PrivateValue", "Value": ""}
a property that type does not define. Studio Pro resolves each stored property
against the type's property list and throws on open:
System.InvalidOperationException: Sequence contains no matching element
at ...MprProperty.cs:line 25
the same failure shape as mendixlabs#759. mxbuild does not catch it — `mx check` passes on
the corrupted document. The blast radius is wider than a local mistake:
configurations are shared in version control, so one developer running any
ALTER SETTINGS or CREATE CONFIGURATION corrupted every developer's private
overrides and pushed the result.
Separately, the read type-asserted to *SharedValue, failed, and returned "" with
no way to tell private from empty — so `describe settings` rendered the override
as `value ''`, and replaying describe's own output converted it into a *shared*
empty override, publishing into git a value the developer chose to keep local.
The shared/private choice belongs to the constant; configurations must just
respect it. So mxcli now carries the distinction and preserves it without ever
authoring it:
- model.ConstantValue.IsPrivate, set by both read paths (codec matches on the
child's type name rather than its Go type, since the registry may decode the
marker to a bare element.Base).
- The overlay leaves a PrivateValue node byte-identical.
- `describe settings` emits a comment instead of a re-executable statement.
- `alter settings constant ... value ...` on a private override is refused, with
a pointer to change it in Studio Pro first. `drop constant` is still allowed —
it discards the whole override, which is what was asked for.
Verified on a real 11.12.2 project with a Studio-Pro-shaped private override:
an unrelated port change leaves the node as {"$Type": "Settings$PrivateValue"}
with no Value key, on both write engines, and `mx check` passes. All four guards
were mutation-checked — reverting each makes its test fail with the reported
symptom.
Follows the private-override fix with the reporting and documentation half. SHOW CONSTANT VALUES rendered a private override as an empty cell, which is indistinguishable from an override deliberately set to the empty string. Since the value genuinely is not in the project, report "(private)" instead. Documentation, covering each surface the constant/configuration story touches: - mxcli syntax domain-model.constant.create — a "Shared vs private values" section stating the rule and what each statement does with a private override. It lives on the constant topic, not the settings topic, because the choice belongs to the constant. - docs-site create-constant.md — the same, as a table. This page already told readers to use an empty default "for secrets"; private values are the actual mechanism, so it now says so. - docs-site show-settings.md — what the (private) marker means. - MDL_QUICK_REFERENCE.md — a note under Constants. - skills/mendix/project-settings.md — the SHOW CONSTANT VALUES behaviour alongside the describe/alter/drop rules. CLAUDE.md gains the invariant both this bug and mendixlabs#759 come from, since it applies to any future overlay write and neither `mx check` nor the build catches a violation: write only keys the document already carries (property names are version-specific — JavaVersion/JavaMajorVersion, Tracing/OpenTelemetry), and dispatch a polymorphic child on $Type before assigning fields, because variants can differ in arity (SharedValue carries a value, PrivateValue is a bare marker). Plus the sibling trap: validate enum-valued properties against generated/metamodel rather than passing a user string through. The (private) marker is mutation-checked.
…nt-values # Conflicts: # .claude/skills/fix-issue.md
ako
pushed a commit
that referenced
this pull request
Aug 1, 2026
…driver Every bug fix appends a row to the symptom table in .claude/skills/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 simply moved with it (PRs #76, #77 and #78 all hit it). git's built-in "union" merge driver keeps BOTH sides of a conflicting hunk instead of raising a conflict. That is exactly right here: the file is only ever appended to, and it is looked up by matching a symptom rather than read in order, so row order carries no meaning. Verified: two branches that each append a row now merge with no conflict and both rows present. Caveat: union applies to the whole file, so two branches editing the same *prose* line would silently keep both instead of conflicting. The failure is a visible duplicated line, not corruption. Splitting the table into its own file — so union covers only append-only content — is the follow-up, deferred because that restructure would conflict with the three open PRs that all touch this table.
…nt-values # Conflicts: # .claude/skills/fix-issue.md
Every bug fix appends a row to the symptom table, so two concurrent fixes always collide on the same line — and moving rows from the top of the table to the bottom did not help, because both sides still append to the same place. git's built-in union driver keeps both sides instead of conflicting, which is correct for a file that is only appended to and looked up by matching a symptom. Appended to the existing rules, not replacing them.
ako
pushed a commit
that referenced
this pull request
Aug 1, 2026
The rationale in fix-issue.md and CLAUDE.md claimed that appending at the end of the table avoids the conflict two concurrent fixes cause. It does not — both sides still append to the same line, which is why #76, #77 and #78 each hit it again after that convention was adopted. Merging is handled by the merge=union driver in .gitattributes (added in #77). Appending at the end is kept for readable diffs and rough chronology, which is a human reason, not a git one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A constant override's value is stored one of two ways:
Settings$SharedValueValue— lives in the model, so in version control, and every developer gets itSettings$PrivateValueThe overlay assumed every stored
SharedOrPrivateValuewas aSharedValueand assignedcv.Valueinto it. For a private overridecv.Valueis always""— the value is not in the model — so any settings write produced:{"$Type": "Settings$PrivateValue", "Value": ""}a property that type does not define. Studio Pro resolves each stored property against the type's property list and throws on open:
the same failure shape as mendixlabs#759.
mx checkpasses on the corrupted document — mxbuild's deserializer tolerates unknown properties, so the build is not a safety net.The blast radius is wider than a local mistake. Configurations are shared in version control, so one developer running any
ALTER SETTINGSorCREATE CONFIGURATIONcorrupted every developer's private overrides and pushed the result.Separately, the read type-asserted to
*SharedValue, failed, and returned""with no way to tell private from empty. Sodescribe settingsrendered the override asvalue '', and replaying describe's own output converted it into a shared empty override — publishing into git a value the developer chose to keep local.Approach
The shared/private choice belongs to the constant; configurations must just respect it. mxcli now carries the distinction and preserves it without ever authoring it.
settingsoverlay.constantValuecv.Valueinto whatever node it found$Type; aPrivateValuenode is returned byte-identical*SharedValue, returned""model.ConstantValue.IsPrivatecarries the distinctiondescribe settingsalter settings constant 'X' value ''alter settings constant … value …alter settings drop constant …show constant values""(private)The codec read matches on the child's type name rather than its Go type, since the gen registry may decode the marker to a bare
element.Base.Verification
On a real 11.12.2 project seeded with a Studio-Pro-shaped private override, an unrelated
alter settings configuration 'Default' HttpPortNumber = 8099:--engine legacy).mxcli docker check→ 0 errors.New tests:
mdl/settingsoverlay/private_value_test.go(3),mdl/executor/cmd_settings_private_test.go(4), plus one incmd_constants_mock_test.go. Full./mdl/... ./sdk/... ./cmd/... ./model/...suite green.Documentation
CLAUDE.mdgains the invariant this bug and mendixlabs#759 both come from, since it applies to any future overlay write and neithermx checknor the build catches a violation:JavaVersion/JavaMajorVersion,Tracing/OpenTelemetrybetween 11.6 and 11.12).$Typebefore assigning fields — variants can differ in arity, not just values.Plus the sibling trap: validate enum-valued properties against
generated/metamodelrather than passing a user string through.User-facing docs updated on each surface the story touches:
mxcli syntax domain-model.constant.create(on the constant topic, not the settings topic — the choice belongs to the constant),docs-sitecreate-constant.md+show-settings.md,MDL_QUICK_REFERENCE.md, andskills/mendix/project-settings.md.create-constant.mdalready told readers to use an empty default "for secrets"; private values are the actual mechanism, so it now says so.Symptom row appended to
.claude/skills/fix-issue.md, and a manual-repro script added atmdl-examples/bug-tests/private-constant-values.mdl— the override itself cannot be scripted, since authoring it is exactly what MDL now refuses.Notes for review
Two judgement calls worth a second opinion:
drop constanton a private override is allowed. It removes the whole override including the marker, which is what the user asked for — unlike setting a value, it publishes nothing. Refusing it too would be defensible if private overrides should be untouchable from MDL entirely.alter settings constant … valuerefuses rather than warns. It would both push a deliberately-local value into git and break the developer's local binding, silently. This matches how cross-module grants are handled (reject, don't remap). A warn-and-proceed variant is a one-line change.This bug was not from a filed issue — it surfaced from a description of how configurations and private values are used day to day.
Found alongside mendixlabs#759, but branched independently off
main. Both touchsettingsoverlay.goandsettings_read.go, in different functions; whichever merges second should apply cleanly.Generated by Claude Code