Skip to content

Sync ako/mxcli: annotations/folders/enum ids for lint rules, page round-trip fixes, MOVE access rules, docker builds as stored - #1793

Merged
ako merged 56 commits into
mendixlabs:mainfrom
ako:main
Nov 10, 2026
Merged

ako merged 56 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

Syncs ako/mxcli:main into upstream: 14 commits — the code behind several v0.31.0 release-note entries, plus page round-trip, MOVE, lint and docker fixes. 7 upstream issues fixed.

Lint API and catalog

Pages

  • The Data grid 2 / Gallery empty placeholder builds. emptyplaceholder { … } was accepted while showEmptyPlaceholder was its default none, which hides the slot — CE0463 on the grid. MDL-WIDGET10 now counts a slot filled by a child block as set, and a filled hidden slot is an error naming the property. mxcli syntax page.widgets documents the form that builds (showEmptyPlaceholder: 'custom' plus the block) and its ALTER PAGE equivalent. Fixes Data grid 2: no MDL form for the emptyPlaceholder slot #1704
  • An unchanged page's round trip no longer rewrites it. describe → exec reported "Replaced page" with nothing changed, for 15 of one test app's 68 pages: a tab container's null default tab was written as the first tab (a stored second-tab default was reset every pass — a real loss), an expression's trailing newline was dropped, and an empty screen-reader label was written null. Reconcile now carries each from the stored element; all 15 report Unchanged, and 24 documents leave the round-trip suites' known-failure lists.
  • A snippet parameter may come from any enclosing data container of the parameter's entity, a specialisation or a generalisation — as Studio Pro and mx check accept — not only the nearest. And an association hop whose stored destination is neither end of its association prints Assoc/Destination, so the rebuild arrives at the stored entity instead of the association's end.

MOVE between modules

Splitting a module left an entity access rule's XPath naming the old module ([Src.Order_Customer/Src.Customer/…], CE1613), its rules on the old module's roles, and a moved nanoflow's grants unmapped. Path steps in XPaths and expressions are now rewritten when the moved element is an entity or association, and an entity's access rules and a nanoflow's grants move to the target's same-named roles (a role the target lacks is kept, with a warning). Measured on 11.12.6: the issue's split leaves 2 errors before, 0 after. Fixes #1687

lint

docker build / run / reload

They ran mx update-widgets on their temporary copy before the check and build, so a project with stale stored widget definitions built while docker check, run --local and Studio Pro reported CE0463 — the deployment was not built from what is committed. The pre-build check now checks the stored copy and labels each CE0463 as one mxcli fix widgets clears or one that persists, and MxBuild builds the model as stored. --update-widgets keeps the old behaviour (and says the project still has stale widgets); --no-update-widgets skips the labelling re-check.

init --tool opencode

The generated opencode.json listed both skill trees as instructions, which OpenCode inlines whole: 150 files, 2.7 MB, ~73% of a 1M context window before the user typed anything. Its instructions are now AGENTS.md alone; OpenCode discovers .opencode/skills/ and loads a skill when its description matches. Those copies no longer get a second frontmatter whose title-based description OpenCode read instead of the skill's own. Fixes #1683

Tests

EntityToRefresh = "CustomModule"."NPActivity" on a JavaScript action argument is pinned: main stores the bare qualified name and mx check 11.14 reports 0 errors, so the regression reported against 0.26.0 is caught by name. Refs #1785

claude and others added 26 commits November 9, 2026 13:21
The generated opencode.json listed both skill trees as `instructions`,
which OpenCode inlines whole: 150 files, 2.7 MB, ~73% of a 1M context
window before the user typed anything. Its instructions are AGENTS.md
alone; OpenCode discovers .opencode/skills/ and loads a skill when its
description matches.

Those copies were also given a second frontmatter, described by the
skill's title, in front of the skill's own; OpenCode read the title
instead of the "Use when…" description. A skill with frontmatter now
keeps its description in a single block.

Fixes #1822

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
Inside a loop Mendix allows only Rollback (CE0644; check warns MDL006
on anything else), and a call to a Java action that exists to throw,
such as CommunityCommons.ThrowException, is how a flow rolls itself
back. CONV013 reported both, so an app needed a baseline entry for a
deliberate pattern. Neither is reported now; other external calls are
reported as before.

