Seventeen changes, mostly from one real project: a decision outcome that made projects unopenable, a glyph that broke deploy at 0 errors, and offline sync - #1075
Merged
Conversation
…uses `mxcli check --references` under-reported. Two switches in validate_duplicates.go with nothing comparing them: stmtCreateInfo classified 24 document types, projectNameSets.setFor knew 20. The four that fell through returned nil, which the caller reads as "no conflicts for that type". The cost is not a missing warning. `exec` stops at the first conflict having already written every statement before it, so a script that only fails at execution time leaves the project half-modified. Measured on a three-statement script: check said "Check passed!", exec created the first entity, died on the rule, and never reached the third statement. Three of the four gaps were real and are now wired up — association, rule and javascript action, each of which exec refuses with "already exists". The fourth, module, is a deliberate exemption: CREATE MODULE is a no-op when the module exists (prints "already exists", exits 0), which is what lets `create module M;` open nearly every script. It is recorded as an exemption with its reason rather than left as an omission. A second, pre-existing defect surfaced while sweeping for false positives: stmtCreateInfo's idempotent flag read CreateOrModify only, missing the third spelling, IF NOT EXISTS. Exec skips such a statement with "already exists — skipped", but check reported it as a conflict — so a re-runnable domain script failed its own second run at check time. Fixed for entity and association, the two statements that carry the modifier. Guards, which are the actual fix for the class: three tests read BOTH lists out of the Go source with go/parser rather than restating them, since a guard that keeps a third copy of the list joins the defect instead of ending it. Each t.Fatals on an empty extraction so it cannot pass vacuously. Writing the mirror guard paid immediately — it found that stmtDropInfo had no DropRuleStmt case, so making rules project-checked would have turned `drop rule X; create rule X;` into a false positive this change was about to introduce. Measured, over the 88 example scripts that exercise the changed paths, each exec'd into a fresh project and then re-checked: 621 conflicts before, 724 after. 105 added, every one verified to trace to a plain CREATE in its script; 2 removed, which were the pre-existing IF NOT EXISTS false positives. Controls: reverting each of the four parts of the fix makes its test fail with the reported symptom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
`ALLOWED_ACTIVITY_TYPES` held ExclusiveSplit and not ExclusiveMerge, and an `if` produces both. So an ACT_ microflow that guards anything — "do not open a page with an empty parameter", the most ordinary thing an action microflow does — was flagged for its own closing brace while the branch it closes was permitted. Measured on a microflow whose ONLY violation was the merge, and 122 times over on one real project (ako/CapTrackV4 R11). This package had already settled the question in the other direction: countMicroflowActivities excludes ExclusiveMerge as structural, with a comment saying so. CONV010 was the only place treating it as business logic. The test asks the catalog's own labeller for the two names rather than hardcoding them, matching the existing action test, and a control keeps LoopedActivity and InheritanceSplit flagged — a fix that widened the list to "anything that is not an ActionActivity" would pass the first test and gut the rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…w it names A microflow wired as the project's AfterStartupMicroflow reported no callers and no references, and `mxcli lint` QUAL004 said "not called from anywhere. Remove if unused." Taking that advice left a dangling name that `mx check` did not catch either — it surfaced only when the runtime refused to start (ako/CapTrackV4 049, R13). The runtime is what calls these, so nothing in the model does, and the reference graph had no edge for it. Three settings name a microflow: AfterStartupMicroflow, BeforeShutdownMicroflow and HealthCheckMicroflow. Each now emits a `settings` edge, the same shape and for the same reason as the `schedule` edge a scheduled event emits. The source of the edge is the SETTING, not the project, so `show references to <microflow>` names which setting depends on it — the thing you need before dropping it, and otherwise only reachable by reading `describe settings`. The list of settings is a literal rather than reflection over ModelSettings, because most of that struct is strings that are not microflow names and a wrong entry would invent an edge rather than miss one. Verified on a real project: `show references to` goes from "no references found" to naming AfterStartupMicroflow, and QUAL004 stops reporting it while still reporting a genuinely uncalled microflow beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…it writes MDL-WIDGET11 resolved `datagrid` against Atlas Core's `DataGrid` — the DEPRECATED data grid — while MDL's `datagrid` has always written Data grid 2 from the DataWidgets module. Their design properties are disjoint: Atlas Core DataGrid Style, Hover style, Row size DataWidgets com.mendix.widget.web.datagrid.Datagrid Borders, Compact, Hover, Striped So the tool warned that Compact / Hover / Striped were "not defined for this widget type" — they are exactly its properties — and suggested Style and Row size, which mxbuild refuses with CE6083 "not supported by your theme". Taking the advice turned 16 warnings into 17 build errors (ako/CapTrackV4 010). The warning now flips the right way round on the same two scripts: 3 warnings to 0 on the working one, 0 to 2 on the one that does not build. Three more keywords were wrong in the quieter direction. `combobox`, `gallery` and `image` named keys that no web design-properties.json defines, so the registry lookup missed and validateWidgetDesignProps skipped those widgets entirely — silence that reads as approval. A gallery with a made-up design property is now reported. The fix derives the key rather than listing it, from the two places that already decide which widget a keyword writes: keywordDispatchTable and the embedded widget definitions. The hand-written table keeps the native widgets, which are the majority. A test fails if a keyword appears in both halves, since the native entry would be dead code. Each of the four pairings was measured by writing the widget and reading its type back out of the catalog, not inferred from the builder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
`create or modify snippet M.S (params: { $T: Mod."Thing" })` failed at
execution with `failed to resolve entity Mod."Thing": entity not found`,
while the identical quoted form in a PAGE parameter resolved fine
(ako/CapTrackV4 019). The project convention is to quote every identifier,
so this was reached by following the house style, and the asymmetry gives
no clue which of the two spellings is the odd one.
The cause was one line: buildSnippetParameterListAsPage re-split the parse
node's TEXT, and GetText() returns the source verbatim, quotes included.
The page path has always walked the parse tree, where buildQualifiedName
unquotes each part.
A correct implementation already sat beside it — buildSnippetParameters,
which nothing called. Two copies of one conversion, one of them dead, is
how they drifted; the dead one is removed rather than left to be the next
thing someone fixes by accident.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
`mxcli lint` QUAL002 reported "Page 'X' has no documentation" against a page carrying a javadoc comment, the catalog's Description column was blank for every page and snippet, and `describe page` emitted no documentation — so the comment looked, from every angle, like it had been dropped (ako/CapTrackV4 R12). It had not. The comment reaches the AST, the executor sets it and the writer stores it: `mxcli bson dump --type page` shows Documentation with the right value, and the LEGACY reader parses it back correctly. Only the default engine's readers failed to carry it, and page and snippet were the two that did, out of five sibling readers in the same file — layout, building block and page template all had it. Every symptom the report lists is downstream of that one omission, DESCRIBE included, which now round-trips. The report's third row is a correction rather than a fix. A Mendix module HAS no documentation property, measured three ways: the metamodel's ProjectsModule declares none, modelsdk/gen's Module offers no accessor, and none of a real project's stored Projects$ModuleImpl units contains the key. QUAL002 was asking for something no editor can supply and reporting every module of every project forever, so modules are dropped from the sweep. That change cost the System-module test its positive control — it proved the exclusion had not over-reached by checking the user's own module was still reported. It now checks the user's own entity instead, since a module being absent no longer proves anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Neither statement had an idempotent form, so a one-time cleanup of what `mxcli new` ships either broke every later run of its slice script or had to be commented out — which is what a real project did (ako/CapTrackV4 R5). Re-running every mdlsource/*.mdl in order is what a fresh clone does, and that loop is how the class was found. Five statement forms there succeeded once and failed after; four already had an idempotent spelling (`create or modify`), and these two had none at all, which is exactly why they are the ones that ended up commented out. The spelling reuses the grammar's own ifExists rule, so it matches ALTER ENTITY's `drop attribute if exists` rather than inventing a second way to say it. A missing target prints "does not exist, skipping" and returns cleanly; without the clause it is still an error, which the example exercises as a control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
A glyph code is a bare integer, so nothing resolved it: `icon glyph 57562` passed `mxcli check` AND `mx check` at 0 errors and broke the deploy with ERROR: One or more errors occurred. (An exception occurred while exporting layout 'CapTrack.App_Default'.) which names a document that is not the cause and never mentions navigation. Bisecting it cost three build cycles (ako/CapTrackV4 007, R2). Measured on 11.14.0 with two full builds of one project differing only in the code. 57562 (0xEBDA) fails at the layout export with InvalidOperationException "Sequence contains no matching element" at Forms.Icons.GlyphFont.GetClass(Int32) — a LINQ .First(...) over mxbuild's glyph table, which throws rather than reporting. 57377 (0xE021) exports pages and layouts cleanly. The rule checks the cmap of the font Atlas_Core ships (glyphicons-halflings-regular.woff): 247 codes in 35 runs, which agrees with mxbuild on both measured points. It is a literal table because the check has to run in the project-free pass, which is how CI runs `mxcli check`. A warning, not an error: the table is a snapshot of a Mendix asset, and if Mendix extends the font then refusing a newly-valid code would be worse than the gap this closes. The message carries the way out, since mxbuild's stack trace does not lead back to the cause — an icon collection reference is a model reference and `check --references` resolves it before anything is written. Tests include the gaps (the font is 35 runs, not one range, so a first-to-last check would accept every bad code in a hole) and the single-code runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
One record per fix, each carrying the measurement that settled it and the cheap discriminator that would have found it faster — `bson dump` to separate a write bug from a read bug, an engine split to localise a defect to one backend, and reading a widget's type back out of the catalog rather than trusting the builder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
`show icon collections` / `describe icon collection` cover icons that are
model documents. A glyph is not one — `icon glyph <n>` stores a bare
character code into a FONT — so there was nothing to list, and MDL078 had
to spell its advice as numeric ranges with holes in them:
"use a code the font defines — 57345-57495, 57601-57753,
57856-57952 (with gaps), or 63743"
which is close to unusable. It now points at `show glyphs` instead.
show glyphs; -- all 247, with names
show glyphs like 'star'; -- 57350 star, 57351 star-empty
describe glyph 57350; -- by code
describe glyph 'star'; -- or by name
Both halves of the data come from assets Mendix ships. The CODES are the
private-use range of the cmap in Atlas_Core's
glyphicons-halflings-regular.woff — already verified against mxbuild by
two deploy builds when MDL078 landed. The NAMES are the
`.glyphicon-<name>:before { content: "\eXXX" }` rules in Atlas_Core's
bundled Bootstrap stylesheet, which covers all 247 with none left over.
MDL078's range table is gone: glyphCodeDefined now searches the same
table, so the set the rule accepts and the set the command lists cannot
drift apart.
Neither statement needs a project, for the same reason MDL078 runs in the
project-free pass: the answer is in a font, not in the model. LIKE matches
the NAME, including aliases, because that is the direction an author needs
— they know they want a star, not that a star is 57350. An ambiguous name
lists the candidates rather than picking one, since their codes differ.
The keyword-coverage guard caught the token being added without a row in
the keyword rule, which would have made `glyphs` unusable as an ordinary
identifier; a test now pins the icon clause still parsing, since GLYPH and
GLYPHS differ by one character.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
`mxcli check --references` reported an enumeration as missing — attribute 'CriticalPathStation': enumeration not found: Approval.StationKey while DESCRIBE ENUMERATION returned its values, SHOW ENUMERATIONS listed it, `exec` wrote the attribute and mxbuild built the project at 0 errors (mendixlabs#1071). A pure false negative: the only broken thing was the checker, and it blocked scripts that are entirely valid. It read as "enumerations are never resolved" because the reporting project keeps its enumerations in folders, and that is the whole discriminator. enumerationExists matched containers directly, if enum.ContainerID == module.ID && enum.Name == enumName which only ever holds for an enumeration sitting in the module ROOT: one inside a folder has the FOLDER as its container. Every other command resolves through the container hierarchy, which walks folders up to the module, so the reference checker was the only one that could not see inside one. This is upstream mendixlabs#976 a second time. That fix corrected DROP's container matching and did not sweep for the other caller of the same question — its own test file even spells out the class ("SHOW, DESCRIBE and ALTER all use the container hierarchy... DROP was the one command of the four"). The reference checker was the one left. enumerationExists now defers to findEnumeration, which deletes the duplicate rather than patching the copy. Two implementations of "does this enumeration exist" are what let them drift, and only the interactive one was ever exercised; the delegation also picks up the live-over-excluded handling (mendixlabs#914) that the copy never had. Both call sites are covered, not just the reported one: ALTER ENTITY ADD ATTRIBUTE (validate.go:660) and CREATE ENTITY with an enumerated attribute (:447), which fails identically and was not in the report — measured before fixing. The controls hold: an enumeration at the module root still resolves, a genuinely missing one is still reported, and a foldered one still does not answer for another module's name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
fix(check): resolve an enumeration inside a folder (mendixlabs#1071)
…r questions The proposal listed four things a real document had to settle and said no project on this machine had one. ako/TestApp does: its TabletOffline profile configures seven entities across all six sync modes, including the constrained one. Every question is now measured rather than inferred, and two answers were not what the draft assumed. Studio Pro writes FOUR properties, not gen's six. DownloadMode and ShouldDownload occur zero times in the document — gen declares them and nothing on a web profile writes them. That shrinks the carry problem from three-of-six to one-of-four: CompatibilityMode is the only written property MDL will not spell. It also adds a rule to the writing section, because the inverse mistake is the expensive one: a property absent from every real document is one Studio Pro fills in on load, and emitting it is how a document mxbuild accepts becomes one Studio Pro cannot open. ThrowPartialSyncError is the throw-on-reject checkbox, and it is in NEITHER generated source — zero occurrences in modelsdk/gen and zero in generated/metamodel. It is also on the online Responsive profile, so it belongs to every web profile. A property neither source knows cannot go through the codec's typed accessors at all, so it becomes a raw-BSON overlay under the settingsoverlay rules. That is a constraint on the design, not a detail, and it splits cleanly out of the SYNC block in the phasing. CompatibilityMode is real and present on all seven configs, so the arbiter question resolves in gen's favour — generated/metamodel being a 11.6.0 snapshot that predates it, exactly the caveat CLAUDE.md documents. The caption-to-key mapping is confirmed against stored values rather than read off the dialog, and since all six members occur in one profile all six get MDL words; None vs NoneAndPreserveData is a modifier rather than an unrelated word because the difference is whether data already on the device survives. Three smaller things the document changed. System.Language is configured, so a rule refusing System or Marketplace entities would reject a real project. The constraint is stored multi-line with Mendix's doubled-quote escaping, which is the one part of the syntax with a non-obvious round-trip test. And the earlier claim that five local projects had offline configs is corrected in place: that scan matched the OfflineEntityConfigs key every profile carries, not the element. What remains unmeasured is named rather than assumed away: native profiles, where DownloadMode and ShouldDownload are the obvious candidates for being written, and CompatibilityMode: true, which no reference config carries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven fixes from the CapTrackV4 report, plus glyph browsing
…#1073) A `call external action` on an OData action with a NULLABLE parameter was CE7252 "The parameters for remote action '<x>' have changed", and no MDL cleared it. Microflows$ExternalActionParameterMapping.CanBeEmpty was never set, so it was Go's false on every mapping mxcli wrote. Mendix compares it against the contract's Nullable on every build and reports the disagreement as CE7252. mdl/types.EdmActionParameter already parsed Nullable as a three-state *bool; the value was dropped between the parser and the mapping builder, so this carries it rather than adding any parsing. The fix is in the shared semantic builder, so both engines are covered by one change. CSDL makes Nullable optional on <Parameter> and defaults it to TRUE — the opposite of Go's zero value — so an absent attribute means nullable. The reported premise was wrong and the bug was real; they were not the same thing. A Studio Pro reference document (ako/TestApp, 11.14.0) settled both: the stored mappings are {command, Argument "empty", CanBeEmpty false} and {additional, Argument "empty", CanBeEmpty true}. So Argument is the EXPRESSION `empty`, never an empty string — `additional = empty` was always correct MDL and already wrote a byte-identical Argument, and `= empty`/`= null` parse at v0.20.0 too (the three grammar rules involved are byte-identical there). What actually differed was CanBeEmpty, which no syntax reaches because it is derived from the contract, not typed by the developer. Controls: reverting only the assignment takes the four-statement repro from 0 to 4 errors, one CE7252 per call; defaulting an absent Nullable to false reproduces CE7252 on Annotate alone, which pins the CSDL default. Studio Pro's own microflow in the same project is the 0-error control. Not fixed, deliberately: Studio Pro also writes empty AdditionalAttributes and IncludedAssociations markers on the call and each mapping. mxbuild 11.14 builds at 0 errors without them and they are unverified against Studio Pro, so they are recorded in the finding rather than guessed at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…on read Phase 1 of PROPOSAL_offline_sync_configuration.md: read-only, no syntax, no writing. It is the precondition slice — nothing can be written safely until everything stored is read. CompatibilityMode was read from the model and discarded. types.NavOfflineEntity carried three of the four properties Studio Pro actually writes, so a write path built on that model would have dropped it with no error and no mx check failure, exactly the way `create or modify entity` dropped access rules. It is now carried through both engines and the convert layer. DownloadMode and ShouldDownload are deliberately NOT added, though gen declares them. They occur zero times in the reference document, so the risk here is inverted from the expected one: a property absent from every real document is one Studio Pro fills in on load, and emitting it is how a document mxbuild accepts becomes one Studio Pro cannot open. Two things found while doing it, neither in the plan. TestFieldCountDrift passed VACUOUSLY. It exists to catch drift on the structs convert.go copies by hand, and neither NavOfflineEntity nor NavigationProfile was in its list — so adding a field left it green. Both are guarded now. A drift guard that passes on a struct it does not know about is worse than none, because it reads as coverage. `describe navigation` was already broken on real data. Studio Pro writes an offline sync constraint multi-line, and the comment interpolated it verbatim, so the `--` ended after `where '[` and four lines of raw XPath followed as if they were MDL. Pre-existing, and only a document carrying a real constraint could reveal it — no local project had a populated offline config at all. Verified end to end against ako/TestApp: all seven configs read back, spanning all six sync modes, with the constraint folded onto one line. The table test covers those seven plus a CompatibilityMode: true case, which earned its place immediately — reverting the carry made it the ONLY failing subtest, so the seven real configs alone would have passed against broken code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`alter page P { set PageSize = 10 on dgProducts }` failed with
pluggable property "PageSize" not found
on a DataGrid 2 that `create page … (PageSize: 20)` had just written and the
app really paged at. `mxcli check --references` passed the script, so the
failure only appeared at exec, after earlier statements had already been
written; the documented workaround was to re-emit the whole page with
`CREATE OR MODIFY`, turning a two-value edit into a 190-line script.
A pluggable property is keyed in the widget template in lowerCamel
(`pageSize`). CREATE resolves the author's spelling case-insensitively (the
widget engine's lookupProperty, and WidgetV3.GetStringProp before it);
setPluggableWidgetPropertyMut compared the template key byte-for-byte, so
only the exact `pageSize` worked. DESCRIBE PAGE prints the capitalised
`PageSize:`, so describe -> edit -> exec produced a script mxcli then
refused to run.
This is the sequel to alter-page-lowercase-set-on-builtin.mdl, which fixed
the same class for first-class properties and left the pluggable fallback
case-sensitive on the belief that template keys must be matched exactly.
They are stored case-sensitively; that is not a reason to match them that
way. Measured across every shipped widget template and definition: 96
property scopes, 1208 keys, 0 pairs differing only in case — so the relaxed
match is unambiguous, and TestPluggablePropertyKeysAreUniqueIgnoringCase
keeps that true as templates are added.
Both engines share pagemutator, so the one change fixes modelsdk and legacy
(verified separately). A genuine typo still errors, which is the author's
only signal: check --references does not resolve pluggable property names.
Verified end-to-end on a real 11.13.0 project — all four spellings alter the
grid, `mx check` reports 0 errors, and the control (`set PagSize`) still
fails. The regression test fails with the reported message when the fix is
reverted.
Refs mendixlabs#1069
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…ing on an unquoted mapping value Two workflow defects reported upstream, both of which reported success and then failed somewhere the author could not see. mendixlabs#1031, mendixlabs#1065 — a decision outcome that is not fully qualified makes the project UNOPENABLE. Mendix stores the value in EnumerationValueConditionOutcome.Value and parses it through EnumerationValueIdentifier.FromString in the UnitLoader, before any consistency check runs, so the failure is a StorageLoadException with no CE number and no "The app contains: N errors." line — while mxcli check, exec and describe workflow all reported success. What settled the threshold was three runs of the same workflow, one per copy of the same app, verdict read off the literal mx check line: 'OutcomeA' (1 segment) -> StorageLoadException, unloadable 'Status.OutcomeA' (2 segments) -> StorageLoadException, unloadable 'WFP.Status.OutcomeA' (3 segments) -> The app contains: 0 errors. The two-segment row is the one worth keeping: "qualify it" is ambiguous without it, and shortening an enumeration that lives in the same module is exactly what an author would try. It agrees with the stored corpus, where every EnumerationValueConditionOutcome holds Module.Enum.Value. MDL-WF03 now requires that form, on CREATE WORKFLOW and on ALTER WORKFLOW … INSERT CONDITION, which writes the same field through wfmutator. Because the rule is an error and exec refuses a script with errors, the corrupting write can no longer happen. This TIGHTENS the rule #408 had widened: that change kept bare identifiers accepted on the reasoning that the rule was only there to catch free text, and the loader measurement shows a bare identifier is the corruption. mendixlabs#1023 — an unquoted `with (Ctx = $WorkflowContext)` crashed the binary with a SIGSEGV on check, check --references and exec alike. The grammar requires a string literal there, but visitor.Build() walks the parse tree even when the parse failed (deliberately — that is what lets check report more than the first error), so under ANTLR error recovery the rule was visited with a nil STRING_LITERAL child and read unguarded. Both mapping sites now nil-check and skip, leaving the syntax error the listener already recorded as what the author sees: "mismatched input '$WorkflowContext' expecting STRING_LITERAL". mxcli syntax taught both broken forms — the decision entry's example used 'Under 1000' / 'Over 1000', and the WITH clause was absent from both call entries — so the docs are corrected alongside. Verified with controls. Reverting the regex leaves the bare and two-segment rows undetected; removing the nil guard reproduces the SIGSEGV in the test. End to end: check refuses the bare outcome with both outcomes named, exec refuses and writes nothing (no module created), the unquoted mapping reports a syntax error on check and exec, and the new bug-test fixture execs into a real 11.10.0 app at 0 errors. Refs: mendixlabs#1031, mendixlabs#1065, mendixlabs#1023 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…come-and-crash # Conflicts: # CHANGELOG.md
Phase 2 of PROPOSAL_offline_sync_configuration.md. An offline profile
downloads NOTHING until each entity is given a sync mode, so before this 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.
Full pipeline: lexer -> grammar -> AST -> visitor -> executor -> both backends
-> describe, plus the syntax topic, quick reference, skill and a doctype
example exercising all six modes.
sync (
sync Mod.Setting online;
sync Mod.Vehicle all;
sync Mod.Trip where '[Distance > 0]';
sync Mod.Audit never;
sync Mod.Lookup none;
sync Mod.Draft none preserve data;
)
WHERE implies Constrained rather than naming it, so a constraint without a mode
and a mode without a constraint are both unspellable rather than merely
diagnosable.
The write is an overlay keyed by entity, not a rebuild: CompatibilityMode is
stored, has no syntax, and is put back untouched. Every reference config
carries false, so a writer that always emitted false would look correct against
all seven — the true case is the only thing that distinguishes them, and it is
the test that fails when the carry is broken. DownloadMode and ShouldDownload
are deliberately not written though gen declares them; they occur zero times in
the reference document, and a property Studio Pro fills in on load is one whose
emission makes a document Studio Pro cannot open.
Both engines got the same overlay, with a parity test. Two writers drifting
apart is how an engine-specific defect hides.
The enum guard mendixlabs#1035 earned: every MDL word maps to a declared
Navigation$SyncMode member, every member has a spelling, and Studio Pro's
captions ("All Objects", "By XPath") are refused rather than written — they are
not members of the enumeration at all.
The gates caught a collision this feature's own tests structurally could not.
NEVER is already a page property value (`editable: never`), so the new token
broke a previously-passing unrelated example until all four words were added to
the keyword rule. Recorded as a finding: grep the examples for a word before
making it a token, and test that it still parses as an identifier — the
coverage test only asserts the rule lists it.
Verified against ako/TestApp: describe -> exec -> describe is byte-identical,
including the six-quote constraint (the stored XPath carries Mendix's escaping
and MDL's doubling composes with it); the written BSON is marker 3 plus exactly
the four properties, with ThrowPartialSyncError and the PWA settings untouched;
mx check reports 0 errors both there and on a project the new example builds
from scratch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ide literals
The SYNC block took `where '<xpath>'`, a quoted string, so every quote inside
doubled. A stored constraint already carries Mendix's own escaping, so the two
compose: ako/TestApp's constraint came back as SIX consecutive quotes. Correct,
verified by round trip, unreadable.
It was also inconsistent. The bracket form is the first-class one and predates
this work: `retrieve … where [...]`, `visible: [...]`, `editable: [...]` all
take it, and `describe microflow` already emits a retrieve's constraint
unquoted. Only the new block was the outlier.
WHERE now takes `xpathConstraint` and DESCRIBE emits it, so nothing is escaped:
where '[ ( contains(ActionValue, ''''''abc'''''') ) ]' before
where [ ( contains(ActionValue, '''abc''') ) ] after
The remaining triples are Mendix's own escaping inside the stored XPath — they
encode the literal 'abc' — so reproducing them verbatim is right. MDL adds
none. The quoted form still parses, because scripts contain it, and a test
asserts the two forms produce an identical stored constraint.
Also fixes a defect the change exposed. singleLine folded whitespace with
strings.Fields, which collapses it INSIDE string literals too, so a constraint
containing 'two spaces' would silently match on 'two spaces' — a change to the
value being selected, in a place nobody would look. The fold now tracks quote
state and treats a doubled quote as an escape rather than a close. The first
attempt at that was itself wrong, emitting the deferred space inside the
literal it preceded; the table test caught it.
Round trip converges after one write and is stable thereafter — the one-time
change is normalizeXPathTokens, which every retrieve constraint already goes
through, so it is existing behaviour rather than something introduced here.
Reported upstream as mendixlabs#750, whose general case is now
PROPOSAL_first_class_expressions.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…erties Written for mendixlabs#750, which lists four design questions and defers them to a proposal. It has sat two months, and the reason is in question one. The framing needs correcting first. "Expressions are stored as strings" is the natural reading and is wrong — PROPOSAL_expression_type_checking.md already corrected it once, in its 2026-06-19 revision: the visitor parses expressions into typed mdl/ast nodes, XPath included, and a retrieve's constraint already round-trips first-class. The defect is narrower: specific properties are declared as generic quoted-string slots while the expression grammar sits unused beside them. dynamicclasses is handled by name as an ordinary string property and never meets an expression rule at all. The blocking question is that there are TWO expression families, not one. The bracket form parses xpathExpr and serves visible/editable/retrieve-where, while dynamicclasses is a full Mendix expression with if/then/else. The recommendation is not to unify them: one delimiter over two grammars means either a parser that guesses the language or an XPath rule quietly extended until it accepts if/then/else, and a value parsed under the wrong grammar stores cleanly and fails at build. dynamicclasses should take the UNDELIMITED microflow expression form, because brackets read as XPath everywhere else. Sized rather than asserted: 23 runs of four-or-more consecutive quotes across the shipped skills and examples, twelve of them five long. The worst case is multiplicative rather than additive, and turns up wherever a stored value already carries Mendix's escaping. DESCRIBE is the point rather than a nicety. Three shipped bugs share one root — a describer that has to escape eventually will not: mendixlabs#1006 emitted TARGETING USERS XPATH without doubling and failed its own check, #394 emits unescaped quotes in enumeration captions, mendixlabs#642 mis-stored every quoted where-constraint as CE0161. Emitting a form that needs no escaping removes the class. Ships in family order, XPath first because it is additive — a worked precedent exists in the offline-sync SYNC block, one grammar alternative and eight lines of visitor. Leaves one open question rather than closing it silently: whether any other path folds or normalises whitespace without tracking quote state. The offline-sync describer did, collapsing runs inside string literals, and that defect leaves no trace — the document stays valid and the build stays green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves the conflict GitHub reports on the findings shards. They are merge=union in .gitattributes, so both appended lines are kept — but GitHub's server-side merge does not run merge drivers, which is why it shows a conflict the local merge does not have. Verified both sides survived: 84 -> mine 85, main 85, merged 86 on mdl-backend, and the same shape on mdl-executor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#418 took a merge of origin/main to resolve the findings-shard conflict GitHub reports but the union driver resolves locally. Phase 2 is stacked on it, so it needs the same base or its diff shows phase 1's changes twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(navigation): carry an offline entity config's stored properties on read
fix(workflow): refuse an unqualified decision outcome, and stop crashing on an unquoted mapping value
…create a profile
CI failed on the new doctype example: TestMxCheck_DoctypeScripts runs every
script through exec + mx check on BOTH engines, and the script creates a
PhoneOffline profile.
Execution error: failed to create navigation profile: creating navigation
profile "PhoneOffline" needs the modelsdk engine
That refusal is deliberate, not a gap. Creating a profile means writing a
fourteen-key Navigation$NavigationProfile pinned against a Studio Pro
reference, and the legacy writer has no such path — "a profile assembled from a
guess builds clean and will not open in Studio Pro" (mdl/backend/mpr/backend.go).
It is the same shape as the existing menu, rule and layout skips.
What is modelsdk-only here is the PROFILE CREATION the script needs, not the
feature under test: the SYNC block is implemented on both engines and unit
tested on each, including the CompatibilityMode carry and the property set.
Nothing local catches this — `mxcli check` needs no engine and a local exec
uses the default. Recorded as a finding so the next doctype example does not
learn it from CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The SYNC work landed with the syntax topic, the quick reference and the manage-navigation skill updated, and three user-facing places missed. docs-site/src/reference/navigation/alter-navigation.md had the SYNC clause absent from its synopsis and, worse, a stale profile list: `Responsive`, `Tablet`, `Phone`, `NativePhone` — no offline kinds at all, which predates this work. A reader of the reference page could not learn that offline profiles exist, let alone that they need a SYNC block. Adds the three offline kinds, the clause, the six modes and the caption-vs-key warning. docs-site/src/language/navigation-profiles.md had the same stale table and no offline section; it now carries both and links to the reference. CLAUDE.md gains the feature entry, including the two things a future change would otherwise have to rediscover: DownloadMode/ShouldDownload must NOT be written though gen declares them, and CompatibilityMode's carry is only distinguishable by a synthetic true case because every reference config is false. `make check-skill-mdl` passes — it runs `mxcli check` over the MDL blocks in the site docs and skills, so the new examples are verified rather than plausible: 205 blocks checked, all pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR #420 was stacked on #418 and merged into #418's branch rather than into main. GitHub retargets a stacked base only when the base branch is deleted on merge, and it was not — so phase 1 landed and all of phase 2 was stranded on a dead-end branch. This carries it forward unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`mxcli new`/`init` writes a CLAUDE.md that every context started in the
project re-reads, so its size is a per-session tax rather than a one-off.
Measured on a real 15-slice build (ako/CapTrackV4): 6,918 tokens re-read
per context, ~104k tokens across the run, none of it ever wrong and none
of it ever needed in full.
About 4,800 of those tokens were tables transcribing what mxcli answers
itself — the MDL command reference by domain, the MDL syntax summary, the
lint rule table and the skill table. That is the exact thing the project
brain's own rule forbids writing down, and for the same reason: a copy
disagrees with the tool on the next release. It already had:
- 10 of the 14 registered built-in lint rules, missing MPR008-011 —
MPR008 being precisely the rule whose advice projects argue with;
- "27 additional rules" against 31 shipped .star files;
- no layouts, rules, scheduled events, queues, menus or regular
expressions in the command tables, all shipped since it was written.
This is mendixlabs#906 repeating: the skill table drifted to 12 of 68 before anyone
noticed, and nothing failed when it did.
So the tables are replaced by the commands that own them — `mxcli syntax
<topic>` (which has a --json mode built for this consumer), `lint
--list-rules`, `help <command>`, and the skills' own frontmatter, which is
the index now. What stays is what no command can say: where the binary is,
the gates and their order, the quoting rule, the communication rules, and
read the brain first.
Three tests pin it rather than leaving it to review: a byte budget, a check
that no registered rule ID or MDL command row appears in the file, and a
check that every command it now points at is actually registered. The rule
list is extracted to builtinLintRules() so the test reads the real registry
instead of a second copy of it — a second copy being the drift it exists to
prevent.
Measured: 27,591 -> 4,175 bytes (~6,893 -> ~1,043 tokens), 85% smaller.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qdci88mRumJGXU5ifseAeq
`brain capture` is an O_APPEND write and was measured concurrency-safe: three simultaneous captures land as three well-formed lines. That measurement is right, and it is easy to read as covering more than it does. `promote` and `drop` are not appends. Both do Load -> rebuild the file in memory -> os.WriteFile it back whole, with no lock, so a capture landing between the load and the write is overwritten by a snapshot taken before it existed. Nothing reports it: the queue stays well-formed and one line is simply gone. The cost of that depends on what the queue is for. In one long session a lost capture is a note whose author can still recall it. Under one sub-agent per slice — the shape the brain's own sharding is designed for — the queue is the only channel between slices, so a capture is the agent's return value and losing one loses the decision. An orchestrator promoting slice 7's entries while slice 8's agent captures is exactly this interleaving. So the read-modify-write is serialised with an O_EXCL lock file (rather than flock, which does not mean the same thing on the Windows builds), and the rewrite goes through a temp file and a rename so a reader never sees a torn queue — Load parses a JSON object per line, so a truncated file reads as corrupt and blames capture for what an interrupted promote did. A lock older than 30s is taken over, because a crashed process must not leave a project whose agents can no longer record anything. Control, as the checklist requires: with the lock stubbed to a no-op and the atomic write left in place, TestQueueDoesNotLoseACaptureRacingAPromote fails at round 1 with "2 of 6 captures were silently lost". With the lock it passes under -race. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qdci88mRumJGXU5ifseAeq
An orchestrator dispatching one sub-agent per slice has to DECIDE on what
the brain reports — whether a slice staged anything, whether the store
still checks out — not read it. Today it cannot: every brain subcommand
prints for a human only.
The improvement note this came from assumed otherwise ("--json is already
global, so the output side is done"). It is not: globalJSONFlag is parsed
in rootCmd's PersistentPreRun but only consulted by commands that carry a
--format flag, and no brain subcommand does. `staged`, `plan` and `show`
Printf unconditionally, and `check` has --ci, which prints only the
problems and none of the states or counts.
So `staged`, `plan`, `show` and `check` now honour --json, and --json wins
over --ci where both are given: both exist for a machine, and only one of
them carries the whole report.
Two details are load-bearing rather than cosmetic:
- AnchorState is an int, so a plain marshal would emit 0/1/2 — a contract
whose meaning lives in the order of a const block, and where inserting
a state silently reassigns every existing value. The three states are
the substance of the check (only the middle one is a failure), so they
marshal as their names.
- The report carries a derived "failed", computed at marshal time rather
than stored so it cannot disagree with Failed(). Otherwise every
consumer re-implements which states are defects, and a not-indexable
anchor or an open question — neither of which is one — is exactly the
thing they would get wrong.
`staged` also reports each entry's derived shard, so a caller knows where a
promote would put it without reimplementing the routing rule, and an empty
queue is still an object with a count rather than the words "Nothing
staged" — the case a dispatcher most needs to parse.
Tested through the real root command, so the persistent flag and
PersistentPreRun are exercised as a caller gets them, with two controls:
without --json the output must NOT parse as JSON (otherwise a change
emitting JSON unconditionally would pass), and with the staged wiring
reverted the test fails on the human table.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qdci88mRumJGXU5ifseAeq
`brain staged` lists everything ever captured and offers no way to narrow
it, so an orchestrator running one sub-agent per slice cannot ask the one
question it needs — did this slice record anything — and so cannot refuse
to advance on a slice that recorded nothing.
Which filter answers that is decided by what an entry carries, and the two
obvious candidates do not:
- A slice filter matches Entry.Slice, and `capture --slice` is what MAKES
an entry a requirement. A decision found while building slice 07 carries
no slice at all, and a slice's findings are mostly decisions — so a
slice filter silently answers a narrower question than the one asked.
- Entry.Date is a day, so every capture in a session shares one value and
a boundary inside a day does not exist.
The queue is append-only, so its own order is the honest timeline: --since
<id> returns everything staged after the entry that was last there. A
dispatcher notes last_id before handing the slice off and passes it back
afterwards. --slice is added too, for the different, plan-shaped question
of what scope is queued for a slice; the help says which is which.
Three details are what make it usable rather than merely present:
- An unknown --since id is an ERROR, not an empty result. Empty is the
signal the dispatcher acts on, so a typo or a since-promoted id
producing it would abort a slice that had in fact done its job.
- last_id is reported from the unfiltered queue and even when nothing
matched, because a slice that staged nothing still has to hand the next
slice a boundary — otherwise the next one re-reports this one's captures.
- A zero count is marked "filtered", since the number alone cannot say
whether the queue is empty or the filter matched nothing, and only one
of those means the slice fell short.
--fail-if-empty exits 1 so a shell dispatcher does not have to parse for it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qdci88mRumJGXU5ifseAeq
The store is sharded so a session can load project.md plus the modules it is touching instead of the whole thing, and the README says exactly that — "what lets a session load the shards for the modules it is touching". But nothing produced that pack. docs/brain/ is a directory, so a session either read all of it or guessed, and both are wrong in the same direction. Measured on a real 15-slice project: the whole store is 7,531 tokens and the correct pack for one slice is ~2,530. That is a rounding error in one long session, where the store is read once and then cached. It is a third of the context under one sub-agent per slice, where the pack is re-read from a cold start every slice — which is the shape the sharding was designed for and the only one in which it was not actually usable. mxcli brain brief --slice 07-planning mxcli brain brief --module Sales --module Finance Which module shards a slice needs is DERIVED from that slice's requirements' anchors, not configured. Asking the caller which modules its slice touches would be asking it the thing it opened the brief to find out. Every anchor counts, not just the first: the first anchor decides where an entry is filed, but a requirement spanning two modules is worked in both, and a session that read only one of them is the case this prevents. Ordering is project, then modules, then plan — a session reads the decisions it must not contradict before the scope it is about to build. The pack goes to stdout and the size line to stderr, so `brain brief | ...` pipes the content and not the commentary; --json returns the shards separately with their paths. --slice and --module together are refused rather than merged: a brief's value is what it leaves out, and silently widening it is the whole-store read it exists to replace. `brain plan --slice <name>` is added alongside, for the same reason — it printed all 15 slices whether or not the caller cared about 14 of them. The test that matters is the negative one: a brief must NOT contain the module a slice does not touch, or another slice's plan. Without it the suite would pass against a brief that concatenated the whole store. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qdci88mRumJGXU5ifseAeq
`mxcli rename` "renames an element and automatically updates all
cross-references" — within the model. The brain's anchors are references to
the same elements and were not among them, so a refactor invalidated them
silently.
`brain check` can only report half of that, and the missing half is
inherent to the design rather than a gap in the check. A decision's anchor
points backward at something that exists, so one that stops resolving is
reported NOT FOUND. A requirement's points forward at something intended,
so one that stops resolving counts as PLANNED — which is exactly what a
forward anchor failing is supposed to mean. Once the old name is gone there
is no way to tell "never built" from "built, then renamed". The observed
symptom was a plan's progress moving from 65/65 to 63/65 with nothing else
to see.
That ambiguity is the argument for fixing it at the rename, where both
names are still known and nowhere later.
So rename now rewrites matching anchors in docs/brain/ and in the staged
queue, reports the count, and previews it under --dry-run. Four decisions
in it are load-bearing:
- The match ends at a name boundary. Anchors are dotted names, so a plain
prefix replace turns @Sales.OrderLine into @Sales.PurchaseOrderLine on
a rename of Sales.Order — a name that resolves to nothing, which
`brain check` then reports as a stale decision. The repair would invent
the exact problem it exists to prevent. (Control: with the naive
replace, the boundary test fails with both anchors rewritten.)
- A module rename moves modules/<Old>.md to modules/<New>.md. Without it
every entry in that shard anchors into New the moment the rewrite lands
and reads as misfiled — one false signal traded for another.
- Entry ids are NOT re-derived, though they are content-derived and their
content includes anchors. An id is a handle (`brain promote <id>`, prose
that cites one); invalidating every reference TO an entry in order to
fix that entry's references to the model trades one dangling pointer
for several.
- A shard with an unparseable entry block is refused rather than
rewritten, because SaveShard re-renders the file from what it parsed and
would drop the block.
A type an anchor cannot name is skipped rather than guessed at, and a
failure to update the store is a warning rather than a failed command: the
rename itself has already been applied and is what the user asked for.
Verified end to end on a real project: `@Sales.Order` and a requirement
anchoring it were rewritten, `@Sales.OrderLine` was not, ids were unchanged
and check stayed OK. The control — renaming the entity and putting the
anchors back by hand, which is the old behaviour — reproduces the reported
symptom exactly: 1 of 1 requirements built becomes 0 of 1, and the decision
half exits 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qdci88mRumJGXU5ifseAeq
A `call microflow` in a workflow's MAIN flow was auto-wired to
`with (Ctx = '$WorkflowContext') outcomes DEFAULT -> { }`; the identical
statement inside a decision's enum branch was written with neither, and
mxbuild rejected it with CE6685 + CE6686 once each. `mxcli check` passed
and `exec` reported success.
The bug is per OUTCOME KIND, not per nesting depth — the discriminator was
in the reported script all along, since the decision's `''` (void) branch
was fine and its two `'Module.Enum.Value'` branches were not.
autoBindActivitiesInFlow enumerated the flows to recurse into with a type
switch handling BooleanConditionOutcome and VoidConditionOutcome only:
EnumerationValueConditionOutcome was absent from both the decision and the
call-microflow arm, and no boundary-event body was entered at all.
deduplicateActivityNamesInFlow walks the same tree from a second,
independently maintained switch and had the boundary-event half of the
same gap.
Both walks now enumerate nested flows through one nestedFlows helper.
Condition outcomes go through the ConditionOutcome.GetFlow interface,
which already exposed the accessor the switches were standing in for, so
no outcome kind can be skipped by omission — patching the two switches
would have left the third walk to be written wrong.
Scope was 3x the report: 6 of 9 nesting sites were unwired (decision/enum,
call-microflow/enum, and boundary events on user task, call microflow,
call workflow and wait-for-notification), and only 1 was reported.
Verified: TestAutoBindReachesNestedCallMicroflow covers all 9 sites and
failed on exactly those 6 before the fix; each subtest asserts the
MAIN-flow control in the same run, because auto-bind needs a resolvable
target microflow and a fixture mistake fails identically to the bug.
Control at the mxbuild layer on Mendix 11.10.0, fresh project copy per
run: fix reverted -> the reported 4 errors; fix applied -> 0 errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…k-704080 # Conflicts: # CHANGELOG.md
A shard is re-rendered from the entries parsed out of it, so anything in
the file that is not an entry was discarded on the next promote, drop,
resolve or rename. That is right for the parts mxcli owns — the title and
preamble are regenerated so they cannot drift from the shard's identity —
but it also ate YAML frontmatter, which mxcli does not own.
Frontmatter is where every markdown tool in the ecosystem keeps its
per-file metadata: Foam and Obsidian tags, a docs site's nav weight, a
linter's per-file config. A store that eats it cannot be kept in one of
them, and it does so silently: the file stays valid, every entry is still
there, and the loss only shows up when someone next looks. That is the
failure ADR-0005's guard-don't-drop rule exists to prevent, and this writer
was on the wrong side of it.
Preservation goes in SaveShard, the single write choke point, so promote,
replace, drop and rename are all covered by one change. Four details:
- Recognition is strict, because a loose rule does not miss a block, it
swallows the document: only an opening fence on the very first line,
closed by a later line of exactly `---`. An unterminated `---` is left
alone rather than carrying the whole file forward as opaque text with
the entries re-emitted beneath it.
- The module-rename move is the one path the choke point does not cover:
it writes to a path with no existing file, so the block is read off the
OLD shard and passed explicitly. Otherwise it would be lost precisely
when the shard is moved rather than edited.
- Frontmatter counts toward the shard's cap. `brain show` measures the
file on disk, so a promote-time check that ignored it would disagree
with `show` exactly when a shard is near its limit.
- A shard nobody has annotated is byte-identical — no empty `---` block
is invented, so existing projects see no diff. That is asserted as a
control, since inventing one would otherwise let the fix "pass".
Deleting the last entry still deletes the file, frontmatter included: a
shard with no decisions is not a shard, and keeping a husk alive for its
metadata is the accumulation that branch exists to prevent.
Control: with preservation reverted to the old unconditional re-render,
TestFrontmatterSurvivesAPromote and TestFrontmatterSurvivesEveryWritePath
both fail. Verified end to end against the built binary — tags added by
hand survive a later promote, and project.md still opens with its title.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qdci88mRumJGXU5ifseAeq
feat(navigation): author offline synchronization (SYNC block)
fix(workflow): auto-wire a nested call-microflow activity (#417)
Make the project brain usable as the channel between sub-agents
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.
Seventeen commits. Most came from running mxcli against one real application (
ako/CapTrackV4) and one reference project (ako/TestApp), which is why so many of them share a shape: every gate was green and the thing was still broken.Broken in ways nothing before deploy would catch
mendixlabs/mxcli#1031,#1065). Reported success, then failed where the author could not see it. The same commit stops a crash on an unquoted mapping value.icon glyph 57562is a bare integer, so nothing resolved it:mxcli checkandmx checkboth passed, and the deploy died with "An exception occurred while exporting layout". Now MDL078 validates the code against the font — and since a glyph is not a model document,SHOW GLYPHS/DESCRIBE GLYPHwere added so the rule can name real alternatives instead of numeric ranges with holes in them.mendixlabs/mxcli#1073) —ExternalActionParameterMapping.CanBeEmptywas never set.Advice that was actively wrong
mxcli lintQUAL004 told you to delete your after-startup microflow. A microflow wired asAfterStartupMicroflowreported no callers and no references — "not called from anywhere. Remove if unused." Taking that advice left a dangling namemx checkdid not catch either; it surfaced only when the runtime refused to start. A project setting now counts as a reference.mxcli lintQUAL002 reported "no documentation" on documented pages. The modelsdk reader dropped a page's and snippet's documentation, so the catalog's Description was blank,describe pageemitted nothing, and the javadoc comment looked dropped from every angle.ACT_microflow for its own closing brace.ALLOWED_ACTIVITY_TYPESheldExclusiveSplitbut notExclusiveMerge, and anifproduces both — so guarding anything, the most ordinary thing an action microflow does, was a violation while the branch it closes was permitted.Checks that under-reported or resolved the wrong thing
check --referencesunder-reported project conflicts. Two switches with nothing comparing them: one classified 24 document types, the other knew 20. The four that fell through returned nil, read as "no conflicts".datagridagainst the deprecated Atlas Core DataGrid while MDL'sdatagridhas always written Data Grid 2 from DataWidgets. Their design properties are disjoint, so the advice was for a different widget.check --references, whileDESCRIBE ENUMERATIONreturned its values andSHOW ENUMERATIONSlisted it.alter page … set PageSizefailed on a DataGrid 2 thatcreate page (PageSize: 20)had just written: the property was resolved by casing rather than by name.Idempotence
DROP USER ROLE/DROP DEMO USER … IF EXISTS. Neither had an idempotent form, so a one-time cleanup of whatmxcli newships either broke every later run of its slice script or had to be commented out — which is what a real project did.Offline synchronization
Phase 1 of a proposal pinned against a real document: an offline entity config's stored properties are now carried on read.
CompatibilityModewas read and discarded, so any future write path would have dropped it silently — the document stays valid andmx checkreports 0 errors either way.Measured on
ako/TestApp, whoseTabletOfflineprofile configures seven entities across all six sync modes: Studio Pro writes four properties, not the sixmodelsdk/gendeclares.DownloadModeandShouldDownloadoccur zero times, so the risk is inverted from the expected one — a property Studio Pro fills in on load is one whose emission makes a document Studio Pro cannot open.Also fixed:
describe navigationemitted a broken comment, because a real constraint is multi-line and a newline ends a--comment.Proposals
ako/TestApp— all four of its open questions closed by the document, two answers not what the draft assumed.mendixlabs/mxcli#750. It corrects the framing: expressions are not stored as strings — the visitor already parses them into typed AST nodes — and the reason the issue stalled is that there are two expression families, XPath and Mendix expression, which should not be unified behind one delimiter