Skip to content

Sync ako/mxcli: two pluggable-widget defects, a stdlib toolchain bump, and a corrected parser claim - #889

Merged
ako merged 9 commits into
mendixlabs:mainfrom
ako:main
Aug 14, 2026
Merged

Sync ako/mxcli: two pluggable-widget defects, a stdlib toolchain bump, and a corrected parser claim#889
ako merged 9 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Syncs 6 commits from ako/mxcli:main. Upstream is not ahead, so this fast-forwards.

Pluggable widgets — two independent defects on the same path

A conditionally shown property wrote a widget Mendix rejects (CE0463) (c8a483d). On ProgressCircle, both showLabel: true and labelType: 'percentage' produced it; showLabel: false and the same widget's General-group properties were clean. Three lines of MDL reproduce it — no DESCRIBE involved.

Two gaps, and closing either alone inverts the bug rather than fixing it:

  • SerializationWidget property conditional visibility from editorConfig.js (CE0463 on VideoPlayer + Timeline) #574 nulls the TextTemplate of a hidden conditional property but left a visible one null, where Mendix stores an empty Forms$ClientTemplate. Null and empty are each invalid in the other's state.
  • Extraction — the editorConfig reader did not understand a ternary's else branch (cond ? (…) : hidePropertiesIn([…])), so showLabel's gate was never seen and labelText read as visible whenever labelType was "text", its default.

Filling visible templates without the missing gate simply moved which case failed. Both directions are measured, before and after. Only conditional properties are filled: Studio Pro's convention for an unset TextTemplate is not uniform — a DataGrid custom-content column stores null for tooltip and an empty template for exportValue — so filling every unset one would trade this bug for its mirror image.

A page carrying a pluggable widget could not be described and re-applied (9d0ac46). mxcli check on the description failed from the first widget onward with extraneous input ':', taking such pages out of the DESCRIBE-edit-CREATE OR REPLACE workflow mxcli documents as the way to change an element you did not author — and a widget has no other route.

  • Emit — explicit properties went out through a raw %s, so every string lost its quotes; a JSON spec then broke the parse at its first brace.
  • ReadextractExplicitProperties skipped any value of "true"/"false" as a "common default". A widget's default may be either, the document only stores what the author set, so the property was simply gone.

Fixed together because fixing one is worse than fixing neither: quote without emitting booleans and the description re-parses cleanly while silently dropping a property — a wrong page that validates. Quoting is decided by the declared type, never the value's shape: a String holding "30" is indistinguishable from an Integer once it's a string in BSON.

Both were diagnosed by handing the failing project to Mendix's own mx update-widgets on a copy and diffing against that reference — which named one meaningful path out of 969.

Build

  • Go toolchain 1.26.5 → 1.26.6 (cad70cc). govulncheck ./... began failing with exit 3 on every push with nothing in mxcli changed: all six findings are stdlib advisories Found in: <pkg>@go1.26.5 / Fixed in: @go1.26.6, published between the last green run and the first red one. govulncheck@latest re-resolves the database each run, so the pinned toolchain went stale under an untouched workflow.
  • Regenerate LSP completions for the FIRST keyword (64935d4). The import-mapping Range work added FIRST to the lexer but the committed completion list was never refreshed, so make build left a clean checkout dirty.

Correctness of the record

  • A false claim about how Studio Pro stores a retrieve Range (8991344). parseRange carried the comment "Studio Pro stores custom ranges as ConstantRange with LimitExpression/OffsetExpression", plus a branch acting on it. Measured against a real document on 11.13.0, one retrieve per UI option: AllConstantRange {SingleObject: false}, FirstConstantRange {SingleObject: true}, Custom(limit 4, offset 2)CustomRange {LimitExpression, OffsetExpression}. LimitExpression never appears on a ConstantRange, so the branch had never fired.
  • Proposal: derive container Size from contents (cd2b01e, docs only). A LoopedActivity's Size is computed from a pre-pass over the AST before its body is built, so it's a function of statement count alone and children's real positions — @position included — have no effect on the box meant to contain them. Measured on 11.6.6: children at x=150/310, x=1500/2000 and x=160/170 all yield Size 480;160; only the statement count moves it. In the x=1500/2000 case both children sit entirely outside their container and mx check reports nothing.

claude and others added 9 commits August 13, 2026 21:43
…roblem 1)

