Skip to content

Sync ako/main → upstream: RSS-reader + sudoku findings — authoring/check fixes, DESCRIBE round-trips, tracing & catalog observability - #792

Merged
ako merged 20 commits into
mendixlabs:mainfrom
ako:main
Jul 27, 2026

Conversation

@ako

@ako ako commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Brings the 17 commits on ako/main since the last sync (#788). Clean merge against
main; each fix has a Go regression test and (where applicable) an MDL repro under
mdl-examples/bug-tests/. Two rounds of findings, all verified by the reporters:

Authoring / write-path fixes (RSS-reader)

  • ALTER PAGE set X on <builtin> now matches property names case-insensitively
    (was a hard error on every built-in widget). mdl/backend/pagemutator
  • call javascript action is serialized on the default modelsdk engine (was
    persisted as -- Empty action → CE0008/CE0109). Microflows + nanoflows.
  • create or modify persistent entity preserves attribute identity — no more
    destructive DB column drop/re-add on an identical re-run (data loss).
  • textbox placeholder + onchange are supported end-to-end (were silently dropped).
  • Widget action args accept keyword names (View/Source/Item/Page/Entity)
    unquoted, matching call microflow.

Checker (check --references)

  • New MDL-WIDGET13: association traversal in a widget expression (dynamicclasses
    /visibleif/editableif) is flagged (CE0117) before the build.
  • JS/Java action parameter names are validated against the action's declared
    parameters (a wrong/mis-cased name wrote a dangling ref → CE1613).
  • Corrected MDL001 (→ FIND(...)) and MDL044 (aggregate-aware) hints; fixed the
    dead mxcli syntax expressions reference; documented fallback-chain CE0111.

DESCRIBE round-trip

  • textbox placeholder/onchange and non-string dynamictext attributes
    (toString($currentObject/Attr) → bare attribute) now round-trip — re-applying a
    DESCRIBE dump no longer corrupts the page (was CE1613).

Diagnostics / docs

  • Distinct "string cannot span lines" parser diagnostic (was the misleading
    "double your apostrophes"); corrected the generated CLAUDE.md template and the
    $latestHttpResponse/Content casing.

Tooling & observability (sudoku)

  • mxcli new hard-links the binary into new projects (no ~111 MB copy each).
  • mxcli run --local --trace-otlp <endpoint> exports traces to an OTLP collector
    (the console exporter can't do flame charts). Implies --trace; user OTEL_* wins.
  • Catalog: xpath_expressions is flagged as FULL-only (a fast-mode query warned
    for activities/refs but not xpath → silent empty results).
  • New analyze-runtime skill (logs + metrics + traces + catalog + the cross-source
    "app warehouse" join) and a catalog direct-attach docs recipe.

🤖 Generated with Claude Code

claude and others added 20 commits July 27, 2026 02:08
`alter page P { set class = 'x' on topBar; }` hard-errored on every
built-in widget:

  failed to set: failed to set class on topBar:
    property "class" not found (widget has no pluggable Object)

The MPR page mutator (setRawWidgetPropertyMut) dispatched first-class
widget properties via a case-sensitive switch on canonical casing
("Class"/"Caption"/"DynamicClasses"/...). A lowercase name typed in MDL
missed every case and fell through to the pluggable-Object path, which
errors on built-in widgets. `create page` was unaffected because
WidgetV3.GetStringProp reads properties case-insensitively, and the
sibling MCP page mutator already lowercased — only the MPR ALTER path
diverged.

Match first-class property names via strings.ToLower (mirroring the MCP
backend); the pluggable fallback keeps the original casing since template
property keys are case-sensitive.

Reported from the RSS-reader build (finding #1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`call javascript action` (e.g. NanoflowCommons.OpenURL) passed
`mxcli check --references` and `mxcli exec` reported success, but the
activity was written with no action — DESCRIBE showed `-- Empty action`
and `mx check` failed with CE0008 "No action defined." and CE0109 on the
now-missing output variable.

The default modelsdk engine's microflowActionToGen had no case for
*microflows.JavaScriptActionCallAction, so it returned nil and the
ActionActivity was emitted without an Action. The read path and the
legacy engine both handled it, which is exactly why check/exec looked
green. Nanoflows reuse the same converter, so both flow kinds were hit
(the RSS-reader build tripped it via a nanoflow).

Add the JavaScriptActionCallAction case, built directly to mirror the
legacy serializer — including the JS-specific storage keys
(JavaScriptAction/OutputVariableName) and the "ParameterValue" parameter
key (not the Java-style "Value").

Reported from the RSS-reader build (finding #11).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Re-applying a byte-for-byte identical `create or modify persistent
entity` NULLed every value in the affected columns on the next runtime
DB sync — rows survived, values gone. The op reported success and
`mx check` passed; the loss only surfaced when something read the data.
On a real domain model this blanked 11 feeds + 98 articles.

execCreateEntity minted a fresh attribute ID (GUID) for every attribute
on each run, even unchanged ones. Mendix's DB synchronizer keys off
attribute identity: a new ID for an unchanged attribute reads as
"attribute departed + new attribute added", so it drops and re-adds the
column, discarding the data. Only the entity's own ID was preserved.

On CREATE OR MODIFY of an existing entity, reuse each retained
attribute's existing ID (matched by name); only genuinely new attributes
get a fresh ID. attrNameToID then propagates the reused ID into
validation rules and indexes, so those stay stable too. An identical
re-run is now identity-idempotent — no spurious sync, no data loss.

Also steer the "already exists in project" duplicate hint toward
`alter entity … add attribute` for entities (the non-destructive path).

Includes fix-issue.md symptom rows for this and the two preceding
high-severity fixes (ALTER PAGE case-insensitivity, JS-action
serialization).

Reported from the RSS-reader build (finding #13).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
A parameter name accepted bare by `call microflow M.Foo(Source = $x)`
was a parse error in a widget's OnClick/Action argument list:

  container r (OnClick: microflow M.Foo(Source: $x))
    -> no viable alternative at input 'OnClick'

The affected names — View, Source, Item, Page, Entity — are all lexer
keywords. The widget-argument name rule (microflowArgV3) accepted only
(IDENTIFIER | QUOTED_IDENTIFIER), while call-microflow's callArgument
used the permissive identifierOrKeyword. Same identifier, two grammars:
keyword-named params failed the widget rule but passed call-microflow.
The misattributed error also anchored on OnClick (a valid property)
rather than the offending token, which made it very hard to diagnose.

Widen microflowArgV3 to identifierOrKeyword (matching callArgument) and
read the name via identifierOrKeywordText in the visitor. Keyword-named
params now parse unquoted in widget action args, and the wrong-token
error position disappears for these names.

Reported from the RSS-reader build (findings #2 and #3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…DGET13)

A widget expression property (dynamicclasses / visibleif / editableif)
that walks an association passed `mxcli check --references` and then
failed the build with CE0117 "Error(s) in expression." — Mendix
client-side expressions cannot follow associations. This is easy to trip
over because a data binding (contentparams) on the same widget may
traverse the same association legitimately.

Add MDL-WIDGET13: scan the expression-typed widget properties for a
module-qualified association step between slashes (`/Module.Assoc/`). A
plain attribute (`$obj/Slug`) or a qualified enum literal
(`Mod.Enum.Value`, no leading slash) does not match, so data bindings and
enum comparisons are not flagged. The message points at the offending
step and suggests denormalising onto the bound entity or using a data
binding.

Reported from the RSS-reader build (finding #4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Three checker hints steered authors the wrong way:

- MDL001 (nested loop) recommended `retrieve $Match from $List where …
  limit 1`, which is a parse error — a plain retrieve cannot filter a
  list variable. Recommend `$Match = FIND($List, <cond>)` (in-memory,
  O(N)). Same broken advice fixed in CLAUDE.md idiom #2.
- MDL044 flagged `count()` used in an expression with "Did you mean
  'round()'?" — count is an aggregate activity, not a typo. For the known
  aggregates (count/sum/average/minimum/maximum) emit "assign it to a
  variable first: $n = count($List);" instead of a did-you-mean.
- MDL044's generic hint cited `mxcli syntax expressions`, which does not
  exist; point it at `mxcli syntax microflow`.

Also lock in finding #5's second trigger: a microflow-call output
variable reused across a fallback chain (try A, else try B) is CE0111.
It was already caught by the shared create-output-variable check
(`check --references`); add TestValidateDuplicateMicroflowCallOutputVar
(same-scope + nested-in-if) as a regression guard and document the
correct pattern (one variable per call + a plain set) in
write-microflows.md.

Reported from the RSS-reader build (findings #7, #8, #14c, #5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`placeholder:` and `onchange:` on a textbox passed check (with an
MDL-WIDGET07 "silently dropped" warning) and then vanished — DESCRIBE
showed neither. The design's live-search box degraded to a button and
the field lost its hint text. Both are standard Mendix TextBox
properties.

The pages.TextBox model already carried Placeholder/OnChangeAction and
the modelsdk (default) engine's widgetToGen already serialized them
(mirroring TextArea/DatePicker/CheckBox) — but the grammar had no
onchange:/placeholder: property, buildTextBoxV3 never populated the
model, the legacy serializeTextBox hardcoded empty values, and the two
keys weren't in the known-props list.

- Grammar: add `ONCHANGE COLON actionExprV3` + `PLACEHOLDER COLON
  STRING_LITERAL` to widgetPropertyV3 (the tokens already existed).
- Visitor: store Properties["OnChange"]/["Placeholder"]; add
  GetOnChange()/GetPlaceholder() accessors.
- Builder: buildTextBoxV3 sets tb.Placeholder (a *model.Text) and
  tb.OnChangeAction (via buildClientActionV3, reusing the button path).
- Legacy engine: serializeTextBox emits serializePlaceholderTemplate +
  serializeClientAction from the model instead of empties.
- Add OnChange/Placeholder to staticWidgetKnownProps so MDL-WIDGET07
  stops firing.

Reported from the RSS-reader build (finding #9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…ltiline-string hint

Three separate authoring papercuts from the RSS-reader build:

- #10 The CLAUDE.md/AGENTS.md template that `mxcli init` writes into every
  project contradicted the shipped skills: it listed `DECLARE $Entity
  Module.Entity;` and `DECLARE $List List of ... = empty;` as supported
  (both are rejected — MDL043/CE0053 and MDL040) and listed `CASE … END
  CASE` as NOT supported (it IS — the enum split). Move the two DECLARE
  forms to the NOT-supported table with the correct "use a parameter /
  retrieve / create" guidance, and list CASE as supported.

- #12 write-microflows.md documented the `$latestHttpResponse` body
  attribute as lowercase `content`; the real name is `Content` (inherited
  from System.HttpMessage, so DESCRIBE ENTITY System.HttpResponse doesn't
  list it). Lowercase fails CE0117. Fix both mentions and note why.

- #6 A string literal that runs off the end of its line got the
  "unescaped apostrophe — double your apostrophes" hint, which only makes
  it worse. Detect a newline inside the token-recognition-error payload
  and emit a distinct "string literals cannot span multiple lines" hint
  instead. The single-line apostrophe heuristic is unchanged.

Reported from the RSS-reader build (findings #10, #12, #6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…pying

`mxcli new` copied the full ~111MB binary into every project's ./mxcli
(gitignored, but a full duplicate per project). On Linux, prefer a hard
link: it shares the inode (no duplication on the same filesystem), is a
real ELF the devcontainer can exec, and survives the original binary
being moved or removed (unlike a symlink). Fall back to a full copy
across filesystems (EXDEV) or when linking isn't possible.

Reported from the RSS-reader build (finding #14a).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
A `call javascript action` / `call java action` with a wrong-cased or
misspelled parameter name passed `check --references` — the action itself
resolved — but wrote a dangling reference that only failed the Mendix
build with CE1613 "The selected … parameter … no longer exists". Example:
NanoflowCommons.OpenURL's parameter is `Url`, but `(url = $x)` slipped
through.

The reference checker resolved only the action NAME (the qualified-name
builders discard the parameter list) and never compared the call's written
argument names to the action's declared parameters.

Carry each call's argument names on the ref collector (codeActionCallRef);
after the name-exists check passes, read the action's declared parameters
(ReadJavaScriptActionByName / ReadJavaActionByName → Parameters[].Name)
and diff case-sensitively. A casing-only mismatch suggests the correct
spelling; System.* runtime actions are skipped, and the check degrades to
no error when the backend can't report parameters. References-mode only.

RSS-reader follow-up finding (verification note on #11).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`DESCRIBE PAGE` omitted a textbox's placeholder and onchange even though
both are written correctly (they are in the .mxunit and the live app
honours them) — so a DESCRIBE round-trip wasn't a reliable way to confirm
the write landed (verification note on the #9 fix).

The describe read path (parseRawWidget) only read LabelTemplate and
AttributeRef for a textbox; PlaceholderTemplate and OnChangeAction were
never read back. Add Placeholder/OnChange to the rawWidget describe model,
read the placeholder via extractTextFromTemplate (same as the label) and
the onchange via a key-agnostic renderClientActionMDL — refactored out of
extractButtonAction, since OnChangeAction is the same client-action type
as a button's Action, just under a different BSON key — and emit
`Placeholder:` / `OnChange:` in the TextBox case. DESCRIBE reads raw BSON
via GetRawUnit, so one path serves both engines.

RSS-reader follow-up (verification note on #9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`DESCRIBE PAGE` of a dynamictext bound to a non-String attribute
(Integer/DateTime/…) emitted the rendered template expression:

  ContentParams: [{1} = toString($currentObject/CountAll)]

Re-applying that made mxcli treat `toString($currentObject/CountAll)` as
an attribute NAME, failing the build with CE1613 "The selected attribute
'…toString($currentObject/CountAll)' no longer exists." A string binding
round-tripped fine (bare attribute name). On the real Reader page this hit
all six non-string bindings at once.

The write side converts a non-String attribute binding to
`toString($currentObject/Attr)` (dynamictext template parameters must be
String); the describe reader emitted that Expression verbatim instead of
reversing the transform.

Unwrap the exact auto-generated forms — `toString($currentObject/<path>)`
→ `<path>`, `toString($param/<attr>)` → `$param.attr` — in
extractClientTemplateParameters, so DESCRIBE round-trips (the write side
re-derives the toString on the next apply). Any other expression (extra
text, a hand-written toString) is left untouched.

RSS-reader follow-up finding (found verifying the #9/DESCRIBE work).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The `--trace` console exporter omits start/end timestamps and parent span
IDs, so call trees and durations can't be reconstructed from it — for
flame charts you had to hand-set three OTEL_* env vars
(OTEL_TRACES_EXPORTER=otlp, OTEL_EXPORTER_OTLP_PROTOCOL,
OTEL_EXPORTER_OTLP_ENDPOINT).

Add `mxcli run --local --trace-otlp <endpoint>` (implies --trace): it
switches the traces exporter to OTLP and points it at the collector
(protocol http/protobuf). User-set OTEL_* still take precedence, so a
fully hand-rolled env keeps working; without the flag the console
default is unchanged.

Reported from the sudoku project (OpenTelemetry tracing follow-up on the
console-exporter limitation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Plain `refresh catalog` leaves the analytic tables empty (0 rows, no
error); a query then silently returns nothing. `warnIfCatalogModeInsufficient`
already warns for the FULL-only views, but `xpath_expressions` was missing
from `fullOnlyTables` — so `SELECT … FROM CATALOG.XPATH_EXPRESSIONS` in
fast mode returned empty with no hint (activities/refs were covered, but
xpath was not).

Add `xpath_expressions` to `fullOnlyTables` so a fast-mode query against
it warns "requires refresh catalog full" like the sibling analytic tables.
TestFullOnlyTablesCoverage guards that every FULL-gated view
(activities/refs/xpath_expressions/widgets) is flagged and a fast-mode
table (entities) is not.

Reported from the sudoku "app warehouse" finding (#30) — the
REFRESH-CATALOG-FULL silent-empty papercut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The catalog is a plain SQLite database at <projectDir>/.mxcli/catalog.db —
exporting it to JSON first (as one might reach for) is strictly worse: it
loses most of the tables, adds human-readable chatter, and goes stale.
Document attaching it directly, plus an external DuckDB "app warehouse"
recipe that joins the catalog to the app's Postgres and the OTLP trace
dump read-only for cross-source questions no single source can answer
(mxcli does not embed DuckDB — this is a dev-container recipe).

Also flag, in both the use-cases page and the REFRESH CATALOG page, that
FULL mode is what populates the analytic tables (activities/refs/
xpath_expressions/widgets) — plain refresh leaves them empty.

Reported from the sudoku "app warehouse" finding (#30).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…alog)

Analyzing an app's runtime behavior is a recurring procedure spanning four
signals — the runtime log, Prometheus metrics, OpenTelemetry traces, and
the catalog (model shape) — so it belongs in a skill, not scattered across
feature docs.

Add `.claude/skills/mendix/analyze-runtime.md`: when to reach for each
signal, how to collect it (`run --local` + `--metrics` / `--trace` /
`--trace-otlp`, `refresh catalog full`), the cross-source "app warehouse"
join (external DuckDB — mxcli does not embed it), the logs↔traces gap, and
a decision guide mapping questions to tools. Registered in the skills
README (Specialized Skills + loading guide); synced into user projects by
`mxcli init`.

Trim the docs-site catalog "app warehouse" block to the catalog-attach
feature note plus a pointer to the skill, so the full procedure has a
single canonical home.

Follow-up to the sudoku "app warehouse" finding (#30).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
run --local: --trace-otlp to export traces to an OTLP collector
@ako
ako merged commit 2c0f683 into mendixlabs:main Jul 27, 2026
4 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.

2 participants