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/findings/mdl-backend.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@
{"area": "mdl/backend", "date": "2026-09-08", "symptom": "A property Studio Pro writes on an offline entity config (CompatibilityMode) was read from the model and silently discarded, so any future write path would have dropped it with no error and no mx check failure", "cause": "types.NavOfflineEntity carried three of the four properties Studio Pro actually writes. TestFieldCountDrift, which exists to catch exactly this on hand-copied structs, did not list NavOfflineEntity or NavigationProfile — so adding the field left the guard passing vacuously", "file": "`mdl/types/navigation.go`, `mdl/backend/mpr/convert.go`, `mdl/backend/mpr/convert_roundtrip_test.go`", "insight": "A drift guard is only worth what its list covers, and a guard that passes on a struct it does not know about is worse than none — it reads as coverage. Check the guard names your type before trusting a green run. Also: measure which properties are actually WRITTEN before deciding what to carry — gen declared six here and the reference document had four, with DownloadMode and ShouldDownload occurring zero times, so the risk was inverted from the expected one (writing a property Studio Pro fills in on load, not dropping one)", "refs": ["ako/TestApp", "ako/mxcli#413"]}
{"area": "mdl/backend", "date": "2026-09-08", "symptom": "An offline navigation profile authored by mxcli builds, routes and installs as a PWA, and shows an empty app — every gate green", "cause": "MDL had no syntax for offline synchronization, so a created offline profile got an empty OfflineEntityConfigs list. A Mendix offline profile downloads nothing until each entity has a sync mode; `mx check` reports 0 errors either way because an empty list is valid", "file": "`mdl/grammar/MDLParser.g4` (navSyncDef), `mdl/backend/modelsdk/navigation_write.go`, `sdk/mpr/writer_navigation.go`", "insight": "Creating a document kind is not the same as being able to configure it, and the gap is invisible to every static check — the symptom is an empty screen at runtime. When adding a profile/document kind, ask what makes it DO anything, not just what makes it exist. The write is an overlay keyed by entity so CompatibilityMode (stored, unauthorable) survives; building the element from the spec alone would clear it silently, the access-rule defect again", "refs": ["ako/TestApp", "PROPOSAL_offline_sync_configuration.md"]}
{"area": "mdl/backend", "date": "2026-09-08", "symptom": "`alter page P { set PageSize = 10 on <datagrid2> }` errors `pluggable property \"PageSize\" not found` on a grid that `create page \u2026 (PageSize: 20)` had just written and the app really pages at. `mxcli check --references` passes the script, so it fails only at exec, after earlier statements have landed; DESCRIBE PAGE prints the same capitalised `PageSize:`, so describe \u2192 edit \u2192 exec produced a script mxcli refused to run", "cause": "A pluggable property key is lowerCamel in the widget template (`pageSize`). CREATE resolves the author's spelling case-INsensitively (widget engine `lookupProperty`, and `WidgetV3.GetStringProp` before it); ALTER went through `setPluggableWidgetPropertyMut`, which compared the template key byte-for-byte, so only the exact `pageSize` worked. Direct sequel to Findings #1 (2026-07-27), which fixed the same class for FIRST-CLASS props and deliberately left the pluggable fallback case-sensitive with the comment 'template keys must match the template exactly'", "file": "`mdl/backend/pagemutator/mutator.go` (`setPluggableWidgetPropertyMut`)", "insight": "`strings.EqualFold` against the widget's own PropertyTypes. **The disproven belief is the reusable part**: keys are STORED case-sensitively, which is not a reason to MATCH them that way \u2014 the resolver searches one object type's PropertyTypes, and across every shipped template and definition that scope holds no two keys differing only in case (96 scopes, 1208 keys, 0 collisions). Measure the ambiguity before assuming it; here there was none, and the assumption cost a whole verb. **Cheapest localiser**: run the failing statement with the template's own casing \u2014 `set pageSize` succeeded where `set PageSize` failed, in ONE measurement, on the same widget in the same project. Both engines share `pagemutator`, so an engine split says nothing here (verified: modelsdk and legacy both fixed by the one change). Tests `TestSetPluggableProperty_MatchesTemplateKeyRegardlessOfCase` (+ typo-still-errors control, + `TestPluggablePropertyKeysAreUniqueIgnoringCase` pinning the no-collision argument); repro `mdl-examples/bug-tests/alter-page-pluggable-property-casing.mdl`; verified 0 errors on `mx check` 11.13.0. Two reporter claims did NOT hold: `describe page` DOES emit PageSize, but only when it differs from the widget default 20 (deliberate, so describe round-trips) \u2014 at the default it is omitted, which reads as 'describe cannot show it'. Still open and separate: `check --references` does not resolve pluggable property names at all, so a genuine typo (`PagSize`) still checks clean and fails at exec", "refs": ["mendixlabs/mxcli#1069"]}
{"area": "mdl/backend", "date": "2026-09-09", "symptom": "A control that should have proven a guard was load-bearing PASSED with the guard removed — the test could not distinguish a correct writer from one that reset the property on every write", "cause": "The fixture stored `false` for a boolean property, and false is also the zero value. A writer that ignored the spec entirely wrote false; the correct writer preserved false. Identical output, so the assertion held either way", "file": "`mdl/backend/modelsdk/navigation_throw_sync_test.go`", "insight": "For a BOOLEAN property, a preservation test must exercise BOTH stored values — the non-zero one is the only case that can fail. This is the second time in one feature: CompatibilityMode needed a synthetic `true` because all seven reference configs carry false. The generalisation: when every real document agrees on a value, the fixture drawn from real documents cannot test preservation, and a synthetic counter-case is not optional. The tell is a control that fails to fail — if stubbing the guard leaves the suite green, the test is measuring nothing, and that is worse than no test because it reads as coverage", "refs": ["ako/mxcli#413", "ThrowPartialSyncError"]}
14 changes: 14 additions & 0 deletions .claude/skills/mendix/manage-navigation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,20 @@ The `sync` row names the profile. Every mode produces one, **including the
modes that download nothing**: a profile with `sync X never` still names `X`,
so renaming or dropping it leaves the configuration dangling.