Fixes #1838

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
describe page X, then exec of that output, reported "Replaced page"
for 15 of TestApp's 68 pages with nothing changed. Three values MDL
cannot state were regenerated on rebuild:

- a tab container's DefaultPagePointer: stored null ("no default"),
  written as the first tab; a stored second-tab default was reset on
  every pass, a real loss;
- an Expression with a trailing newline, which MDL writes bare;
- an empty screen-reader label Studio Pro stores as a ClientTemplate
  with no text, written null.

Reconcile now carries each from the stored element of the same $ID,
after TransplantIDs: the default tab while it is null or names a tab
the rebuild still has, an expression that differs only in surrounding
whitespace, an empty template where the rebuild writes null. All 15
pages report Unchanged; a real change still lands.

Refs #1897

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
…1436)

A rule saw a folder only through the folder of a document in it, so an
empty folder did not exist for it. folders_data holds one row per folder
with its module, path (spelled as a document's Folder), parent path,
depth and the documents and folders directly in it; folders() exposes it
to Starlark rules. The rows are project-wide: a document moving between
folders changes two folders' counts but rebuilds only the document.

CATALOG.FOLDERS needed FOLDERS in catalogTableName, being a keyword;
without it the select parsed to nothing and printed nothing. A new test
parses every name Catalog.Tables() lists, and found CATALOG.AGENTS broken
the same way on main, fixed here too.

Schema 38.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(catalog): folders() and CATALOG.FOLDERS, empty folders included (#1436)
The integration suite fails when a listed loss stops reproducing, and
this one did: the page's whole-document round trip is now exact, the
tab container's null default tab being carried (#1897).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
…ists

The round-trip suites fail when an allowlisted document stops breaking,
and the carries for #1897 made 23 of them exact: Account_Overview on
PedApp and TestApp, and on TestApp fifteen WorkflowCommons pages and
seven of its snippets. No other document's outcome changed; make
test-integration-roundtrip passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
enumerations().values gave each value's caption in the default language
only, and a caption in another language is a strings() row naming the
value only by element_id, which matched nothing a rule could see. Each
value now carries id, the value's $ID, which is that element_id, so a
rule can resolve getCaption(Module.Enum.Value) per language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…24rqgv

Unchanged pages no longer rewritten, opencode context size, CONV013 false positives
feat(lint): an enumeration value's id in Starlark rules (#1301)
Splitting a module with MOVE left an entity access rule's XPath naming
the old module ([Src.Order_Customer/Src.Customer/...]; 11.12.6 mx check:
CE1613 for both), its rules on the old module's roles, and a moved
nanoflow's grants unmapped while a microflow's are mapped.

The reference sweep now also rewrites path steps, in XPaths and in
expressions such as $O/Src.Assoc/Name, when the moved element is an
entity or an association; an enumeration rename still leaves a
same-named association alone. An entity's access rules and a nanoflow's
grants move to the target's same-named roles; a role the target lacks
is kept, with a warning.

Measured on a blank 11.12.6 app: the issue's split leaves 2 errors with
main, 0 with this change.

Fixes #1836

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
They ran mx update-widgets on their temporary copy before the pre-build
check and MxBuild, so a project whose stored widget definitions are
stale built, while docker check, run --local and Studio Pro reported
the CE0463s: the deployment was not built from what is committed.

The pre-build check now checks the stored copy, as docker check does
since #646, and on CE0463 labels each one as a stale definition
`mxcli fix widgets` clears or one that persists; MxBuild builds the
model as stored. --update-widgets (build, run, reload) keeps the old
behaviour and says the project still has stale widgets;
--no-update-widgets skips the labelling re-check.

Fixes #1794

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
…lised hop

describe → exec of TestApp's WorkflowCommons.ManageTaskAssignments was
refused: its snippet call omits $Account, which Studio Pro stores
unmapped and mx check accepts (11.14) because a System.User data view
two containers out supplies it, while the builder looked at the nearest
container only. An omitted parameter is now satisfied by any enclosing
data container whose entity is the parameter's, a specialisation or a
generalisation of it.

With that lifted the page built with CE1613: describe printed the
single association hop TaskAssignmentHelper_User without its stored
destination Administration.Account, so the rebuild arrived at
System.User and labels bound to FullName lost their attribute. A hop
whose stored destination is neither end of its association now prints
Assoc/Destination.

The page now reports Unchanged and mx check 0 errors; it and three
WorkflowCommons snippets leave the TestApp round-trip allowlist.

Fixes #2055

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
…24rqgv

MOVE rewrites XPaths and roles; docker build builds the stored project; snippet params from outer containers
mendixlabs#1785 reported `EntityToRefresh = "CustomModule"."NPActivity"`
stored with its MDL quotes on mxcli 0.26.0, giving CE1613 at mxbuild.
Main stores the bare qualified name (EntityTypeCodeActionParameterValue,
checked in the BSON) and mx check 11.14 reports 0 errors on TestApp. The
reporter's MDL is now a test, so a regression is caught by name.

Refs #2057

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
… notes (#1300)

A microflow's annotations reached a rule as activities; a domain model's
were in no table, so a "no TODO notes" rule passed every domain model.
annotations_data holds one row per domain model annotation with its
module, full caption (newlines kept) and canvas position, owned by the
domain model unit like the entity rows; annotations() exposes it to
Starlark rules. ANNOTATIONS joins catalogTableName, being a keyword.

Schema 39.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`emptyplaceholder { ... }` on a Data grid 2 (or Gallery) was accepted by
check and exec, and the page then failed mx check with CE0463 on the
grid (11.14): the slot is hidden while showEmptyPlaceholder is "none",
its default. Studio Pro's own grid with a placeholder stores "custom".

MDL-WIDGET10 now counts a slot filled by a child block as set, and a
filled hidden slot is an error naming the property, as a hidden action
already is. `mxcli syntax page.widgets` documents the form that builds
(showEmptyPlaceholder: 'custom' plus an emptyplaceholder block) and its
ALTER PAGE equivalent; both check clean in mx check and round-trip
through describe unchanged.

Fixes #1862

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
feat(catalog): annotations() and CATALOG.ANNOTATIONS for domain model notes (#1300)
)

jar_dependencies() lists the Maven dependencies module settings declare,
not what is on disk, so a hand-added jar and the managed one in another
version went unseen. jar_files() reads the top level of userlib/ and
vendorlib/ at each call, through the backend's Path(), and gives each
jar's folder, file name, library name and version (split at the last
dash followed by a digit) and the modules whose .RequiredLib marker names
it. No catalog change: the jars change without the .mpr changing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…24rqgv

Empty placeholder documented and checked; pin quoted entity-type JS action arguments
feat(lint): jar_files() lists the jars in userlib/ and vendorlib/ (#1304)
Brings in upstream's two commits the fork lacked: the v0.31.0 release
changelog/blog and a gofmt of splice_handler_test.go.

CHANGELOG resolved by hand. Git's merge placed the fork's 11 Unreleased
entries (work that is not in the v0.31.0 tag) under the new [0.31.0]
heading; they stay under [Unreleased]. Everything from [0.31.0] down is
upstream's text verbatim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeAyg3qx2my26Q8cvGyW5z
Upstream's 1365ba4 reformatted it with a gofmt other than go1.27.3's,
the toolchain go.mod pins and make fmt-check runs, so the merged tree
failed fmt-check (mendixlabs#1793). This restores the fork's
toolchain-formatted version; make fmt-check passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeAyg3qx2my26Q8cvGyW5z
`brain capture --from-json <file|->` queues a JSON array of entries:
every item is validated before any is queued (a bad one is named by
its index), and a duplicate of a committed, queued or earlier item is
skipped with the existing id, so the same list twice is a no-op. Ids
are assigned against those given earlier in the batch as well as the
stored ones, and the queue is appended once under its lock.

`brain promote <id>...` resolves every id and checks each shard's cap
with all of the batch's additions before writing anything. Shards are
written first, each atomically, then the queue once; a re-run after an
interruption between the two counts an id already in its shard with
the same content as promoted and completes the batch. --to applies to
every id, and a single id keeps its --json object shape.

Shard files are now written through writeFileAtomic.

Fixes #2054

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
ako and others added 3 commits November 10, 2026 07:02
…it (#1303)

With two buttons on a page a rule could not tell which caption belonged to
the one without a confirmation: widgets() had no caption, and the strings()
caption row named the button's inner Forms$ClientTemplate. widgets gains
Caption and Tooltip in the default language (a button's CaptionTemplate, a
label's Caption), and strings gains WidgetId, the nearest named Forms$ or
CustomWidgets$ element around the text, so a widget's caption, tooltip and
confirmation texts can be collected per language.

Schema 40.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
Merge upstream v0.31.0 into main; re-gofmt splice_handler_test.go with the pinned toolchain
ako and others added 27 commits November 10, 2026 07:26
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nces (#2074)

An agent-editor document produced no refs, and a microflow passed as a
microflow-typed Java action parameter was no reference, so an agent's
tool microflow, a chat context's ActionMicroflow and MCPServer.AddPrompt's
Microflow showed "(no callers found)" and sat in GRAPH_DEAD_ASSETS.

The agent pass now decodes each document (mprread.DecodeAgentEditorUnit,
the decoder describe agent uses) and buildReferences writes its edges:
an agent's tool microflow, nanoflow or agent is a call, a knowledge base
or MCP service a tool, its model a model, its context entity a type, and
a model's or knowledge base's key constant a key. A microflow or agent
document passed as a Java action parameter is a call. show callers counts
tool and model.

Schema 41.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…24rqgv

brain: capture and promote many entries as one act
feat(catalog): a widget's caption and tooltip, and its texts tied to it (#1303)
fix(catalog): agents and the microflows a Java action runs are references (#2074)
#2031)

Agent Editor 2.2+ refused every agent mxcli created: AgentEditorCommons'
AgentFromModel_Validate requires SelectedModelID ("Model with name  is
referenced but could not be found for the Agent"), and the refusal at
startup stops the import of every agent document. mxbuild does not read
agent documents, so check and the build stayed green.

- create / create or modify / alter agent take SelectedModel: '<id>' and
  an optional SelectedModelName. IM_AgentFromModel reads only
  selectedModel/id (matched against DeployedModel.Model); name is the
  display text Studio Pro stores, so it defaults to the id and keeps the
  stored name while the id is unchanged. '' clears it; left out, create
  or modify keeps the stored value.
- selectedModel is modelled in the Contents carry and written after
  model, before entity, as Studio Pro does. describe prints it.
- check warns on a create agent without one (MDL-AGENT02).
- Skill, syntax help, reference page, quick reference and examples show
  it (part of #2067).

Measured: AgentEditorCommons 2.2.1 (ICXGraphPilot, mxcli-chat) and 2.6.0
(Agent Builder Starter App) require it; 1.1.0 (FactoryManagement) does
not read it. describe -> exec of every agent-editor document in fresh
ICXGraphPilot (6), FactoryManagement (33) and starter-app (9) copies
changes no unit; a build without the encoder field changes GraphExplorer.
alter set SelectedModel changes only selectedModel, and setting it back
restores the stored bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82ttaGzay5CwiC2V7cf
…2075)

CATALOG.AGENTS and its siblings held a document's name only; the settings
live in the JSON Contents the agent pass did not read. AGENTS gains usage
type, model, selected model, generation settings, streaming switches (NULL
when not stored), context entity, variables and tool counts; AI_MODELS and
KNOWLEDGE_BASES provider and key constant; CONSUMED_MCP_SERVICES protocol
version, version and timeout. New AGENT_TOOLS (with the AGENT_MCP_SERVICES
view), AGENT_KNOWLEDGE_BASES and PUBLISHED_MCP_SERVICES; a published MCP
service's tool and authentication microflows are publish edges. Schema 42.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a microflow M.Nightly and a scheduled event M.Nightly, `move
scheduled event M.Nightly to folder 'Jobs'` was refused as "a
microflow, not a scheduled event": the generic move looked the name up
whatever its type and took the first match. A name is unique per
document type, not per module.

FindDocumentUnits returns every document of that name in the module,
and the move (and its already-placed check) picks the one of the asked
kind, falling back to the first so a true typo still names what the
document is. Verified on TestApp: the unfixed binary refuses, the fixed
one moves the scheduled event only, and mx check reports 0 errors.

Fixes #2072

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
feat(catalog): agent details, agent tools and published MCP services (#2075)
CATALOG.STRINGS indexed only an agent's documentation, so search could not
find its system or user prompt or the descriptions the model reads for each
tool and knowledge base. They live in the JSON Contents, which the
translatable walk does not see. The strings pass now indexes them from the
documents the agent pass already decoded, each row owned by its agent's
unit. Schema 43.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ompts

# Conflicts:
#	CHANGELOG.md
#	mdl/catalog/tables.go
…#2060)

create consumed mcp service wrote only protocolVersion, version,
connectionTimeoutSeconds and documentation. AgentEditorCommons'
IM_ConsumedMCPService maps endpoint/qualifiedName to MCPEndpointConstant
and authenticationMicroflow/qualifiedName to GetCredentialsMicroflow, and
ConsumedMCPService_CreateUpdateFromModel imports the service only when
the endpoint constant has a value. So an mxcli-written service was
skipped at startup and an agent's mcp service block got no tools, while
mx check and the build stayed green.

- create / create or modify / alter consumed mcp service take
  Endpoint: @Module.UrlConstant (a String constant) and
  AuthenticationMicroflow: Module.GetHeaders; describe prints them;
  create or modify keeps stored ones; alter ... = '' clears them.
- Both are modelled in the Contents carry; a new document writes endpoint
  first and the credentials microflow last, each documentId first, as
  Studio Pro 11.16 saves them.
- check warns on a create without an Endpoint (MDL-AGENT03).
  check --references and exec refuse a missing or non-String constant;
  --references also refuses a credentials microflow MCP Client cannot
  call: ConsumedMCPService_CreateMCPClient runs it with no arguments and
  reads its result as a list of System.HttpHeader.
- Agents skill, syntax help, reference page and examples (part of #2067).

Measured: the same mapping in AgentEditorCommons 1.1.0, 2.2.1 and 2.6.0;
documentId is the referenced unit's id. describe -> exec of every
agent-editor document in fresh ICXGraphPilot (6), FactoryManagement (33)
and starter-app (9) copies changes no unit; a build without the encoder
field changes all six FactoryManagement consumed MCP services. alter set
Endpoint changes only endpoint, and setting it back restores the stored
bytes. mx check of a copy with a new service equals the untouched copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82ttaGzay5CwiC2V7cf
…#2075)

The client's model reads them as an agent's model reads its tool
descriptions; available now that #2080 decodes the published MCP service.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…24rqgv

move: pick the document of the kind the statement names
A page hosting Conversational UI's chat needs a data view on
ConversationalUI.ChatContext: the chat snippet takes that entity as its
parameter, exactly as the module's own full-screen chat page does. No
view entity or microflow can stand in, yet ARCH001 told every such page
to use one.

A cross-module entity is now exempt when the page calls a snippet, or
links to a page, of that entity's module that takes the entity as a
parameter, read from the refs table the catalog already builds. Every
other cross-module data view is still reported. Verified on a real
catalog: the old rule flags the chat host page, the new one does not,
and a bare data view on the same entity stays flagged.

Fixes #2069

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
feat(catalog): agent prompts and tool descriptions in full-text search (#2075)
…ed-model

# Conflicts:
#	CHANGELOG.md
#	mdl/types/agenteditor.go
#	modelsdk/mprread/reader_documents.go
…24rqgv

lint: ARCH001 exempts an entity fed to its own module's snippet or page
`rename page M.Old_Page to New_Page;` followed by `show page
M.New_Page` was refused by check --references ("page not found"),
though exec ran it. The rename was recorded only in indirectDocs, which
only the alter-target check reads; the reference checks consult each
kind's own script-defined set. markIndirectDoc now writes both, for
renames and cross-module moves alike.

Verified on TestApp: check refused before and passes after; exec and
mx check report 0 errors.

Fixes #2070

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82qrr1WnXxq1eRn7qRJ
…24rqgv

check: resolve a document renamed or moved earlier in the script
fix(agents): create agent writes the selected model Agent Editor needs (#2031)
fix(agents): a consumed MCP service gets its endpoint and credentials (#2060)
@ako
ako merged commit c0c8b4d into mendixlabs:main Nov 10, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment