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
2 changes: 2 additions & 0 deletions .claude/skills/fix-issue/findings/mdl-other.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@
{"area": "mdl/offlinepaths", "date": "2026-08-27", "symptom": "Adding an offline navigation profile makes a build fail with **CE6206** (\"Attribute paths with multiple steps cannot be used on pages that are accessible through an offline-based navigation\") in pages the statement never mentioned", "cause": "An offline profile restricts every page it can REACH to at most one association hop. The pages were valid before; creating the profile is what invalidated them, and mxcli writes multi-step paths happily and said nothing", "file": "`mdl/offlinepaths/scan.go` (the stored-document scan), `mdl/executor/offline_profile_warning.go` (the report), `mdl/types/navigation_profile_kind.go` (`IsOfflineProfileKind`)", "insight": "**The threshold is TWO hops, not \"any indirect reference\"** — one hop is explicitly allowed, pinned by a control inside one page where mxbuild flags column 2 of 2 and accepts column 1. Scan for the stored `DomainModels$AttributeRef` whose `EntityRef.Steps` holds ≥2 steps, keying on `$Type` and not on a table of widget property names — a DataGrid2 column's binding is nested several levels inside a `CustomWidgets$WidgetValue`, so a property-name scan reports a clean project for the widget people actually use. Do NOT match every `IndirectEntityRef`: a data source that navigates an association is a different element and CE6206 does not reject it. Report as a **warning**, never a refusal — reachability needs the whole page graph (home pages, menu items, and every page those open), which mxcli does not walk, and the end-to-end control shows mxbuild at 0 errors while the flagged page is unreachable and CE6206 the moment the profile's home page points at it. ako/mxcli-maintenance", "ce": ["CE6206"]}
{"area": "mdl/types", "date": "2026-08-28", "symptom": "`rename entity` / `rename module` reports success but prints no \"Updated N reference(s)\" line, and the next `mx check` fails **CE0174** \"Cannot resolve object name 'MyFirstModule.Period'\" on a view entity", "cause": "A view's OQL is the ONE place a qualified name is stored **embedded in a sentence** rather than as a property of its own. Both engines' rename walkers match a string that *equals* the old name or *begins with* it — correct for a BY_NAME property, and blind to `from MyFirstModule.Period as p` mid-query", "file": "`mdl/types/oql_rename.go` (`RewriteOQLQualifiedName`), wired at the `Oql` key in `mdl/backend/modelsdk/infrastructure_write.go` (`replaceQNInDocCounted`) and `sdk/mpr/writer_rename.go` (`replaceStringsInDoc`)", "insight": "**Ask where each reference is STORED, not just which documents reference it** — a scan built for whole-string properties silently covers 0 of the embedded ones, and reports 0, which reads like \"nothing referenced it\". Scope the rewrite to the `Oql` key: a blanket substring replace across every string reaches documentation and expressions, where a similar-looking name is not a reference. Three name-shaped ways it goes wrong, all covered by tests: a **longer name starting with the old one** (`M.PeriodDetail`) must not move, a **quoted** reference must stay quoted (bare is CE0174 — quoting is how a view names an entity called after an OQL reserved word), and a **module rename arrives as a prefix pair** (`\"Old.\" -> \"New.\"`) with no entity half to split, so it needs its own path or module rename stays broken while entity rename looks fixed. Where a query-local alias is spelled exactly like the module, the rewrite **declines** rather than guessing — 0 references reported is a visible non-event, a corrupted query is not. Both engines, one shared rewrite. ako/mxcli-captrack", "ce": ["CE0174"]}
{"area": "mdl/translations", "date": "2026-08-30", "raw": "| `create or modify translations in <Module> for <lang>` reports success (\"Set 212 nl_NL translation(s) across 20 document(s)\") and the app's pages switch language while the **menu does not** | `mdl/translations/outofscope.go` (new), `mdl/executor/cmd_translations.go`, `cmd/mxcli/syntax/features_misc.go` | The **navigation is a project-level document**, not a module one, so `in <Module>` never reaches it. Measured on the reporting project: 151 strings scoped against 546 unscoped, and re-running the same file unscoped landed 65 more across 22 further documents. Nothing warned — the document count was the only tell, and only if you knew what number to expect. A scoped run now names **the file's own entries** it did not reach (`translations.OutOfScope`), not \"the project has other strings\", which is true of every scoped run and would warn forever — the per-module workflow is exactly what the scoping exists to support. Second, load-bearing half: those entries were previously swept into the **drift** warning, whose premise (\"no text has this as its source\") is *false* about them — they matched, out of scope. They are subtracted from it, so \"the text may have been deleted\" is only said where it is true. Controls: an unscoped run of the same file reports nothing new and lands the strings; a key matching nothing anywhere is still reported as drift. Reported as ledger #137 |", "refs": ["#137"]}
{"area": "mdl/catalog", "date": "2026-09-03", "symptom": "`SHOW LANGUAGES` omits a language the project really has (ar_DZ absent from a list of 8 where the project has 9), and `search '<a widget caption>'` returns \"No matches found\" for a string `DESCRIBE TRANSLATIONS` lists. Nothing errors and the catalog builds clean.", "cause": "CATALOG.strings was filled by hand-written per-type extractors reaching five sites (page title, enum caption, three microflow message templates), so a text anywhere else — every widget caption, tooltip, validation message, client template — was never indexed.", "file": "mdl/catalog/builder_strings.go", "insight": "A language present only on an unindexed site is INVISIBLE, not undercounted, so it vanishes from SHOW LANGUAGES entirely and from lint rule QUAL005, which discovers its language set from the same table. The fix is not a sixth case — that is how five was ever the number. Index from the type-agnostic walk DESCRIBE TRANSLATIONS already uses (translations.SitesInUnit over ListRawUnitsByType(\"\")), leaving only non-Texts$Text strings in the typed path (URLs, log nodes, REST paths, documentation, and Microflows$StringTemplate, which holds a plain Text and cannot carry a translation). Derive ObjectType from the unit $Type mechanically rather than via a table. Measured before: 69 of 3265 texts, 8 of 9 languages, 66 en_US of 1045. After: 1496 rows, 9 languages, counts identical to an independent BSON walk. Atlas design templates are ~70% of the corpus and are indexed rather than excluded, because CREATE TRANSLATIONS writes them and a SHOW LANGUAGES that excluded them would reopen the same split. CONTROL: stub the walk and the run reports `strings: 3` with SHOW LANGUAGES reporting nothing at all.", "refs": ["#250"]}
{"area": "mdl/linter", "date": "2026-09-03", "symptom": "Lint rule QUAL005 reports no missing translation for an enumeration where only one value is translated (11 real gaps unreported), and likewise for a page's sibling action buttons.", "cause": "The rule grouped by (QualifiedName, StringContext) while ElementId sat unused in the strings table, so every sibling element of one type collapsed into one group and a single translated value made the set look complete.", "file": "mdl/linter/rules/missing_translations.go", "insight": "Add ElementId to the SELECT, the ORDER BY and the elementKey struct. No test caught it because the harness synthesized ElementId from QualifiedName+StringContext, giving every sibling the same value and reproducing the defect inside the fixture — a fixture that encodes the bug cannot detect it. CONTROL: with every sibling translated the run must stay at 0 violations, or the new violation is an artifact of splitting the group rather than the missing translation.", "refs": ["#250"]}
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

