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
5 changes: 5 additions & 0 deletions .claude/skills/fix-issue/findings/mdl-executor.jsonl

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .claude/skills/fix-issue/findings/mdl-other.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,4 @@
{"area": "mdl/catalog", "date": "2026-09-25", "symptom": "Two import mappings with the same name, one excluded and one live (valid: mx check 0 errors), show up in CATALOG.IMPORT_MAPPINGS and CATALOG.SOURCE as two indistinguishable rows — integer Ids 2 and 3, no Excluded column, and both source rows the same `create or modify import mapping …` text with no `@excluded` prefix (mendixlabs/mxcli#1185)", "cause": "Three gaps: describeImportMapping/describeExportMapping never printed @excluded; import_mappings_data/export_mappings_data used an AUTOINCREMENT Id and recorded no Excluded; and buildSource enumerates DOCUMENTS but called the describe callback with only a qualified name, so every describer resolved by name and rendered the preferred twin for both rows. The last one was not mapping-specific — microflow/nanoflow/rule/page twins had identical source rows too, masked because the microflows table itself carries Id + Excluded", "file": "`mdl/catalog/builder_source.go` (`sourceItem.id`, `ElementId`), `mdl/catalog/builder.go` (`DescribeFunc` gains id), `mdl/catalog/tables.go` + `builder_modules.go` (mapping Id/Excluded, schema 14), `mdl/executor/excluded_docs.go` (`pickDescribed`, `describedMapping`), `mdl/executor/cmd_catalog.go` (`ExecContext.describeID`), `mdl/executor/cmd_{import,export}_mappings.go`, `mdl/visitor/visitor_import_export_mapping.go`, `mdl/backend/modelsdk/mapping_write.go` (by-name lookup prefers the live twin)", "insight": "**A collector that walks documents must not hand a NAME to a callback that resolves names** — #914's pickLive is correct for an interactive DESCRIBE and exactly wrong for a per-document sweep, because it maps both twins onto one. Pin by ID (ExecContext.describeID) and fall back to pickLive. **Printing a new annotation in DESCRIBE obliges the write side**: `@excluded` on a mapping was MDL059 until the visitor read it and documentAnnotations listed it; skipping that turns describe->exec into a refusal. **Check CatalogSchemaVersion against its own history comment**: the history recorded 13 (entity event handlers) while the constant said \"12\" — parallel bumps, the merge kept the lower — so caches built at 12 never rebuilt; bumping to 14 carries both. **Making a twin for a real run**: MDL cannot (CREATE refuses a taken name, no RENAME for mappings); `@excluded create` under a second name, then rewrite that unit's BSON Name (bson.D round-trip of the .mxunit) — mx check stays 0 errors, which is the reporter's state. Pre-fix binary on that project: two rows Id 2/3 and identical source text; fixed: document Ids, Excluded 0/1, excluded row starts `@excluded`. Controls: disable the describeID pin (4 executor tests fail, microflow included), drop the @excluded print, pass \"\" as id in buildSource (1 source row, not 2), stash tables.go/builder_modules.go (no such column: Excluded)", "refs": ["mendixlabs/mxcli#1185", "#914"]}
{"area": "mdl/linter", "date": "2026-09-25", "symptom": "No way to ask a structural question about ONE document: `lint` scopes by module and by rule but not by document, so validating a microflow after each `exec` meant a project-wide lint (~13 s measured by the reporter) plus a baseline diff to see which findings were new. At that price the gate gets batched to once per session \u2014 three CONV011 violations shipped under a clean mxbuild log, six across three microflows before anyone looked.", "cause": "Feature gap, but the shape of the fix is not obvious: every rule guards its expensive per-document read (`FullMicroflow`) with `IsExcluded(moduleName)`, so module scoping already skipped the costly part for other modules \u2014 the missing granularity was WITHIN a module.", "file": "`mdl/linter/context.go` (`SetIncludedDocuments`, `IsDocumentExcluded`, `documentFilterSQL`; `Microflows`/`Pages`/`Widgets` narrowed in SQL; new `includeActive` flag), `cmd/mxcli/cmd_lint.go` + `main.go` (`-d/--documents`), `cmd/mxcli/lint_document_filter.go` (violation post-filter), tests `mdl/linter/context_document_filter_test.go`", "insight": "**Scope in the ITERATOR, not in each rule** \u2014 one SQL predicate on `Microflows()` covers CONV011, MPR002, CONV010, QUAL003 and every other rule that walks it, with no rule edited and no second copy of the filter to drift. **Two traps, both found by tests I wrote and then tried to break.** (1) An empty inclusion map means 'no filter' to `IsExcluded`, so intersecting `--modules A` with `--documents B.C` \u2014 an empty set \u2014 made lint scan the WHOLE project instead of nothing; distinguishing 'no allowlist' from 'allowlist that matched nothing' needs an explicit bool, not `len(map) > 0`. (2) The narrowing test PASSED against code with the SQL filter reverted, because the shared fixture has one microflow per module and the module implication alone explained the result \u2014 a sibling document in the SAME module is the only fixture that isolates document narrowing from module narrowing. Prove-by-revert is what caught both; the second would otherwise have shipped a test that could never fail. Also: a rule that reports from project settings rather than a document iterator is untouched by SQL narrowing, so a scoped run needs a violation post-filter too \u2014 and it must accept BOTH spellings of Location.DocumentName, since CONV010 sets the qualified name and MPR011 the short one.", "refs": ["ako/mxcli#681", "mendixlabs/mxcli#1186"]}
{"area": "mdl/catalog", "date": "2026-09-25", "symptom": "java_actions.ReturnType showed a type-parameter return as the type parameter's own name: 'TypeParameter', 'TypeParEntity', 'FileTypeDocument' depending on what the modeler called it, and a type parameter named `String` (Studio Pro allows it) read back as 'String', identical to the primitive. java_action_parameters.ParameterType had the same ambiguity for both the object parameter and the entity-type selector.", "cause": "The catalog stored TypeString(), which is DESCRIBE's MDL rendering: a bare type-parameter reference IS its name in MDL syntax, so the value carried no marker that it was a type parameter at all.", "file": "mdl/catalog/builder_modules.go (catalogCodeActionType)", "insight": "The report reads like three inconsistent conventions ('TypeParameter' / 'TypeParEntity' / a name) but it is one: every value was the modeler's chosen name, and 'TypeParameter' is merely Studio Pro's default. Fix the encoding in the catalog only (`TypeParameter:<name>`, `EntityTypeParameter:<name>`, the `Kind:Name` shape microflows_data already uses; no primitive contains a colon) \u2014 changing TypeString() would change DESCRIBE output, where the bare name is the syntax. Test with the name colliding with a primitive and a primitive action as control: a unit test on the builder with MockBackend.ListJavaActionsFullFunc, plus an end-to-end catalog query on testdata/expr-checker (copy the whole dir; the .mpr alone is v2 without mprcontents/). MDL itself cannot declare a type parameter named String (`entity <String>` is a parse error), so the colliding case is only reachable via Studio Pro-authored models.", "refs": ["mendixlabs/mxcli#1183"]}
{"area": "mdl/catalog", "date": "2026-09-26", "symptom": "`impact Module.Entity.Attr` answered \"(no impact - element is not referenced)\" for an attribute a change activity sets and a page displays (Evora Factory Management: DigitalTwin.Machine.NumberOfIncidents); `impact` on an enumeration said the same; `callers` of a workflow started by a microflow said \"(no callers found)\". An agent auditing the app read these as safe-to-delete.", "cause": "The refs graph stopped at documents: no ATTRIBUTE / ENUMERATION / ENUMERATION_VALUE targets at all, and no edge for WorkflowCallAction, for a page navigating an association, or for an import/export mapping mapping an entity. Page/snippet XPath constraints also had no TargetEntity, because resolveEntityRefFromBSON read EntityRef.QualifiedName, a key no stored DirectEntityRef carries (it is `Entity`; an IndirectEntityRef ends on its last step's DestinationEntity).", "file": "mdl/catalog/builder_member_refs.go (memberRefsInUnit, scanPaths, extractXPathRefs, extractEnumerationTypeRefs), builder_references.go (microflowActionRef WorkflowCallAction), builder_xpath.go (resolveEntityRefFromBSON), catalogdb.go (CatalogTx.Query)", "insight": "Member references are found by a RAW-document walk matching every string value against the names the model declares (whole-string = structured ref: MemberChange.Attribute, AttributeRef.Attribute, EntityRefStep.Association, EnumerationType.Enumeration, ObjectMappingElement.Entity; path tokens inside expressions = association paths and qualified enum values). A typed walk would reach only the sites someone wrote a case for; the raw walk reached 4043 attribute bindings on Evora with no per-type code, and exact-set matching means prose cannot produce an edge (Documentation is skipped anyway; the test's control is an unused attribute that must stay unreferenced). XPath is resolved separately because its context entity IS known: bare names resolve against the target entity and its generalizations, predicates after a path switch context, and an enum attribute compared to a literal names the value. What stays invisible is a bare member through a variable in an expression ($Order/Total) \u2014 so the executor must not say 'not referenced' (see the executor finding). New member kinds deliberately stay OUT of graphRefKinds and off graph_god_nodes' asset side: attributes are members, not assets, and reusing change/create/retrieve for them would have pulled every attribute into communities/centrality. Bump CatalogSchemaVersion for any new edge (refs are only written by REFRESH CATALOG FULL). Verified the lint output on Evora is byte-identical in counts before/after, so no rule changed verdicts silently."}
8 changes: 4 additions & 4 deletions .claude/skills/mendix/write-lint-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,13 +515,13 @@ Returned by `permissions()` (all types) or `permissions_for()` (entity-specific)
### reference
| Property | Type | Example |
|----------|------|---------|
| `source_type` | string | The document the edge comes FROM, upper-case: `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"SNIPPET"`, `"ENTITY"`, `"ASSOCIATION"`, `"WORKFLOW"`, `"NAVIGATION"`, `"SCHEDULED_EVENT"`, `"PUBLISHED_REST_OPERATION"`, `"PROJECT_SETTINGS"` |
| `source_type` | string | The document the edge comes FROM, upper-case: `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"SNIPPET"`, `"ENTITY"`, `"ASSOCIATION"`, `"WORKFLOW"`, `"NAVIGATION"`, `"SCHEDULED_EVENT"`, `"PUBLISHED_REST_OPERATION"`, `"PROJECT_SETTINGS"`, `"IMPORT_MAPPING"`, `"EXPORT_MAPPING"` |
| `source_id` | string | Source UUID |
| `source_name` | string | `"Sales.ACT_Customer_Create"` |
| `target_type` | string | What it points AT, upper-case: `"ENTITY"`, `"ASSOCIATION"`, `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"LAYOUT"`, `"WORKFLOW"`, `"WIDGET"`, `"JAVA_ACTION"`, `"REST_OPERATION"`, `"REGULAR_EXPRESSION"`. `LAYOUT` and `WIDGET` are only ever targets; `SCHEDULED_EVENT` and `PROJECT_SETTINGS` only ever sources |
| `target_type` | string | What it points AT, upper-case: `"ENTITY"`, `"ASSOCIATION"`, `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"LAYOUT"`, `"WORKFLOW"`, `"WIDGET"`, `"JAVA_ACTION"`, `"REST_OPERATION"`, `"REGULAR_EXPRESSION"`, `"ATTRIBUTE"`, `"ENUMERATION"`, `"ENUMERATION_VALUE"`. `LAYOUT`, `WIDGET`, `ATTRIBUTE`, `ENUMERATION` and `ENUMERATION_VALUE` are only ever targets; `SCHEDULED_EVENT` and `PROJECT_SETTINGS` only ever sources |
| `target_id` | string | Target UUID |
| `target_name` | string | `"Sales.Customer"` |
| `ref_kind` | string | How it references: `"call"`, `"create"`, `"retrieve"`, `"change"`, `"delete"`, `"show_page"`, `"datasource"`, `"action"`, `"layout"`, `"parameter"`, `"return"`, `"generalize"`, `"associate"`, `"home_page"`, `"login_page"`, `"menu_item"`, `"calculate"`, `"schedule"`, `"validate"`, `"settings"`, `"widget"`, `"sync"`, `"publish"`, `"event"` — lower-case, unlike the types above |
| `target_name` | string | `"Sales.Customer"`; three-part for an attribute or an enumeration value: `"Sales.Order.Total"`, `"Sales.OrderStatus.Open"` |
| `ref_kind` | string | How it references: `"call"`, `"create"`, `"retrieve"`, `"change"`, `"delete"`, `"show_page"`, `"datasource"`, `"action"`, `"layout"`, `"parameter"`, `"return"`, `"generalize"`, `"associate"`, `"home_page"`, `"login_page"`, `"menu_item"`, `"calculate"`, `"schedule"`, `"validate"`, `"settings"`, `"widget"`, `"sync"`, `"publish"`, `"event"`, `"member"` (binds/reads/writes an attribute or navigates an association), `"xpath"` (an XPath constraint names it), `"type"` (typed as an enumeration), `"value"` (an expression names an enumeration value), `"mapping"` (an import/export mapping maps the entity) — lower-case, unlike the types above. Attribute names used only through a variable in a free-text expression (`$Order/Total`) have no edge |
| `module_name` | string | Source module |

### project_security
Expand Down
13 changes: 12 additions & 1 deletion cmd/mxcli/cmd_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,14 @@ Examples:
var refsCmd = &cobra.Command{
Use: "refs <qualified-name>",
Short: "Find references to an element",
Long: `Find all references to the specified element (entity, microflow, page, etc.).
Long: `Find all references to the specified element (entity, microflow, page, etc.,
or an attribute Module.Entity.Attribute, an enumeration, or an enumeration
value Module.Enum.Value). Each (source, kind) is listed once.

Examples:
mxcli refs -p app.mpr Module.Customer
mxcli refs -p app.mpr Module.OrderPage
mxcli refs -p app.mpr Module.Customer.Email
`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
Expand All @@ -98,10 +101,15 @@ var impactCmd = &cobra.Command{
Use: "impact <qualified-name>",
Short: "Show impact of changing an element",
Long: `Analyze the impact of changing an element by showing all elements that reference it.
The summary counts distinct elements. For an enumeration, the uses of its values are
included. When nothing is found for an attribute or an enumeration value, the message
says which usage sites were checked: one named only through a variable in a free-text
expression ($Order/Total) is not resolved, so run 'mxcli search' before deleting.

Examples:
mxcli impact -p app.mpr Module.Customer
mxcli impact -p app.mpr Module.OrderStatus
mxcli impact -p app.mpr Module.Customer.Email
`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -212,6 +220,9 @@ var searchCmd = &cobra.Command{

Searches across string literals (captions, labels, messages) and MDL source
definitions. Requires at least a FULL catalog build (done automatically).
MDL source is searched only once the source index exists — build it with
mxcli -p app.mpr -c "refresh catalog full source"
Until then a warning on stderr says only string literals were searched.

Output Formats:
table - Human-readable table (default)
Expand Down
26 changes: 25 additions & 1 deletion docs-site/src/reference/catalog/show-references-impact.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,28 @@ These commands provide different views of cross-reference information for a give

**SHOW CONTEXT OF** assembles the surrounding context of an element -- its definition, its callers, callees, and related elements -- suitable for providing to an LLM or for understanding an element in its broader project context. The optional `DEPTH` parameter controls how many levels of related elements to include.

### Attributes, enumerations and enumeration values

The target may also be an attribute (`Module.Entity.Attribute`), an enumeration, or an enumeration value (`Module.Enum.Value`). The graph records:

| Kind | Meaning |
|------|---------|
| `member` | a microflow, nanoflow, rule, page, snippet, workflow or import/export mapping binds, reads or writes the attribute, or navigates the association |
| `xpath` | an XPath constraint names the attribute or association, or compares an enumeration attribute with the value |
| `type` | an attribute, parameter or variable is typed as the enumeration |
| `value` | an expression names the enumeration value |
| `mapping` | an import or export mapping maps the entity |

`IMPACT OF` an enumeration includes the uses of each of its values, with a `Target` column naming which one.

Results list each (source, kind) once, and the `IMPACT` summary counts distinct elements.

When nothing is found, the message says what was searched. An attribute named only through a variable in a free-text expression (`$Order/Total`), and an enumeration value used only as a decision branch, are not resolved by the catalog, so an empty result for an attribute or a value tells you to run `SEARCH` before treating it as unused.

## Parameters

**qualified_name**
: The fully qualified name of the element to analyze (e.g., `Module.EntityName`, `Module.MicroflowName`).
: The fully qualified name of the element to analyze (e.g., `Module.EntityName`, `Module.MicroflowName`, `Module.Entity.Attribute`, `Module.Enum.Value`).

**n** (CONTEXT only)
: The number of levels of related elements to include. Defaults to 1 if not specified. Higher values include more surrounding context but produce more output.
Expand All @@ -41,6 +59,12 @@ SHOW REFERENCES TO Sales.Customer;
SHOW IMPACT OF Sales.Customer;
```

### Check an attribute before dropping it

```sql
SHOW IMPACT OF Sales.Order.DiscountCode;
```

### Gather context for a microflow

```sql
Expand Down
49 changes: 49 additions & 0 deletions mdl-examples/bug-tests/describe-expression-trailing-newline.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- describe: a stored expression's trailing newline put `)` / `;` on its own line
--
-- Studio Pro stores an expression exactly as typed, and a newline left at the
-- end of the expression editor is common (Evora Factory Management: every
-- member of one create, dozens of change/call arguments). `describe microflow`
-- interpolated the stored text verbatim:
--
-- change $Ticket (TicketState = BugTestNewline.TicketStatus.Open
-- );
--
-- The statements below store exactly that shape: the visitor keeps an
-- argument's trailing whitespace, so each value below is written with a
-- trailing "\n", as Studio Pro would have written it.
--
-- Verify after exec:
-- ./bin/mxcli -p app.mpr -c "describe microflow BugTestNewline.ACT_Newline"
-- every `)` and `;` must follow its expression on the same line, e.g.
-- change $Ticket (TicketState = BugTestNewline.TicketStatus.Open);
-- and the multi-line `find` keeps its interior line break.
--
-- Re-executing that describe output writes the expressions back WITHOUT the
-- newline, which is a real change to the document: the first re-exec reports
-- `Replaced microflow`, every one after it `Unchanged microflow`.

create module BugTestNewline;

create enumeration BugTestNewline.TicketStatus (Open 'Open', Closed 'Closed');

create persistent entity BugTestNewline.Ticket (
Title: string(200),
TicketState: enumeration(BugTestNewline.TicketStatus)
);

create or modify microflow BugTestNewline.ACT_Newline ()
begin
$Ticket = create BugTestNewline.Ticket (Title = 'First'
, TicketState = BugTestNewline.TicketStatus.Closed
);
change $Ticket (TicketState = BugTestNewline.TicketStatus.Open
);
$List = create list of BugTestNewline.Ticket;
add $Ticket to $List;
$Found = find($List, $currentObject/Title = 'First'
and $currentObject/TicketState = BugTestNewline.TicketStatus.Open
);
log info node 'BugTest' 'Title {1}' with ({1} = $Ticket/Title
);
return;
end;
Loading
Loading