A LoopedActivity's Size is computed from a pre-pass over the AST before
its body is built, so it is a function of statement count alone and the
children's real positions -- @position included -- have no effect on the
box meant to contain them.

Measured on 11.6.6: two activities at x=150/310, at x=1500/2000, and at
x=160/170 all yield Size 480;160; only changing the statement count to
four moves it (800;160). In the x=1500/2000 case both children sit
entirely outside their own container and mx check reports no additional
error, so nothing catches it short of opening the flow in Studio Pro.

The fix is not a one-liner: children are placed relative to an inner
origin derived from the size, so deriving the size from the children
closes a cycle. Proposes build-first / size-after / translate-once, with
the translation as a single post-pass over the nested builder's objects
-- the same single-choke-point shape used for @Curve and @merge.

Includes the four-case repro as a bug-test example, and notes the two
consequences worth a release note: every flow containing a loop changes
geometry once, and under ADR-0008 that means one round of writes users
did not author.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
…etrieve Range

parseRange carried the comment "Studio Pro stores custom ranges as ConstantRange
with LimitExpression/OffsetExpression", and a branch acting on it. Measured
against a real document — ako/TestApp MyFirstModule.RetrieveExamples on Mendix
11.13.0, three retrieves, one per UI option:

  All                       ConstantRange {SingleObject: false}
  First                     ConstantRange {SingleObject: true}
  Custom (limit 4, off 2)   CustomRange   {LimitExpression: "4", OffsetExpression: "2"}

LimitExpression never appears on a ConstantRange. Studio Pro stores exactly what
generated/metamodel and modelsdk/gen declare, so the branch has never fired.

The comment was not harmless. The two engines differ on that input — modelsdk's
rangeFromGen cannot read it at all, because gen binds only SingleObject on
ConstantRange — so a reader trusting the comment concludes the default engine
silently drops a bounded retrieve's limit. It does not. The claim nearly became
a bug report against correct code.

The branch is kept, because one version's evidence justifies correcting a false
claim but not deleting tolerance for pre-11 formats that have not been sampled.
It is now labelled as unobserved, with the asymmetry and its consequences
recorded: if such a document ever appears the fix belongs in gen, not here.

