Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3e4009c
feat(mdl): create … if not exists on every document type (#731)
ako Sep 28, 2026
fd55a9a
mdl: menus, property maps and the database connection take R2's brack…
ako Sep 28, 2026
243d8a5
describe: print menus, property maps and database connections in R2's…
ako Sep 28, 2026
cc60727
docs: menus, property maps and database connections in their R2 forms…
ako Sep 28, 2026
81726f7
fix(diff): a guarded create of an existing element diffs as unchanged…
ako Sep 28, 2026
eed9a32
mdl: the R2 property lists refuse a key written twice and a query wit…
ako Sep 28, 2026
f458526
test: gofmt the R2 rewrite table (#754)
ako Sep 28, 2026
250ba2f
mdl: alter workflow on the generic alter { set / insert / replace / d…
ako Sep 28, 2026
edf4feb
backend: workflow target resolver and insert before on both backends …
ako Sep 28, 2026
544e2d8
docs: canonical alter workflow in skills, syntax, quick references (#…
ako Sep 28, 2026
935b828
fix(executor): guard the shared module cache during parallel describe…
ako Sep 28, 2026
ab867d8
executor: refuse `insert before` a workflow's start activity (#712)
ako Sep 28, 2026
1f53687
docs(skills): canonical alter workflow spellings in write-workflows g…
ako Sep 28, 2026
458e474
fix(describe): write the anchor of the flow leaving an if's merge (#767)
ako Sep 28, 2026
b68a33c
fix(describe): round-trip user role properties, workflow notes, REST …
ako Sep 28, 2026
958e51c
docs: user role property list, workflow notes, REST header templates,…
ako Sep 28, 2026
3fc358b
Merge branch 'c7-787' into c7-789
ako Sep 28, 2026
a82b4db
Merge branch 'c7-789' into c7-788
ako Sep 28, 2026
d692116
Merge branch 'c7-788' into c7-784
ako Sep 28, 2026
11a9f2a
Merge branch 'c7-784' into c7-786
ako Sep 28, 2026
cadc37b
Merge branch 'c7-786' into c7-783
ako Sep 28, 2026
b69e832
test(executor): round-trip expectations follow the R2 describe of dat…
ako Sep 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .claude/skills/fix-issue/findings/mdl-executor.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -738,3 +738,6 @@
{"area": "mdl/executor", "date": "2026-09-27", "symptom": "describe microflow printed `validation feedback $O/A message '{1} \u2026';` with no template arguments; describe -> exec stored the feedback with none", "cause": "formatAction's ValidationFeedbackAction case never read TemplateParameters, although the reader (textTemplateFromGen) and the writer both carried them", "file": "mdl/executor/cmd_microflows_format_action.go (templateArgsClause)", "insight": "A text template has two halves, the text and its parameters; check every describe case that prints one prints both. Found while switching the template form to `with ({1} = \u2026)` (ako/mxcli#751)."}
{"area": "mdl/executor", "date": "2026-09-28", "symptom": "create or modify workflow of its own describe output renames Studio Pro's implicit activities: start1 -> Start, end1 -> End, an event sub-process end2 -> End2", "cause": "describe prints no name for the implicit start/end, branch ends, jumps or end-of-path markers, and the rebuild names them from captions", "file": "mdl/executor/workflow_carry_names.go", "fix": "Pair declared and stored flows structurally (named activities by name, unnamed ones by type ordinal, nested flows by position, event sub-processes by name) and carry the stored names before deduplication", "insight": "Carry before the CE0495 deduplication, not after, so the carried names take part in it; pair nested flows with nil slots kept, because the rebuild leaves an empty outcome's flow nil where the stored one is an empty Flow", "refs": ["ako/mxcli#743"], "test": "TestCreateOrModifyWorkflow_CarriesNamesDescribeCannotPrint"}
{"area": "mdl/executor", "date": "2026-09-28", "symptom": "create or modify external entity declaring OrderId: String(20) on an Edm.Int64 property keeps RemoteType Edm.Int64; mx check CE6616", "cause": "carryStoredAttributeState carries RemoteType by name regardless of the declared type", "file": "mdl/executor/external_entity_remote_type.go", "fix": "Refuse under mdl 1 (MDL-V1-REMOTETYPE), warn under mdl 0", "insight": "CE6616 compares against the service's $metadata, not the stored RemoteType, so deriving RemoteType from the declared type would not fix it; measured with mx check on a TestApp copy", "refs": ["ako/mxcli#764"], "test": "TestCreateOrModifyExternalEntity_RemoteTypeMismatchRefusedUnderMdl1"}
{"area": "mdl/executor", "date": "2026-09-28", "symptom": "describe output that lost data on replay, the part of ako/mxcli#707 that #728 left open: a user role's Description and CheckSecurity were printed as `--` comments and a user role with no module roles described as `create user role X;`, which did not parse; workflow activity, event sub-process and workflow-level annotations were `-- annotation:` comments; a consumed REST header `'Bearer ' + $Token` stored only `Bearer ` and described back as that literal; image-collection describe wrote /tmp/mxcli-preview files and printed their paths.", "cause": "Each describer printed what its grammar had no slot for as a comment: the positional user-role form had no key for the description, check-security or manageable roles; workflows had only the standalone `annotation` statement (refused by MDL-WF04) and no attached form; the REST header grammar took an expression but the writer stores a value template, so the variable was dropped; image bytes live in the model, but describe exported them to a side file rather than into the statement.", "file": "mdl/grammar/domains/MDLSecurity.g4, MDLWorkflow.g4, MDLService.g4, MDLDomainModel.g4; mdl/visitor/visitor_security.go, visitor_workflow.go, visitor_rest.go, visitor_imagecollection.go; mdl/executor/cmd_security.go, cmd_security_write.go, cmd_workflows.go, cmd_workflows_write.go, cmd_rest_clients.go, cmd_imagecollections.go; mdl/backend (SetUserRoleProperties)", "fix": "User role properties are a ( Key: value ) list (ModuleRoles, Description, ManageAllRoles, ManageableRoles, ManageUsersWithoutRoles, CheckSecurity), the list optional; the positional form is MDL-DEPR710 with an fmt --upgrade rewrite. Workflow notes are `@annotation '…'` before an activity or event sub-process and a header `annotation '…'` clause. A REST header value is the template `'Bearer {Token}'`; the expression form is MDL-DEPR711 and now builds that template. Image describe writes `Data: '<base64>'` (Format only when the bytes do not show it), the TUI preview decodes it, and a File: path resolves script-relative.", "insight": "A `--` comment in describe output is a data-loss marker, not a rendering choice: every one of these was a property the grammar could not state. Grep describers for `-- ` emits of model values before assuming a describer round-trips. For the REST header the evidence that settled the template syntax was Studio Pro's own documentation (via the MCP search_mendix_knowledge_base tool): parameters are `{name}` in the path, query, header value and body.", "test": "mdl/executor/issue707_remaining_test.go, mdl/upgrade/issue707_upgrade_test.go, cmd/mxcli/tui/image_render_test.go"}
{"area": "mdl/executor", "date": "2026-09-28", "symptom": "re-executing the full describe of a flow whose if-merge wraps onto a new row (CxLayout.CX_VAL_Factory, CX_VAL_EmailTemplate in 02c) draws the merge-to-split flow out of the merge's right side instead of its bottom; layout flows, ELK, diff, with handles and create or modify's diff inherit it", "cause": "the flow leaving an if's closing merge had no annotation slot: describe wrote only the next split's to: top, and the top-level builder dropped an if's own @anchor(from:) (it honoured it only inside branch bodies)", "file": "mdl/executor/cmd_microflows_show_helpers.go", "fix": "describe writes the merge exit side as from: on the if's @anchor (keyed on the merge the @merge line already names); the builder hands the if's from: to the merge-out flow via nextFlowAnchor, and case/type-split branches clear a nested if's leftover nextFlowAnchor", "insight": "the canonical describe hid it: omitting the split's @position let the rebuild wrap the row, and wrapping re-derives bottom->top. Only the full description, where every node is pinned, exposed that a merge's outgoing anchor was unwritable; compare full describe -> re-exec geometry against the stored flow, not canonical", "refs": ["ako/mxcli#767"], "test": "TestDescribe_MergeExitAnchorRoundTrips"}
{"area": "mdl/executor", "date": "2026-09-28", "symptom": "`refresh catalog full source` under `go test -race` reports two DATA RACEs in getModulesFromCache/findModule (helpers.go), reached from describeEntity in the catalog's parallel describe workers", "cause": "executorCache.modules is filled lazily with no synchronisation; the catalog runs captureDescribeParallel from a goroutine pool sharing one ExecContext.Cache, and preWarmCache warmed the hierarchy and name maps but not the module list, so several workers each listed the modules and published the slice while others read the field", "file": "mdl/executor/helpers.go (getModulesFromCache, executorCache.cachedModules), mdl/executor/executor.go (modulesMu), mdl/executor/cmd_pages_builder.go (getModules)", "fix": "Fill and invalidate the module list under executorCache.modulesMu; the page builder's getModules goes through the same cachedModules", "insight": "The race detector only reports a location pair once per process, so a -race test run with -count>1 passes on later iterations even against the unfixed code; pair it with a detector-free assertion (ListModules called once by N concurrent lookups on a slow mock). Driving the whole `refresh catalog full source` under -race also surfaces a separate race inside the ANTLR Go runtime's shared DFA (page/workflow describes parse expressions concurrently), so the regression test drives entity describes only", "refs": ["ako/mxcli#765"], "test": "TestModuleCache_ConcurrentFillListsOnce, TestParallelEntityDescribes_NoDataRace"}
1 change: 1 addition & 0 deletions .claude/skills/fix-issue/findings/mdl-visitor.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@
{"area": "mdl/visitor", "date": "2026-09-25", "symptom": "A page action's microflow argument `Flag: true and false` was stored as the expression \"trueandfalse\", `Mode: if true then 'a' else 'b'` as \"iftruethen'a'else'b'\", and a REST call parameter `$OrderId = if $x then $id else 'none'` as \"if$xthen$idelse'none'\" (measured by decoding the units on a copy of ako/TestApp). `check -p --references` passed; describe printed the fused text back", "cause": "Four sites stored an expression as text via ANTLR's ctx.GetText(), which concatenates tokens without the hidden-channel whitespace: microflowArgV3 values (page/nanoflow call arguments), contentparams values, send-rest-request WITH parameters, and a dynamic `execute database query`. Literals, `+` and a lone $currentObject are single tokens or need no spacing, so every common case looked correct", "file": "`mdl/visitor/visitor_helpers.go` (`expressionSourceText`), `mdl/visitor/visitor_page_v3.go` (`buildMicroflowArgV3`, `buildParamAssignmentV3`), `mdl/visitor/visitor_microflow_actions.go` (dynamic query, send rest params)", "insight": "**GetText() on an expression context is always a bug** — grep `Expression().*GetText()` / `expr.GetText()` in mdl/visitor; each hit either builds the AST or must use expressionSourceText (whitespace kept, MDL comments stripped). The earlier comment-leak fix moved the six microflow sites to extractExpressionText and missed these four because they lived in page/REST code, not microflow statements: fix a text-extraction defect by searching for the call, not the feature. The tell that hid it: tests used `$currentObject` and `'a' + 'b'`, both immune; a probe with `and` / `if…then` exposed all four at once. Found while writing PROPOSAL_first_class_expressions.md §5.1. Tests `visitor_expression_source_text_test.go`", "refs": ["mendixlabs/mxcli#750"]}
{"area": "mdl/visitor", "date": "2026-09-26", "refs": ["ako/mxcli#706"], "rules": ["MDL-IDX01", "MDL-ENUMDOC01"], "symptom": "Five forms parsed and `check` printed \"Syntax OK\", then the model lost or changed them: `throw 'x';` vanished from the microflow; `Amount: float` / `currency` became String(unlimited) (Void as a microflow type) and `date` became DateTime; `create association X (from A to B, type: referenceset, storage: table)` was stored as a Reference with column storage; a `/** … */` on an enumeration value and an index name (`index Idx on (…)`, `create index Idx on E (…)`) were read and written nowhere", "cause": "Three different gaps behind one symptom: throwStatement had a grammar rule and no listener (a whole statement); buildDataType/buildMicroflowDataType end in a default return (String / Void) that the FLOAT_TYPE and CURRENCY_TYPE tokens fell into, and DATE_TYPE mapped to an ast.TypeDate the writers alias to DateTime; ExitCreateAssociationStatement read only ctx.AssociationOptions(), while the parenthesised alternative puts its options directly under the statement as `(COMMA associationOption)*`. Enumeration-value docs and index names have no home in the metamodel (EnumerationValue and Index have no such property)", "file": "`mdl/visitor/visitor_silent_drops.go` (ExitThrowStatement, EnterDataType, EnterNonListDataType, rejectParenthesisedAssociation), `mdl/executor/validate_unstored_text.go` (MDL-IDX01, MDL-ENUMDOC01); tests `mdl/visitor/silent_drops_706_test.go`, `mdl/executor/validate_unstored_text_test.go`", "insight": "**Split by what is lost: refuse when the MODEL would be wrong, warn when only WORDS are lost.** throw/types/association write a different model, so they are visitor errors with the working spelling in the message; doc comments on values and index names leave a correct model, and were used in 56 places in our own examples, so they are check warnings. **Refuse from the parse tree, not by deleting the grammar alternative**: `float`, `currency`, `date` and `throw` are also keywords-as-identifiers, so removing `| FLOAT_TYPE` from dataType would let `Amount: float` re-parse as an enumeration reference named float — the same silence again. A listener on EnterDataType covers every place a type is written (attribute, parameter, return, declare, constant, service rules) in one method; checking only the direct child token keeps `M.Currency` and `enumeration(M.Float)` legal (the control). To find these: any `if … ; return ast.DataType{Kind: …}` fall-through default in a builder is a silent-substitution site, and a grammar alternative whose sub-rules differ from its sibling's (`associationOption` vs `associationOptions`) needs its own read. Sweep: run old vs new `mxcli check` over `git ls-files '*.mdl'` and diff exit codes — it found every example that relied on the dropped forms (5 files)"}
{"date": "2026-09-27", "area": "mdl/visitor", "symptom": "A member path after `*`, `div`, `mod` (`$a/X * $b/Y`) is stored as division: `describe` shows `$a/X * $b / Y`, and inside a list `filter` `$b / $currentObject/Y`. `mxcli check`, `mx check` and the build are all green; Mendix evaluates a different expression.", "cause": "`/` shares a precedence level with `*`, `div`, `mod` in `multiplicativeExpression` (MDLSettings.g4), so the left-associative chain parses `$a/X * $b/Y` as `(($a/X) * $b) / Y`. `buildMultiplicativeExpression` only turned `/ Member` into an AttributePathExpr when the WHOLE left side was a variable or path; here `$b` was already the right operand of the product, so the `/` stayed a BinaryExpr. The serializer then printed a spaced `/` and the filter's `qualifyIteratorAttributes` read the stranded `Y` as a bare attribute.", "file": "mdl/visitor/visitor_microflow_expression.go (attachPathToLastOperand)", "fix": "When a `/ Member` step cannot attach to the whole left side, attach it to the last operand of the preceding multiplicative BinaryExpr (`*`, `div`, `mod`, `%`, `:`): `(L op $b) / Y` becomes `L op ($b/Y)`. Mendix has no `/` division, so `/` followed by a member name is always navigation. Division by a parenthesised expression is left alone (control test).", "insight": "Fixing it where the AST is BUILT fixes every consumer at once (serializer, iterator qualification, MDL045's slash-division check), where the earlier #52 fix only taught MDL045 to tolerate the mis-nested tree. Test: `TestMemberPathAfterMultiplicativeOperatorIsStoredAsWritten` (flow builder, stored strings for sum, filter, declare, div chain) plus a control `TestSlashAfterNonPathIsNotMadeAMemberPath`. Revert check: stubbing the fix brings back all four wrong strings. A unary-minus branch was dropped because removing it failed nothing (`2 * -$b/Y` was already stored correctly).", "refs": ["#768", "#52"], "rules": []}
{"date": "2026-09-28", "area": "mdl/visitor", "symptom": "`create view entity if not exists M.V (...) as (...)` on an existing view entity fails with \"entity already exists\" although `mxcli syntax` documents `CREATE VIEW ENTITY [IF NOT EXISTS]`; every other create kind rejected `if not exists` as a parse error.", "cause": "The grammar accepted `ifNotExists?` on all five entity alternatives, but only `ExitCreateEntityStatement`'s non-view branch read it; `buildViewEntity` built a `CreateViewEntityStmt`, which had no field for it, so the guard parsed and was dropped. A per-builder guard is the one the next builder forgets (same class as #531 for drop if exists).", "file": "mdl/visitor/visitor_create_guard.go (applyCreateGuard), mdl/executor/cmd_create_guard.go (skipExistingCreate)", "fix": "`ast.CreateGuard` embedded in every named create statement; the visitor applies `if not exists` once in ExitCreateStatement to whichever statement the create rule built (and errors if that type cannot carry it), and Registry.Dispatch probes existence and skips before the handler. Entity and association keep their in-handler check.", "insight": "Tests are driven by the grammar's own list of create kinds (createStatementKinds) plus allKnownStatements, so a new create rule fails until it carries the guard or is exempted with a reason. The executor test swaps the handler for a recorder, which isolates the guard from what each handler needs from a mock; revert check: stubbing skipExistingCreate fails all 38 existing-element cases, and the absent-element and unguarded controls keep it honest.", "refs": ["#731"], "rules": ["MDL067"]}
2 changes: 1 addition & 1 deletion .claude/skills/mendix/alter-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ everywhere. Removing one has its own form:
alter page Pages.Vehicle_Overview {
insert into vehicleListView {
template for Pages.Motorcycle {
dynamictext mcLabel (content: 'Motorcycle {1}', contentparams: [{1} = Brand])
dynamictext mcLabel (content: 'Motorcycle {1}', contentparams: ({1} = Brand))
}
};
drop template for Pages.SUV in vehicleListView
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/mendix/atlas-design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ mxcli -p app.mpr -c "describe building block Atlas_Web_Content.Card"
```
```
{
container container2 (DesignProperties: ['Card style': on]) {
container container2 (DesignProperties: ('Card style': on)) {
dynamictext text22 (Content: 'Card title', RenderMode: H4, Class: 'card-title',
DesignProperties: ['Spacing': ['margin-bottom': 'L']])
DesignProperties: ('Spacing': ('margin-bottom': 'L')))
}
}
```
Expand Down
Loading
Loading