- **`CATALOG.strings` indexes every translatable string, not five hand-picked kinds** — `SHOW LANGUAGES` listed 8 of a project's 9 languages and `search` could not find a widget caption that `DESCRIBE TRANSLATIONS` had just listed. The index was filled by per-type extractors reaching five sites (page title, enum caption, three microflow message templates), so a text anywhere else was never indexed: measured on a stock 11.13 app, **69 of 3265 texts and 8 of 9 languages**. A language present only on an unindexed site is *invisible* rather than undercounted, which also blinded lint rule QUAL005 — it discovers its language set from the same table.

The rows now come from the type-agnostic `Texts$Text` walk that `DESCRIBE TRANSLATIONS` already uses, so the two subsystems cannot disagree about what the project contains; the typed path keeps only the strings that are *not* translatable (URLs, log node names, REST paths, documentation, and the `Microflows$StringTemplate` a workflow name is stored in). `StringContext` now names the site — `Forms$ActionButton.Caption` rather than `page_title` — and `ObjectType` is derived from the unit `$Type` mechanically, so a document type Mendix adds later is named correctly with nobody maintaining a list. Same project after: 1496 rows, 9 languages, counts identical to an independent BSON walk. Atlas design templates are ~70% of the corpus and are indexed rather than dropped, because `CREATE TRANSLATIONS` writes them and a `SHOW LANGUAGES` that excluded them would reopen the same split; `ObjectType` is how a consumer filters them.