**Errors when the server rejects an object.** Studio Pro's *"Throw error when
server rejects objects during synchronization"* checkbox:

```sql
create or replace navigation PhoneOffline
home page MyModule.Mobile_Dashboard
on sync error continue; -- default is `throw`
```

It uses the phrase MDL already has for failure handling — a microflow's
`on error continue` — rather than a keyword of its own. Omitting the clause
leaves the stored value alone; `describe navigation` emits it only when it is
not the default, so existing scripts stay quiet.

**Compatibility mode has no syntax.** mxcli reads it, preserves it across a
rewrite, and `describe navigation` flags any entity that has it on — it is never
silently dropped.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ Full syntax tables for all MDL statements (microflows, pages, security, navigati
## Current Implementation Status

**Implemented:**
- Offline synchronization (`CREATE NAVIGATION … SYNC (…)`): an offline navigation profile downloads **nothing** until each entity has a sync mode, so a profile mxcli created built, routed and installed as a PWA and showed an **empty app** — with `mxcli check`, `exec` and `mx check` all clean. The six mode words are the members Mendix stores, **not** Studio Pro's captions (its "All Objects" is `ALL`, its "By XPath" is `WHERE`), and a caption is refused rather than written — the CE0463 gallery defect wearing a different hat. `WHERE` takes the XPath in **brackets**, verbatim: the quoted form doubles every quote, and a stored constraint already carries Mendix's own escaping, so the two compose into runs of six (mendixlabs/mxcli#750, and `PROPOSAL_first_class_expressions.md`). The write is an **overlay keyed by entity**, so `CompatibilityMode` — stored, unauthorable — survives a rewrite; every reference config carries `false`, so only a synthetic `true` case distinguishes a correct writer from one that always emits `false`. `DownloadMode`/`ShouldDownload` are deliberately **not** written though gen declares them: zero occurrences in ako/TestApp, and a property Studio Pro fills in on load is one whose emission makes a document Studio Pro cannot open. Creating the *profile* stays modelsdk-only (a fourteen-key document pinned to a Studio Pro reference); the SYNC block works on both engines. Both halves are in the catalog: `CATALOG.OFFLINE_ENTITY_CONFIGS` holds one row per configured entity (the profile's `OfflineEntityCount` said how many and nothing else), and a configured entity emits a **`sync` edge** into `CATALOG.REFS` so `show references to Mod.Entity` names the profiles that download it. Every mode gets an edge, **including the ones that download nothing** — a profile with `sync X never` still names X, so renaming or dropping it leaves the config dangling, which is exactly what the edge exists to reveal. See `.claude/skills/mendix/manage-navigation/SKILL.md` and `docs/11-proposals/PROPOSAL_offline_sync_configuration.md`
- Offline synchronization (`CREATE NAVIGATION … SYNC (…)`): an offline navigation profile downloads **nothing** until each entity has a sync mode, so a profile mxcli created built, routed and installed as a PWA and showed an **empty app** — with `mxcli check`, `exec` and `mx check` all clean. The six mode words are the members Mendix stores, **not** Studio Pro's captions (its "All Objects" is `ALL`, its "By XPath" is `WHERE`), and a caption is refused rather than written — the CE0463 gallery defect wearing a different hat. `WHERE` takes the XPath in **brackets**, verbatim: the quoted form doubles every quote, and a stored constraint already carries Mendix's own escaping, so the two compose into runs of six (mendixlabs/mxcli#750, and `PROPOSAL_first_class_expressions.md`). The write is an **overlay keyed by entity**, so `CompatibilityMode` — stored, unauthorable — survives a rewrite; every reference config carries `false`, so only a synthetic `true` case distinguishes a correct writer from one that always emits `false`. `DownloadMode`/`ShouldDownload` are deliberately **not** written though gen declares them: zero occurrences in ako/TestApp, and a property Studio Pro fills in on load is one whose emission makes a document Studio Pro cannot open. Creating the *profile* stays modelsdk-only (a fourteen-key document pinned to a Studio Pro reference); the SYNC block works on both engines. `ON SYNC ERROR THROW|CONTINUE` writes `ThrowPartialSyncError`, a property **neither generated source declares** (zero occurrences in gen and in generated/metamodel), so it is read from `element.Base.Raw()` and written as a raw key. The spec field is a **pointer**: the property is a bare bool with no unset value, so a non-pointer would reset it on every rewrite that never mentions the clause. Absent reads as **true**, matching every reference profile and Studio Pro's checked-by-default box. Both halves are in the catalog: `CATALOG.OFFLINE_ENTITY_CONFIGS` holds one row per configured entity (the profile's `OfflineEntityCount` said how many and nothing else), and a configured entity emits a **`sync` edge** into `CATALOG.REFS` so `show references to Mod.Entity` names the profiles that download it. Every mode gets an edge, **including the ones that download nothing** — a profile with `sync X never` still names X, so renaming or dropping it leaves the config dangling, which is exactly what the edge exists to reveal. See `.claude/skills/mendix/manage-navigation/SKILL.md` and `docs/11-proposals/PROPOSAL_offline_sync_configuration.md`
- Project brain (`mxcli brain init/capture/staged/promote/drop/check/show`): an **opt-in** store in `docs/brain/` for the project knowledge mxcli cannot compute. The governing rule is that anything derivable from the model is answered by a command and never written down — a note that transcribes the model disagrees with it silently. Records shard by **anchor scope**: an entry's first anchor names its file (`@Sales.Order` → `modules/Sales.md`), an anchorless entry is cross-cutting (`project.md`), and there is no index to maintain because the module prefix *is* the file name. That is what makes the cap per-shard rather than a project-wide budget, and lets a session load `project.md` plus the modules it is touching. `check` answers two independent questions: each anchor is **resolved / not found / not indexable** — only the middle one fails, and the third exists because the catalog's `objects` view covers the describable types only, so a scheduled event would otherwise read as *missing* (separated with `FindDocumentUnit`, which cannot miss a kind because it never asks what kind anything is). Misfiling is a **second axis, not a fourth state**: every anchor can resolve and the entry still be in the wrong file, and it is only decided when something resolved — judging it on an all-not-indexable entry reintroduced the same false staleness through the other axis (caught by a test, with the guard stubbed as the control). An agent `capture`s to a git-ignored queue and a person `promote`s; the queue is deliberately **not** sharded, because routing it would force the file decision before a human has looked at the entry. `mxcli lint` prints the unpromoted-queue count, because a report only `brain check` prints is a report nothing demands. Sizes are computed by `brain show` and never written into a committed file. A second record kind, **requirement**, lives in `plan/<slice>.md` and inverts the anchor's meaning: a decision's anchor points backward (not resolving = stale, fails), a requirement's points forward (not resolving = not built yet, passes). Measured: filed as an ordinary entry, one unbuilt requirement takes `brain check` to exit 1 — which is why it is a separate kind rather than more entries in the same files. That inversion is also what makes `brain plan` a real progress report: a requirement is *built* when its anchors resolve, so creating the microflow it names moves the count with the plan file untouched (measured 0/1 → 1/0). A status written beside a requirement is therefore refused by the skill, not just discouraged. Slices are ordered by name (`01-accounts`), span modules by design (so misfiling does not apply), and carry a generous cap that enforces the slicing discipline — a slice too long to read should be split. A third kind, **open question** (`--open`), records what is *not* decided; its anchors are deliberately **not** checked, since the question is often whether the thing should exist at all — measured, the identical anchor exits 1 as a decision and 0 as a question. `brain resolve` converts one into a decision in place, keeping its id and position and starting to check its anchors, which is the transition the kind exists for. Unanswered questions are reported by `brain check` and by `mxcli lint`. The skill also gives capture a **trigger** rather than good intentions — a correction you have had to make twice — because the decisions half otherwise under-fills while the plan half fills at bootstrap. `bootstrap-app` asks for requirements at the interview and records them by default. Package: `cmd/mxcli/brain/`. See `docs-site/src/tools/project-brain.md` and `docs/11-proposals/PROPOSAL_project_brain.md`
- Default styling + runtime theme switching (`mxcli theme list/show/create/apply/remove/switcher`, `mxcli new --theme`): three embedded themes (**signal** light-first, **ledger** light-first, **console** dark-first), each a palette in `theme/web/custom-variables.scss` + a shared Atlas wiring partial + a theme partial imported from `theme/web/main.scss` (which compiles last), plus vendored fonts. **No model changes**, so it hot-applies under `run --local --watch` and cannot affect a build. Generated regions are digest-fenced: a block carrying local edits is refused rather than overwritten. Applying a theme removes the previous one. `--variant auto` (default) ships both palettes — the app follows `prefers-color-scheme` before first paint and honours a `theme-light`/`theme-dark` class on `<html>`; `light`/`dark` bakes one. `theme switcher install` is the only part that writes to the model (JS actions + a nanoflow for a toggle button). A project can add its own themes under `theme/mxcli-themes/<name>/` (committed, not compiled); `theme create <name> [--from <theme|design-file>]` scaffolds one from an existing theme, renaming the identifiers built from the name and optionally seeding the palette from `--mxt-*` declarations in a design artifact. A local theme shadows a built-in of the same name. Package: `cmd/mxcli/theme/`. See `docs/11-proposals/PROPOSAL_default_styling.md`
- MPR v1/v2 reading and writing
Expand Down
8 changes: 8 additions & 0 deletions cmd/mxcli/syntax/features_misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ DISCONNECT;`,
"navigation profile", "phone profile", "tablet profile",
"offline profile", "offline navigation", "sync", "synchronization",
"offline sync", "offline entity", "pwa", "download mode",
"throw error", "sync error", "partial sync", "server rejects",
},
Syntax: `CREATE OR REPLACE NAVIGATION <profile>
HOME PAGE Module.Page
Expand All @@ -164,6 +165,7 @@ DISCONNECT;`,
MENU ITEM 'Label' PAGE Module.Page [ICON Module.IconCollection.Name];
MENU 'Group' [ICON Module.IconCollection.Name] ( ... );
)]
[ON SYNC ERROR THROW|CONTINUE]
[SYNC (
SYNC Module.Entity ONLINE;
SYNC Module.Entity ALL;
Expand Down Expand Up @@ -214,6 +216,12 @@ DISCONNECT;`,
-- and a stored constraint already carries Mendix's own escaping, so the two
-- compose into runs of six quotes. DESCRIBE emits the bracket form.
--
-- ON SYNC ERROR is Studio Pro's "Throw error when server rejects objects
-- during synchronization", and defaults to THROW. It uses the phrase MDL
-- already has for failure handling (a microflow's ON ERROR CONTINUE) rather
-- than a new keyword. OMITTING it leaves the stored value alone; DESCRIBE emits
-- it only when it is not the default.
--
-- The block REPLACES the stored list, the way MENU replaces the menu. An
-- entity's compatibility-mode flag has no syntax and is preserved across the
-- rewrite untouched; DESCRIBE NAVIGATION flags it rather than dropping it.
Expand Down
8 changes: 8 additions & 0 deletions docs-site/src/reference/navigation/alter-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ CREATE OR REPLACE NAVIGATION profile
[ MENU (
menu_items
) ]
[ ON SYNC ERROR { THROW | CONTINUE } ]
[ SYNC (
sync_rules
) ]
Expand Down Expand Up @@ -115,6 +116,13 @@ quoted `WHERE 'xpath'` still parses, but every quote inside it must be doubled.
The block replaces the stored list, the way `MENU` replaces the menu. Omitting
it leaves the stored configuration alone.

`ON SYNC ERROR THROW | CONTINUE` is Studio Pro's *"Throw error when server
rejects objects during synchronization"*, and defaults to `THROW`. It reuses the
phrase MDL already has for failure handling — a microflow's `ON ERROR CONTINUE`
— rather than introducing a keyword of its own. Omitting the clause leaves the
stored value alone, and `DESCRIBE NAVIGATION` emits it only when it is not the
default.

An entity's *compatibility mode* flag has no MDL syntax. It is read, preserved
across a rewrite, and reported by `DESCRIBE NAVIGATION` — never silently
dropped.
Expand Down
5 changes: 5 additions & 0 deletions mdl-examples/doctype-tests/navigation-offline-sync.mdl
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ create page "OfflineSync"."Mobile_Home"
-- names plus "Offline".
create or replace navigation "PhoneOffline"
home page "OfflineSync"."Mobile_Home"
-- Studio Pro's "Throw error when server rejects objects during
-- synchronization". Spelled with the phrase MDL already uses for failure
-- handling (a microflow's ON ERROR CONTINUE), and omitting the clause leaves
-- the stored value alone rather than resetting it.
on sync error continue
sync (
-- Fetched from the server; never held on the device.
sync "OfflineSync"."Setting" online;
Expand Down
Loading
Loading