Skip to content

Sync ako/mxcli: lint --baseline, permission access-rule ids, more Starlark data; faster describe and widget refresh - #1712

Merged
ako merged 25 commits into
mendixlabs:mainfrom
ako:main
Nov 1, 2026
Merged

ako merged 25 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator

Syncs ako/mxcli:main into upstream: 6 commits from fork PRs ako#1843, ako#1844, ako#1845, ako#1849 and ako#1852.

Fixes #1455, #1539 and #1556.

Lint

  • New --baseline mode for mature apps (lint: baseline / new-findings-only mode for mature apps #1455). On a mature app, lint's totals are the backlog, not the change just made.
    • mxcli lint --baseline <file> --update-baseline records the current findings.
    • mxcli lint --baseline <file> then reports only the new ones. Its exit code follows them alone, and stderr gets an "N new, M already known, K fixed" summary.
    • A finding is keyed on its rule and document.
  • Permission rows name the access rule they come from (lint: permission rows carry no access-rule id #1539). permissions() and permissions_for() returned one row per module role and right, with no id of the access rule behind it. So a rule couldn't count an entity's access rules or list one rule's roles exactly. Entity and member rows now carry access_rule_id and access_rule_index.
  • The Starlark API exposes catalog and settings data that guideline rules need (lint: Starlark API lacks data that catalog tables have (view entity OQL, enumeration values, excluded flag, constant values) #1556):
    • microflow excluded and apply_entity_access;
    • enumeration values, each with qualified name, caption and ordinal;
    • constant_values(), the run-configuration constant values;
    • project_settings(), covering default language, OQL and Java version, hash algorithm, time zone, rounding, decimal scale, optimistic locking, foreign keys and web client.
  • Catalog schema bump: the catalog schema goes to 30, so a catalog cached by an older build is rebuilt instead of answering with nothing.

Faster describe and catalog refresh

  • describe decodes only the documents it prints. It found its document, and the snippets, pages, layouts and flows it names, by listing every document of the type, and each listing decoded every one.
    • Lookups now go through headers and decode one unit by ID.
    • On a 4,753-unit app, a first refresh catalog full source took 19 s and 38 s CPU, down from 33 s and 145 s, with identical text for all 3,718 documents.
  • editorConfig visibility rules are read in linear time. The extractor's end-anchored regexps grew with the square of the file: 0.5 s for the 400 KB editorConfig in Maps, on every refresh. Suffix scans give byte-identical rules on 256 widgets across five projects.
  • Colliding widget names are kept apart. KeyboardShortcut.mpk and itvisors.KeyboardShortcut.mpk both derived KEYBOARDSHORTCUT and overwrote one definition in turn, so one widget had none. Every refresh then reported "2 refreshed".
    • Names are now assigned over the whole project.
    • The catalog pairs definitions by widget ID.
    • An unchanged widgets/ folder is skipped instead of re-reading every .mpk

ako and others added 25 commits October 31, 2026 20:56
…#1453)

Starlark rules could not read data the catalog or the project settings
already hold:

- microflow `excluded` and `apply_entity_access` (new ApplyEntityAccess
  column; always false for a nanoflow)
- enumeration `values`, each with qualified name, caption and ordinal, so a
  rule can resolve getCaption(Module.Enum.Value) (#1301)
- `constant_values()`: the run-configuration constant values
- `project_settings()`: default language, OQL version, Java version, hash
  algorithm, time zone, rounding, decimal scale, optimistic locking, foreign
  keys and web client

Catalog schema 30: the linter selects the new column, so a catalog cached by
an older build must be rebuilt rather than answer every Microflows() query
with nothing. The bump also covers #1781's message-definition edges, which
shipped without one. The lint skill moves its security and settings structs
to security-and-settings.md to stay under the 700-line body limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
permissions() and permissions_for() returned one row per module role and
right with no id of the entity access rule behind it, so a rule could not
count an entity's access rules per setting or list one rule's module roles
exactly; grouping on XPath, default and rights merges two rules that agree on
them.

Entity and member rows now carry access_rule_id and access_rule_index (the
rule's position on the entity); a page, flow or service row has "" and -1.
Catalog schema 30, because the linter selects the new columns. The permission
struct doc moves to catalog-tables.md to keep SKILL.md under its size limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne (#1328)

On a mature app lint's totals are the backlog, not the change just made.
`mxcli lint --baseline <file> --update-baseline` records the current findings;
`mxcli lint --baseline <file>` then reports only the new ones, with an exit
code that follows them alone and an "N new, M already known, K fixed" summary
on stderr.

A finding is keyed on its rule and document (the document id where the rule
gives one, so a rename keeps it), not its message: within a key, findings
match by identical message first and then by count, so a reworded rule is not
new while a further finding of the same rule in the same document is. Rule
failures are never baselined. A scoped run (-m/-d/-r) reports new findings but
not fixed ones, and cannot update the baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(lint): expose the catalog and settings data guideline rules need (#1453)
…on-access-rule-id

# Conflicts:
#	.claude/skills/mendix/write-lint-rules/SKILL.md
#	CHANGELOG.md
#	mdl/catalog/tables.go
feat(lint): permission rows name the access rule they come from (#1437)
feat(lint): --baseline reports only findings not in a recorded baseline (#1328)
Studio Pro answers a tool whose handler throws with
"Error executing tool handler: …" and isError:false (measured on 11.16.0
for ped_delete_document). Only the create/update callers looked at the
text, so any other caller trusting the flag read the failure as success.
CallTool now sets IsError for that text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Studio Pro 11.16 ships ped_delete_document, but every DROP of a
standalone document (constant, enumeration, microflow, nanoflow, page,
workflow, business-event service) still demanded --mcp-concord.

deleteDocument asks the native tool when tools/list advertises it
(Client.HasTool) and keeps Concord for older servers, and as a fallback
when the native call fails. The tool's success reply has never been
observed (on 11.16.0 it fails outside Studio Pro's agent session with
"Session 'mcp-server' not found"), so an unrecognised reply is settled
with ped_find_document rather than by guessing the text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The extractor ran end-anchored regexps over everything before each hide
call, so its time grew with the square of the editorConfig.js: 0.5 s for
the 400 KB one in Maps, on every catalog refresh. Suffix scans replace
them; the rules are byte-identical on 256 widgets across five projects.

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

KeyboardShortcut.mpk and itvisors.KeyboardShortcut.mpk both derived
KEYBOARDSHORTCUT and rewrote one .def.json in turn: one widget had no
definition, and every refresh reported "2 refreshed" and regenerated the
widget docs. Names are now assigned over the whole project (the pluggable
widget keeps the keyword), and the catalog pairs definitions by widget ID.

A refresh also re-read every .mpk although nothing had changed. It now
records the stats of the packages and definitions, and skips the step
when they and the mxcli build are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82ttaGzay5CwiC2V7cf
describe found its document, and the snippets, pages, layouts and flows
it names, by listing every document of the type, and on the modelsdk
backend a listing decodes each one. A source catalog build describes
every document, so it paid that once per document.

Lookups now go through headers and decode one unit by ID: snippets,
nanoflows, pages and layouts by name or ID, and GetPage / GetNanoflow /
GetLayout read only their unit. layoutCheckBackend forwards the header
listings, which its embedded interface hid from the type assertion.

FactoryManagement, first `refresh catalog full source`: 33 s / 145 s CPU
before, 19 s / 38 s after, with identical text for all 3,718 documents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82ttaGzay5CwiC2V7cf
Skip widget-definition work on a catalog refresh that does not need it (#1848)
describe decodes only the documents it prints (#1850)
DROP over --mcp uses Studio Pro 11.16's ped_delete_document; handler errors are failures
`--mcp-trace` / `--mcp-verbose` printed each MDL statement but never a tool
call, on exec, -c, the REPL and `mcp capabilities`. The executor got the
tracer; the MCP backend the CLI builds did not, because WithTracer had no
caller outside tests. A run that sent every write to Studio Pro therefore read
as one that sent nothing, which is how mendixlabs#1684 concluded that
`exec --mcp` wrote the .mpr instead.

TestMCPTrace_ListsThePEDCallsOfAWrite drives the CLI's executor builder
against a fake Studio Pro. Before: the fake received ped_get_schema,
ped_create_document and six ped_check_errors while stderr held only the two
statement headers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUbjJ5DeS2vysn9hfKir4Y
Without --mcp-dial, a localhost/127.0.0.1 endpoint was dialled as
host.docker.internal:<port> whenever that name resolved, taken to mean
"inside a container". Docker Desktop for Windows puts the name in the host's
own hosts file, pointing at the LAN address, and Studio Pro listens on
loopback only, so `--mcp http://localhost:7782/mcp` timed out on the machine
Studio Pro runs on (`dial tcp <LAN IP>:7782: i/o timeout`,
mendixlabs#1684).

The client now dials a plan: --mcp-dial alone when given, otherwise the URL
host first and host.docker.internal only as the fallback, reporting every
failed attempt. A devcontainer pays one refused loopback connection.

TestClient_LocalhostReachesStudioProOnTheHostWhenTheDockerNameResolves
forces the name to count as resolving and connects to a loopback fake; with
the old rewrite restored it fails dialling host.docker.internal.
cmd/mcpprobe keeps its own always-rewrite dial: it is a devcontainer probe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUbjJ5DeS2vysn9hfKir4Y
fix(mcp): --mcp-trace lists the PED calls; dial loopback before host.docker.internal (#1833)
@ako
ako merged commit 002ce80 into mendixlabs:main Nov 1, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint: baseline / new-findings-only mode for mature apps

2 participants