- **QUAL005 reports the sibling elements it used to fold together** — the rule grouped by `(QualifiedName, StringContext)` while `ElementId` sat unused in the table, so an enumeration's twelve values became one group and translating any single value made the whole set look complete. Grouping now includes `ElementId`. The existing test harness synthesized that column from `QualifiedName+StringContext`, which is why no test caught it.

- **`call external action` now types its return value and its parameters** (mendixlabs/mxcli#1020) — a call against a consumed OData service was written with neither the result variable's type nor its parameters' types, so Mendix reported `CE7269` ("the return type for remote action … has changed") and `CE7252` ("the parameters … have changed"), and re-running `CREATE OR MODIFY EXTERNAL ENTITIES` never cleared them.

It never could. Both codes are defined on `CallExternalAction.cs` — they are raised by the microflow **activity**, not by the entity — which is what made the reported remedy the wrong lever. Two omissions of the same shape, each a `DataTypes$` sub-document that was never written:
Expand Down Expand Up @@ -43,6 +49,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

- **`describe navigation` no longer drops a profile's login page and not-found page** — the two clauses were written correctly and sat on disk, but the modelsdk reader type-asserted only the `$Type`s `modelsdk/gen` declares for those slots, and neither is what the documents carry: `LoginPageSettings` is stored as `Forms$FormSettings` (page under `Form`) and `NotFoundHomepage` as `Navigation$HomePage`. A failed type assertion leaves the field empty, so `DESCRIBE NAVIGATION` printed neither clause and pasting its output back — the copy workflow the docs recommend — deleted both from the profile.

The legacy engine read the same bytes correctly the whole time, which is both the diagnosis and the control: the two engines now print identical output for the same document. The two slots were wrong in opposite directions. For the login page a blank app's own navigation document and `generated/metamodel` agree with the writers, so **gen** is wrong. For the not-found page — Studio Pro's **"Fallback page"** — gen and `generated/metamodel` agree with each other and mxcli's three **writers** were the odd one out, storing `Navigation$HomePage` where Studio Pro stores `Navigation$NotFoundHomePage`; the writers are corrected and now reproduce a Studio Pro-authored fallback page exactly. mxbuild accepts either spelling, so only a reference document could separate them. The reader keeps accepting both, because every not-found page mxcli wrote before this carries the old spelling and has to keep round-tripping.

The legacy engine read the same bytes correctly the whole time, which is both the diagnosis and the control: the two engines now print identical output for the same document. The two slots were wrong in opposite directions. For the login page a blank app's own navigation document and `generated/metamodel` agree with the writers, so **gen** is wrong. For the not-found page — Studio Pro's **"Fallback page"** — gen and `generated/metamodel` agree with each other and mxcli's three **writers** were the odd one out, storing `Navigation$HomePage` where Studio Pro stores `Navigation$NotFoundHomePage`; the writers are corrected and now reproduce a Studio Pro-authored fallback page exactly.

**Correction to an earlier claim in this entry:** it previously said mxbuild accepts either spelling, and that only a reference document could separate them. That is wrong, and it understated the bug. Measured on 11.13 against a build emitting the old spelling, both `mx check` and `mxbuild --target=deploy` exit 1 with `Object of type 'Mendix.Modeler.WebUI.Navigation.HomePage' cannot be converted to type 'Mendix.Modeler.WebUI.Navigation.NotFoundHomePage'` — the project cannot be **loaded**, so every check downstream is lost with it. Nothing caught it because nothing had ever *built* a project with a fallback page set: the automated `mx check` coverage runs `doctype-tests/` only, and no script there sets one — the first that does was added by this fix. The reader keeps accepting both spellings for a different reason than stated: a project written before this does not build at all, and mxcli parses the BSON directly, so reading the old spelling is what lets it open that project and repair it.
Expand Down Expand Up @@ -491,7 +499,6 @@ Headline: **A statement mxcli accepts is now a statement mxcli honours.** This r
- An additive chain keeps its operators in the order they were written.
- A building-block datasource override is rebound by widget type, not by one that happens to be present already.


## [0.18.0] - 2026-08-14

Headline: **mxcli can now maintain a project it did not author.** Marketplace modules install and update headlessly — carrying the GUIDs the database keys on, the role grants that live outside the module, and the MPR v2 format `mx module-import` silently collapses — and `marketplace diff` reports which elements were edited locally before an update replaces them. Alongside that, a write that changes nothing no longer touches the file, five more document types become authorable (task queues, scheduled events, regular expressions, validation rules, menus), and a long tail of activities that could be written but not read back stop disappearing from the describe → edit → re-exec loop. Separately, the Windows and macOS binaries stop shipping the embedded tunnel — 13.5 MB smaller, and no longer carrying the tunnelling stack that had Defender and enterprise EDR blocking mxcli on managed corporate endpoints.
Expand Down
31 changes: 25 additions & 6 deletions docs-site/src/internals/catalog-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,31 @@ CREATE TABLE PERMISSIONS (

```sql
CREATE VIRTUAL TABLE STRINGS USING fts5(
name, -- Document qualified name
kind, -- Document type
strings, -- All text content concatenated
tokenize='porter unicode61'
);
```
QualifiedName, -- Document qualified name, e.g. MyModule.Home
ObjectType, -- Document type, derived from the unit $Type: PAGE,
-- PAGE_TEMPLATE, BUILDING_BLOCK, MICROFLOW, ENUMERATION, ...
StringValue, -- The string itself
StringContext, -- Where it lives. For translatable text this is
-- <owner $Type>.<property>, e.g. Forms$ActionButton.Caption.
-- Non-translatable strings keep a plain label: page_url,
-- log_node, documentation, rest_path, task_name, ...
Language, -- Language code, empty for non-translatable strings
ElementId, -- The owning element's $ID — what distinguishes an
-- enumeration's twelve values from each other
ModuleName
);
```

Every `Texts$Text` in the project is indexed, found by a type-agnostic walk
rather than per-document-type extraction, so a caption in a document type mxcli
cannot otherwise read is still searchable. That includes Atlas's design
templates (`PAGE_TEMPLATE`, `BUILDING_BLOCK`), which are roughly 70% of a stock
project's text and never render in a running app — filter them out with
`ObjectType` when you want only the app's own strings.

An empty translation — a text that exists but is not translated yet — is **not**
a row, so a language's presence in this table means it is actually translated
somewhere.

### SOURCE (FTS5)

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/language/translations.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ modules ship translations in **nine**, so "other languages already have
translations here" is true and misleading.

> `SHOW LANGUAGES` lists languages that **have translations**, which is a
> different list — a stock app reports eight while one is enabled. The enabled
> different list — a stock app reports nine while one is enabled. The enabled
> list is in `DESCRIBE SETTINGS`.

## Drift: a source string that was edited
Expand Down
Loading
Loading