Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a9b30fc
fix(alter-page): match SET-on-widget properties case-insensitively
claude Jul 27, 2026
6535d07
fix(modelsdk): serialize JavaScript action calls (were dropped on write)
claude Jul 27, 2026
06a9fac
fix(entity): preserve attribute identity on CREATE OR MODIFY (data loss)
claude Jul 27, 2026
1b5e5d5
fix(grammar): accept keyword names in widget action arguments
claude Jul 27, 2026
5d22652
feat(check): flag association traversal in widget expressions (MDL-WI…
claude Jul 27, 2026
d538264
fix(check): correct MDL001/MDL044 hints; document fallback-chain CE0111
claude Jul 27, 2026
b065191
feat(pages): support textbox placeholder and onchange (were dropped)
claude Jul 27, 2026
5976919
fix(docs,check): correct generated CLAUDE.md, HttpResponse casing, mu…
claude Jul 27, 2026
f024b34
perf(new): hard-link the mxcli binary into new projects instead of co…
claude Jul 27, 2026
a4eb812
chore: gofmt visitor.go (comment normalization)
claude Jul 27, 2026
af16d9e
feat(check): validate JS/Java action parameter names (--references)
claude Jul 27, 2026
2296276
feat(describe): round-trip textbox placeholder and onchange
claude Jul 27, 2026
671c145
fix(describe): round-trip dynamictext non-string attribute bindings
claude Jul 27, 2026
fe6052c
Merge pull request #48 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Jul 27, 2026
231f479
Merge branch 'mendixlabs:main' into main
ako Jul 27, 2026
ab706ba
feat(run): --trace-otlp to export traces to a collector (flame charts)
claude Jul 27, 2026
9570619
fix(catalog): flag xpath_expressions as a FULL-only table
claude Jul 27, 2026
3397baa
docs(catalog): attach .mxcli/catalog.db directly + app-warehouse recipe
claude Jul 27, 2026
a9dfaba
docs(skill): add analyze-runtime skill (logs + metrics + traces + cat…
claude Jul 27, 2026
e4c1f65
Merge pull request #49 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Jul 27, 2026
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
10 changes: 10 additions & 0 deletions .claude/skills/fix-issue.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .claude/skills/mendix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Page-specific patterns:
| [create-custom-widget.md](create-custom-widget.md) | Custom pluggable widget AIGC | Creating custom React widgets from scratch |
| [migrate-design-prototype.md](migrate-design-prototype.md) | Turn a Claude Design prototype into a themed Mendix app | Reproducing a design handoff/prototype as an SCSS theme + styled pages |
| [debug-bson.md](debug-bson.md) | BSON debugging | Troubleshooting SDK issues |
| [analyze-runtime.md](analyze-runtime.md) | Analyze runtime behavior — logs, metrics, traces, catalog, and cross-source joins | Profiling a slow page/microflow, finding what hits the DB, correlating cost with model shape |

---

Expand Down Expand Up @@ -97,6 +98,8 @@ Load skills based on the task:
| "Build a pluggable widget" | `create-custom-widget.md` |
| "Turn a design prototype/handoff into a Mendix app" | `migrate-design-prototype.md`, `theme-styling.md`, `create-page.md` |
| "Build/apply a theme from a design" | `migrate-design-prototype.md`, `theme-styling.md` |
| "Why is this slow / profile the app / what hits the database" | `analyze-runtime.md`, `run-local.md` |
| "Trace / metrics / flame chart / correlate cost with model" | `analyze-runtime.md` |

### For Error Recovery

Expand Down
155 changes: 155 additions & 0 deletions .claude/skills/mendix/analyze-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Analyze an App's Runtime Behavior — Logs, Metrics, Traces, Catalog

## Overview

When you need to understand what an app *actually does* at runtime — why a page is
slow, which microflow dominates cost, what hits the database, whether an error is
network or logic — the signals live in four places. This skill is the procedure for
collecting them and, crucially, **joining them**, because the useful questions cross
sources that no single tool answers alone.

| Signal | Where | How you get it |
|--------|-------|----------------|
| **Logs** (server stack traces + your `LOG` output) | `<projectDir>/.mxcli/runtime.log` | `mxcli run --local` tees it automatically |
| **Metrics** (throughput, DB counts, sessions, queues) | `/prometheus` on the admin port | `mxcli run --local --metrics` |
| **Traces** (per-microflow / per-activity spans + timings) | console→`runtime.log`, or an OTLP collector | `mxcli run --local --trace` / `--trace-otlp` |
| **Model shape** (activities, complexity, refs, XPath) | `.mxcli/catalog.db` (SQLite) | `mxcli … "refresh catalog full"` then `SELECT … FROM CATALOG.*` |

## When to Use This Skill

- A page/microflow is slow and you need to find *where* the time goes.
- You want to know which entities/queries the app actually hits, and how often.
- A server-side error shows only a generic dialog in the browser.
- You're profiling and need a flame chart, or want cost correlated with model shape.

Prerequisite: run the app with the fast local loop — see `run-local.md`. Everything
below assumes `mxcli run --local` (add the flags noted per signal).

## 1. Logs — the first stop for errors

`run --local` writes the runtime log to `<projectDir>/.mxcli/runtime.log` (override
`--runtime-log`, `-` disables). It carries JVM stdout/stderr **and** the application
log — server stack traces, your microflow/nanoflow `LOG` output, and the DB
synchronization counts at startup.

```bash
mxcli run --local -p app.mpr
tail -f .mxcli/runtime.log
```

Gotchas:
- **Nanoflow `LOG` lands under the `Client_Nanoflow` node**, not the node name you
declared — a filter built around microflow node names silently drops it. `LOG DEBUG`
from a nanoflow is dropped server-side (browser console only). See `write-nanoflows.md`.
- A spike in "Executing N database synchronization command(s)" on an *unchanged* model
is a red flag (see the `create or modify` data-loss class of bug).

## 2. Metrics — throughput and database pressure

`--metrics` registers a Prometheus registry, served at
`http://127.0.0.1:<admin-port>/prometheus` (loopback).

```bash
mxcli run --local -p app.mpr --metrics
curl -s http://127.0.0.1:8090/prometheus | grep -E 'connectionbus_|handler_requests|sessions_|taskqueue_'
```

Useful families: `connectionbus_{selects,inserts,updates,deletes,transactions}_total`
(database pressure), `handler_requests_total` (throughput), `sessions_*`,
`taskqueue_*` (background work). Merge any extra registry (otlp/influx/statsd) with
`--runtime-setting 'Metrics.Registries=[…]'`.

## 3. Traces — where the time goes

`--trace` attaches the bundled OpenTelemetry agent. **Default span filters ship with
it** (`OpenTelemetry._RuntimeSpanFilters`) because unfiltered per-activity tracing is
**~10× slower** and produces ~110k spans for one busy transaction — that's a
flow-*shape* debugging mode, not a timing mode.

```bash
# console exporter → runtime.log (span names/attrs only)
mxcli run --local -p app.mpr --trace
# flame charts: export to a collector (console can't reconstruct call trees/durations)
mxcli run --local -p app.mpr --trace-otlp http://127.0.0.1:4318
```

- The **console exporter omits start/end timestamps and parent span IDs** — you get
span names + attributes but no call tree and no durations. For real timing/flame
charts use `--trace-otlp <endpoint>` (implies `--trace`), which sets the OTLP
exporter for you; user-set `OTEL_*` env still wins.
- `--trace-service NAME` sets `OTEL_SERVICE_NAME` (default the `.mpr` name); use
distinct names per app for multi-app correlation. Trace context (W3C `traceparent`)
crosses app boundaries automatically over `rest call`.
- To examine flow *shape* on a small flow, temporarily disable the filters with
`--runtime-setting 'OpenTelemetry._RuntimeSpanFilters=[]'`.

## 4. Model shape — the catalog

The catalog is a SQLite database at `.mxcli/catalog.db` describing the model. **Run
`refresh catalog full`** — plain `refresh catalog` (fast mode) leaves the analytic
tables (`CATALOG.ACTIVITIES`, `CATALOG.REFS`, `CATALOG.XPATH_EXPRESSIONS`,
`CATALOG.WIDGETS`) **empty** (a fast-mode query warns "requires refresh catalog full").

```bash
mxcli -p app.mpr -c "refresh catalog full"
mxcli -p app.mpr -c "SELECT MicroflowQualifiedName, COUNT(*) activities
FROM CATALOG.ACTIVITIES GROUP BY 1 ORDER BY 2 DESC LIMIT 10"
```

`CATALOG.ACTIVITIES.Id` is the model GUID the debugger breaks on (see
`debug-microflows.md`), with the action name and its sequence — a named, ordered
activity list per microflow. See `catalog-search.md` / `graph-analysis.md` for the
richer queries.

## 5. The app warehouse — join the signals (external DuckDB)

Each signal alone answers little; the useful questions cross them. Because the catalog
is a plain database and the app's dev data is Postgres, one engine can join model
shape + live data + telemetry with **no ETL**. mxcli does **not** embed DuckDB — this
is a dev-container recipe (dev data, dev telemetry, everything read-only):

```sql
-- in duckdb, from the project dir, after `refresh catalog full` + a --trace-otlp run
ATTACH '.mxcli/catalog.db' AS cat (TYPE sqlite, READ_ONLY);
ATTACH 'dbname=app host=127.0.0.1 user=mendix' AS app (TYPE postgres, READ_ONLY);
CREATE VIEW spans AS SELECT * FROM read_json_auto('spans.jsonl');
```

Two joins that are impossible in any single source:

- **Runtime cost × model shape** — span durations per microflow joined to
`cat.activities_data` (count) and complexity. Complexity does *not* predict cost: a
2-activity flow that delegates can cost more than a 14-activity one. A lint rule
can't see that; this join can.
- **Query time × entity × live rows** — span DB timings joined to `cat` (which entity)
and `app` (row counts). This is how you find that the task-queue poller
(`system$queuedtask`, owned by no microflow) is the app's largest DB consumer —
invisible from any per-microflow view.

Caveats: keep every attachment **read-only** (never a production DB; even locally make
it explicit), and filter/sample traces first — unfiltered span volume is large.

## Known gap: logs ↔ traces

Runtime log lines carry **no trace id**, so joining logs to traces is a fuzzy
timestamp join (worst exactly under concurrency). The OTel agent populates the trace
id in the MDC, but Mendix's log pattern doesn't print it — closing the file-log side
needs an upstream `%X{trace_id}` change, not mxcli. For collector-side correlation,
export traces (and logs) via OTLP with `--trace-otlp` so the backend joins them.

## Decision guide

| Question | Reach for |
|----------|-----------|
| "Why did this error?" (server-side) | **Logs** (`runtime.log`) |
| "How much DB / throughput / queue work?" | **Metrics** (`--metrics`) |
| "Where does the time go in this flow?" | **Traces** (`--trace-otlp` for a flame chart) |
| "What's the flow's shape / activity list?" | **Catalog** (`refresh catalog full`) or the debugger |
| "Which cost belongs to which entity/model construct?" | **Warehouse** (catalog × spans × app DB) |

## See Also

- `run-local.md` — the local loop these flags hang off (`--metrics` / `--trace` / `--trace-otlp` / `--runtime-setting` reference).
- `debug-microflows.md` — interactive breakpoints/stepping when a trace isn't enough.
- `catalog-search.md`, `graph-analysis.md` — catalog query patterns and dependency analysis.
- `verify-with-oql.md` — query the running app's data directly.
16 changes: 14 additions & 2 deletions .claude/skills/mendix/run-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Launch `run --local` as the **sole** command in its invocation (don't chain a tr
| `--metrics` | off | Register a Prometheus meter registry at boot; the runtime serves metrics at `http://127.0.0.1:<admin-port>/prometheus` |
| `--trace` | off | Enable OpenTelemetry tracing (bundled agent, console exporter → the runtime log) with default span filters |
| `--trace-service` | `.mpr` name | `OTEL_SERVICE_NAME` under `--trace` |
| `--trace-otlp` | off (console) | Export traces to this OTLP collector endpoint (e.g. `http://127.0.0.1:4318`) instead of the console. Implies `--trace`. Needed for flame charts |
| `--runtime-setting Key=Value` | — | Merge an extra runtime setting into the boot config (Value parsed as JSON when possible). Repeatable. |

## Metrics and OpenTelemetry
Expand Down Expand Up @@ -159,8 +160,19 @@ with metrics/logs exporters off.
the microflow-level spans). Override with
`--runtime-setting 'OpenTelemetry._RuntimeSpanFilters=[…]'`.

**Export to a collector (OTLP) instead of the console:** set the OTEL env yourself
before running — `--trace` won't override an exporter you've already set:
**Export to a collector (OTLP) instead of the console:** the console exporter
omits start/end timestamps and parent span IDs, so call trees and durations can't
be reconstructed from it — for flame charts, export to an OTLP collector. Pass
`--trace-otlp <endpoint>` (implies `--trace`); mxcli sets
`OTEL_TRACES_EXPORTER=otlp`, `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`, and
`OTEL_EXPORTER_OTLP_ENDPOINT` for you:

```bash
mxcli run --local -p app.mpr --trace-otlp http://127.0.0.1:4318
```

You can still set the `OTEL_*` env yourself for full control — `--trace` /
`--trace-otlp` never override an exporter you've already set:

```bash
export OTEL_TRACES_EXPORTER=otlp OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
Expand Down
23 changes: 21 additions & 2 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,25 @@ retrieve $Items from Module.Entity where Active = true;

**Note**: `returns type as $Var` in the microflow signature does NOT create an activity variable — it only names the return value. So `$Var = call java action ...` after `returns as $Var` is fine (one creation).

**Fallback chains reuse the same name = CE0111.** Because each `$Var = call microflow …` (or retrieve/create) is a *fresh* variable creation, the natural "try A, else try B" shape is invalid — even when the retry is inside an `if`:

```mdl
-- WRONG: the second call re-creates $Summary — CE0111
$Summary = call microflow M.Inner(Tag = 'description');
if trim($Summary) = '' then
$Summary = call microflow M.Inner(Tag = 'summary'); -- CE0111!
end if;

-- CORRECT: one variable per call, then a plain `set` picks the winner
$Summary = call microflow M.Inner(Tag = 'description');
if trim($Summary) = '' then
$SummaryFallback = call microflow M.Inner(Tag = 'summary');
set $Summary = $SummaryFallback;
end if;
```

`mxcli check --references` catches this (CE0111) before the build.

## Legacy SOAP Web Service Calls

`call web service` preserves legacy Mendix SOAP activities. Prefer REST clients
Expand Down Expand Up @@ -1043,7 +1062,7 @@ After every `send rest request`, Mendix automatically populates `$latestHttpResp
```mdl
-- ✅ CORRECT: check $latestHttpResponse
$RootResult = send rest request Module.Service.GetData;
if $latestHttpResponse/content != empty then
if $latestHttpResponse/Content != empty then
-- Process $RootResult (the mapped entity)
end if;

Expand All @@ -1052,7 +1071,7 @@ if $RootResult != empty then -- ERROR!
```

**Key attributes on `$latestHttpResponse`:**
- `content` (String) — response body as string
- `Content` (String) — response body as string. **Capital `C`** — it is inherited from the parent entity `System.HttpMessage`, so `DESCRIBE ENTITY System.HttpResponse` does not list it. Lowercase `$latestHttpResponse/content` fails CE0117.
- `StatusCode` (Integer) — HTTP status code (200, 404, etc.)

**Restrictions:**
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ Regenerate after modifying `MDLLexer.g4`, `MDLParser.g4`, or any `domains/*.g4`
These rules apply whenever generating microflow or nanoflow MDL. Violations are caught by `mxcli check`.

1. **NEVER create empty list variables as loop sources.** If processing imported data, accept the list as a microflow parameter — `declare $Items list of ... = empty` followed by `loop $item in $Items` is always wrong.
2. **NEVER use nested LOOPs for list matching.** Loop over the primary list and use `retrieve $match from $TargetList where key = $item/key limit 1` for O(N) lookup. Nested loops are O(N^2).
2. **NEVER use nested LOOPs for list matching.** Loop over the primary list and use `$match = FIND($TargetList, key = $item/key)` for an O(N) in-memory lookup. A plain `retrieve … where` **cannot** filter a list variable (only a database/association source), so `retrieve $match from $TargetList where …` is a parse error — use `FIND`/`FILTER`. Nested loops are O(N^2).
3. **Use append logic when merging**, not overwrite: `$Existing/Field + '\n' + $New/Field` inside an `if $New/Field != empty` guard.
4. **Read `.claude/skills/patterns-data-processing.md`** for delta merge, batch processing, and list operation patterns.

Expand Down Expand Up @@ -525,7 +525,7 @@ Full syntax tables for all MDL statements (microflows, pages, security, navigati
- Docker build integration (`mxcli docker build`) with PAD patching (Phase 1)
- Warm local dev loop (`mxcli run --local [--watch] [--screenshot]`): Docker-free `mxbuild --serve` + standalone runtime, hot `reload_model` for behavioural changes and restart+DDL for structural ones (chosen from the serve build's `restartRequired`). Bundles the browser client (`web/dist/` via mxbuild's rollup runner, which the serve Deploy target skips) so Mendix 11.x apps render in a browser. `--watch` keeps an incremental rollup bundler hot (CHOKIDAR_USEPOLLING for container fs; ~3-4s page re-bundle, skipped for model-only edits) and watches only model source (`.mpr`+`mprcontents/`). `--ensure-db` provisions the local Postgres + app database if missing; `--setup` does the non-blocking prerequisites (cache mxbuild+runtime, ensure DB) and exits — `mxcli init` wires it into a Claude Code SessionStart hook so a fresh/reaped web session self-bootstraps, and `docs-site/src/tools/bootstrap-prompt.md` is the empty-repo seed prompt. `--screenshot` captures a Playwright PNG each change (pixel-perfect page loop), with `--screenshot-url` deep links (repeatable for multi-page sets, one PNG per page) and `--screenshot-user`/`--screenshot-password` form login (session saved as Playwright storage state, reused via `screenshot --load-storage`). See `docs/11-proposals/PROPOSAL_mxcli_dev_warm_loop.md`
- External browser preview (`mxcli run --hub <url>` + `mxcli tunnel-hub`): the app stays local and reverse-tunnels out over a single 443 connection (embedded chisel) to a static relay, so it is reachable in a browser at a public URL — works from egress-only environments (Claude Code web), verified live through the session's MITM egress proxy. `run --hub` implies `--local`, boots the runtime with `ApplicationRootUrl` set to the assigned URL (so the SPA/`originURI` work under the public origin), resolves the control proxy honouring `NO_PROXY`, and retries forever. `mxcli tunnel-hub --domain <base>` is the **multi-tenant** relay: a registry keyed by prefix/project/solution/branch/worktree (stable URLs on reconnect) fronts many previews at per-subdomain hosts (`[prefix-]project[-branch].<base>`; main collapses to the project) over one 443 with per-subdomain autocert, a registration API (`/api/register|status|deregister|backends`), and a sortable availability overview at `hub.<base>/`. Client identity flags: `--hub-prefix`/`--hub-project`/`--hub-solution`/`--hub-branch`/`--hub-worktree` (project + branch auto-detected). Package: `cmd/mxcli/tunnelhub/`. See `docs/11-proposals/PROPOSAL_mxcli_dev_warm_loop.md` (slices 3–4)
- Runtime metrics + settings passthrough (`mxcli run --local --metrics` / `--runtime-setting Key=Value`): `--metrics` registers a Prometheus Micrometer registry at boot (served at `http://127.0.0.1:<admin-port>/prometheus`); `--runtime-setting` merges arbitrary runtime config (e.g. `Metrics.Registries` for otlp/influx/statsd, or `OpenTelemetry._RuntimeSpanFilters`) into mxcli's **single** boot `update_configuration` call — the admin action replaces rather than merges, so folding settings into the one boot call is the only safe way. OTel traces via `--trace` attach the bundled `opentelemetry-javaagent` to the runtime JVM (console exporter → the tee'd runtime log) and ship default `OpenTelemetry._RuntimeSpanFilters` (unfiltered per-activity tracing is ~10× slower); `--trace-service` sets `OTEL_SERVICE_NAME`, and user-set `OTEL_*` env (e.g. an OTLP collector) is respected. See `.claude/skills/mendix/run-local.md`
- Runtime metrics + settings passthrough (`mxcli run --local --metrics` / `--runtime-setting Key=Value`): `--metrics` registers a Prometheus Micrometer registry at boot (served at `http://127.0.0.1:<admin-port>/prometheus`); `--runtime-setting` merges arbitrary runtime config (e.g. `Metrics.Registries` for otlp/influx/statsd, or `OpenTelemetry._RuntimeSpanFilters`) into mxcli's **single** boot `update_configuration` call — the admin action replaces rather than merges, so folding settings into the one boot call is the only safe way. OTel traces via `--trace` attach the bundled `opentelemetry-javaagent` to the runtime JVM (console exporter → the tee'd runtime log) and ship default `OpenTelemetry._RuntimeSpanFilters` (unfiltered per-activity tracing is ~10× slower); `--trace-service` sets `OTEL_SERVICE_NAME`. The console exporter omits timestamps/parent span IDs (no flame charts), so `--trace-otlp <endpoint>` (implies `--trace`) switches to the OTLP exporter (protocol `http/protobuf`) pointed at a collector; user-set `OTEL_*` env still takes precedence. See `.claude/skills/mendix/run-local.md`
- OQL query execution against running runtime (`mxcli oql`)
- Microflow/nanoflow debugger (`mxcli debug`): set breakpoints **by name** (activity resolved from the model), inspect paused flows + variables, step over/into/out, continue — against a `run --local` runtime. Two M2EE planes wired behind one command (admin `enable/disable/status`, app `/debugger/` session); `run --local --debug` enables it at boot. **Nanoflows** are auto-detected (uses the `nanoflow_name` breakpoint param; paused nanoflows are merged from `poll_events`, which `get_paused_microflows` omits). Nanoflow `LOG` output is rewritten to the `Client_Nanoflow` node in the runtime log. See `.claude/skills/mendix/debug-microflows.md` and `docs/11-proposals/PROPOSAL_microflow_debugger.md`
- Business event services (SHOW/DESCRIBE/CREATE/DROP)
Expand Down
Loading
Loading