Tests pin the three measured shapes, and name the tolerance case so a future
discovery lands somewhere with context. Also verified, on both engines, that a
describe -> exec round trip reproduces all three byte-for-byte — DESCRIBE
renders First as `limit 1`, which maps back to ConstantRange{SingleObject:true}
rather than a CustomRange, so the object-vs-list distinction survives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUToAkUx54bNkNjsBpufRH
The import-mapping Range work (mendixlabs#881) added FIRST to the lexer but the
committed generated completion list was never refreshed, so `make build`
left the tree dirty on a clean checkout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
The `Vulnerability scan` step (govulncheck ./...) started failing with
exit 3 on every push. Nothing in mxcli changed: all six findings are
standard-library advisories reported as `Found in: <pkg>@go1.26.5` /
`Fixed in: <pkg>@go1.26.6`, and go1.26.6 was published between the last
green run and the first red one. govulncheck@latest re-resolves the vuln
database on each run, so the pinned toolchain went stale underneath a
workflow that had not been touched.

  GO-2026-6218  net/url        quadratic complexity in resolvePath
  GO-2026-6090  crypto/tls     unbounded post-handshake messages
  GO-2026-6089  net/http       ReadHeaderTimeout on the h2c check
  GO-2026-6088  encoding/xml   missing recursion depth guard
  GO-2026-5972  encoding/asn1  missing recursion depth limit
  GO-2026-5026  net/http       idna punycode label rejection

Bumps every pin together -- go.mod's toolchain plus push-test, release
and nightly (two jobs) -- so a release binary is not still linked against
the vulnerable standard library after CI goes green. Same treatment as
the 1.26.4 -> 1.26.5 bump for GO-2026-5856.

Verified by running the scan under both toolchains: go1.26.5 reports the
six above and exits 3, go1.26.6 reports "No vulnerabilities found" and
exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
A page carrying a pluggable widget could not be described and re-applied:
`mxcli check` on the description failed from the first widget onward with
`extraneous input ':'`. That takes any such page out of the
DESCRIBE-edit-CREATE OR REPLACE workflow mxcli itself documents as the way
to change an element you did not author — and a widget has no other route,
having never been hand-authorable in Studio Pro terms.

Two independent defects in the same path:

  - Emit. Explicit properties went out through a raw %s, so every string
    lost its quotes. A JSON spec then broke the parse at its first brace.
  - Read. extractExplicitProperties skipped any value of "true"/"false" as
    a "common default". A widget's default may be either, the document
    only stores what the author set, and the property was simply gone.

Both are fixed together because fixing one is worse than fixing neither:
quote without emitting booleans and the description re-parses cleanly
while silently dropping a property — a wrong page that validates.

Quoting is decided by the DECLARED type, never the value's shape. Each
property's ValueType.Type is already in the widget's
Type.ObjectType.PropertyTypes, the array buildPropertyTypeKeyMap walks for
PropertyKey and throws away; a String holding "30" is indistinguishable
from an Integer once it is a string in BSON, and has to come back quoted.
Where no type is declared the shape is all that is left, and the fallback
quotes anything not plainly numeric or boolean — an unquoted arbitrary
string may not parse at all, while a quoted literal still round-trips.

Verified end to end on 11.12.1, which is the test rather than the output:
describe -> check -> exec -> describe is byte-identical, and mx check
reports 0 errors. Before the fix the first step did not parse.

Uncovered while verifying and NOT fixed here, because it is a write-path
defect with no DESCRIBE involved: giving a property in a conditionally
shown group a non-default value writes a widget Mendix rejects with
CE0463. On ProgressCircle both `showLabel: true` and
`labelType: 'percentage'` do it, while the same widget's General-group
properties take non-default values happily. The example keeps that group
at its defaults so it isolates the round trip; the symptom table records
the repro.

Reported in mxcli-ledger FINDINGS #104.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Authoring a pluggable widget property that lives in a conditionally shown
group wrote a widget Mendix rejects with CE0463. On ProgressCircle both
`showLabel: true` and `labelType: 'percentage'` did it; `showLabel: false`
and the same widget's General-group properties were clean. No DESCRIBE
involved — three lines of MDL reproduce it.

Two gaps on the same axis, and closing one alone inverts the bug:

  - Serialization. mendixlabs#574 nulls the TextTemplate of a HIDDEN conditional
    property, but left a VISIBLE one null, where Mendix stores an empty
    Forms$ClientTemplate. Null and empty are each invalid in the other's
    state, so nulling hidden ones was only half the rule.
  - Extraction. The editorConfig reader did not understand a ternary's
    ELSE branch — `cond ? (…) : hidePropertiesIn([…])` — so ProgressCircle's
    `showLabel` gate was never seen and labelText read as visible whenever
    labelType was "text", its default.

Filling visible templates without the missing gate simply moved which
case failed: `showLabel: false` began failing where `true` had. Both are
measured, both ways round, before and after.

Only CONDITIONAL properties are filled. Studio Pro's convention for an
unset TextTemplate is not uniform — a DataGrid custom-content column
stores null for `tooltip` and an empty template for `exportValue`, which
is what emptyClientTemplateRules exists for — so filling every unset one
would trade this bug for its mirror image.

Diagnosis followed .claude/skills/diagnose-ce0463.md. Handing the failing
project to Mendix's own `mx update-widgets` on a copy produced the
reference, and diffing it against mxcli's output named one meaningful path
out of 969: Object/Properties[N]/Value/TextTemplate, null against a
Forms$ClientTemplate. Authoring the same widget with the boolean both ways
gave two documents differing in exactly one path, so nothing else needed
eliminating.

The extractor's preamble is why the first attempt read as "unsupported
shape": the ternary follows a whole switch statement, and walking back
past the `?` to the start of the function hands the guard parser a
fragment with an unbalanced `}`. trailingExpr bounds it to the statement.

Verified on 11.12.1: both showLabel states 0 errors, `showLabel: true`
with `labelType: 'percentage'` 0 errors, and the DESCRIBE round trip of a
widget carrying the boolean parses, re-executes and re-describes
byte-identically. The widgetdemo showcase applies with 0 CE0463 (its 4
CE1613 are a pre-existing attribute reference in the example).

Reported in mxcli-ledger FINDINGS #104 follow-on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Fix red CI (Go 1.26.6), propose container auto-sizing for mendixlabs#884, regenerate LSP completions
Document Range parsing with measured Studio Pro shapes
Pluggable widgets: round-trip through DESCRIBE, and write a valid one when a conditional property is shown
@ako
ako merged commit 67fbf1a into mendixlabs:main Aug 14, 2026
2 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