Fifty-five changes: pluggable widgets become first-class MDL, a security command that never ran, and a run of check false positives only a real project exposed - #1052
Merged
Conversation
A pluggable widget's object lists and child slots are reachable from MDL only when their keyword is in a hand-maintained list of nine, while `mxcli widget init` derives documentation for all of them mechanically. Two lists, nothing comparing them. Measured on the fixture project's 33 widget defs: of 46 documented constructs, 16 parse and 30 do not — object lists 13/16, child slots 3/30. 20 of 33 widgets document at least one keyword that cannot parse. The generated htmlelement.md fails on the first line of its own example. The cost in the report was not the wasted sessions: unable to express the HTML Element `attributes` list, the team fell back to `tagContentHTML`, which executes same-origin — the risk the sandbox was for. `ALTER PAGE` rejects the same construct (verified with a working control), so honest documentation alone would have left them shipping that fallback. What makes the fix small is that the write path is ALREADY def-driven: applyObjectLists matches the AST child's Type string against whatever the def declares, and the visitor sets that string from the token's literal text. The nine keywords exist only so ANTLR has a token to match — an artefact of the parser generator leaking out as a capability boundary. Two slices. Slice 1 ships independently: the four built-in defs that exist nowhere (events, fileuploader, googletag, markdown — all pass check and fail exec), an error message that currently names a remedy which provably cannot work, the missing name in generated examples, and `()` being a parse error on every widget kind. Slice 2 is the grammar change, contained to a body rule the two pluggable alternatives use. Open question 1 could sink slice 2 and is written as such: the tradeoff moves a typo from a parse error to a semantic one, which is only an improvement if the validator fires everywhere the parser did — including with no --project, where only built-in defs load. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
Revises the proposal after a design review asked whether it aligns
widgets with the other MDL extension points. It did not — it fixed one
of four gaps.
The call forms are uniform: CALL MICROFLOW / NANOFLOW / JAVA ACTION /
JAVASCRIPT ACTION all take a qualified name and named arguments. A widget
matches none of it, and the previous draft only addressed the body.
Three gaps added, each measured:
- The widget KEYWORD has the identical defect to its body, one level up.
Every def.json declares an mdlName and WidgetRegistry.Get(mdlName)
exists; cmd_pages_builder_v3.go:425 already tries it FIRST ("Try by
MDL name first"). Only the grammar will not produce the name, so
`htmlelement h (...)` is rejected while `combobox c (...)` parses.
MDLName is currently read only to build error messages.
- There is no DESCRIBE WIDGET in MDL, only a CLI command. That reframes
the whole bug: actions need no generated docs because DESCRIBE answers
in-language, so the widget .md is a workaround for a missing statement
— and that is why it could drift. Fixing the generator treats the
symptom.
- CATALOG.REFS has 15 edge kinds and none is widget use, so "which pages
use HTMLElement?" is unanswerable while the same question about a Java
action is one query. Same class as the scheduled-event gap in CLAUDE.md.
Also answers the rename question. PLUGGABLEWIDGET and CUSTOMWIDGET already
take the same branch and write the same CustomWidgets$CustomWidget, so the
real win is collapsing them rather than the shorter name. Sequenced LAST:
once the keyword form works and DESCRIBE emits it, the ID form is an escape
hatch that is rarely written, which is what makes this a readability change
rather than a headline. The cost is stated rather than left to be found —
WIDGET already means "the widget named X" in ALTER/DESCRIBE STYLING.
Slice 1 is unchanged and still ships independently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…ames Slice 1 of PROPOSAL_def_driven_widget_bodies.md — the parts of mendixlabs#1036 that need no design work. Three independent defects, all found while reproducing the report. `no definition for widget` always said "run 'mxcli widget init -p app.mpr'". Studio Pro BUNDLES File Uploader, Events, Google Tag and Markdown viewer rather than installing them, so their .mpk never reaches widgets/ — the only place widget init scans — and re-running it can never help. Reported as the postscript to the issue, where it cost a debugging session. The message now branches on whether the package is really installed, using the same FindMPK lookup the template loader makes before giving up. `()` was a parse error on every widget kind, reported at the closing paren as though the widget were wrong, while bare `container c` and `container c (x: 'y')` both parsed. widgetPropertiesV3 required at least one property. `widget init` generated child slots with NO NAME (`tagcontentcontainer { … }`), which even a working slot rejects — so the three slots that did parse were documented in a form that could not. Names are emitted and numbered, since two `slot1`s on one page would collide. Item 1 of the slice — shipping the four missing widgets — is NOT here, and the proposal is corrected rather than left to mislead. Two things the draft had wrong: their .mpk files are not in Mendix.Modeler.Core.dll (690 embedded zips, zero widgets.mendix.com hits), and a .def.json alone is insufficient because getOrGenerateTemplate derives the template from the .mpk in widgets/ — shipping a def only moves the error to `template not found`, verified. All four ARE Mendix-published on the Marketplace, so genuine definitions are obtainable and must never be hand-written; the extraction was cross-checked against FileUploader's six known action slots. The control on the error message matters: the installed-package branch must still recommend widget init, or the test passes against a build that simply deleted the recommendation. Its first version was wrong — FindMPK PARSES each .mpk rather than matching names, so an empty file named after the widget is silently skipped — and the control caught it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…g widgets A review question — "could a Studio Pro user even use these without the mpk?" — turned out to invalidate the premise, so the proposal drops item 1 rather than deferring it, and the error message stops implying mxcli might one day ship a definition. Measured on Mendix 11.13: blank project (mx create-project) 33 widgets, none of the four install File Uploader (module 235351) widgets/ 33 -> 34, .mpk present exec a page using it, straight after BUILDS — no widget init needed So a widget whose package is absent is one Studio Pro cannot use either. It is not a gap mxcli can paper over, and there is nothing to ship: the moment the widget is usable at all, the .mpk is in the project and initPluggableEngine refreshes the definition from it unprompted. The error message now points at installing the widget instead of offering to have mxcli ship one, which was advice that would have led nowhere. Two dead ends preceded this, both recorded in the finding because both looked settled at the time: the .mpk files are NOT in Mendix.Modeler.Core.dll (690 embedded zips, zero widgets.mendix.com hits — the match was a bare ID string), and a .def.json alone is insufficient because getOrGenerateTemplate derives the template from the .mpk in widgets/, so shipping one only moves the error to `template not found`. Both were chased before anyone asked whether the widget was usable in Studio Pro at all — which is the cheaper question and settles the rest. Items 2-4 of the slice are unchanged and still shipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
A message definition's association cardinality was derived from the
direction of traversal alone: forward (the element's holder is the
association's FROM entity) stored MaxOccurs 1, reverse stored -1. That is
the right rule for a Reference and the wrong one for a ReferenceSet, which
is many at both ends — so a set reached forward was exposed as a single
object.
forward (holder is FROM) reverse
Reference 1 -1
ReferenceSet -1 -1
Unlike the direction half of the rule, this half has a build error behind
it. Measured on ako/mxcli-rest's RestLab at Mendix 11.13.0, against a
0-error baseline, the forward traversal of RateSnapshot_ExchangeRate gave:
[error] [CE6524] "... The occurrence of 'RestLab.RateSnapshot_ExchangeRate'
has changed" at Entity message definition 'RateSnapshot'
[error] [CE0295] "Association 'RestLab.RateSnapshot_ExchangeRate' is not
allowed." at Object mapping element 'Rates'
and 0 errors after the fix — definition and export mapping both, which is
the nested lane that report said could not be built. The control is the
same run with the four-line branch stubbed out, which reproduces exactly
those two errors, and the Reference beside it that stays a single object in
either build.
The direction rule was measured on a corpus that is exceptionless and still
incomplete: all 927 resolvable associations in it are Reference, 526
storing 1 and 401 storing -1. That pins the direction half and says nothing
about the type half, because the corpus never varied the input the rule was
declared independent of. The proposal that recorded the census now carries
that correction, and the skill and site references state both halves.
ako/mxcli-rest FINDINGS #60
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Slice 4 of PROPOSAL_def_driven_widget_bodies.md, and the slice that actually retires the failure mode behind mendixlabs#1036. A widget was the only MDL extension point with no DESCRIBE. A microflow, nanoflow, Java action and JavaScript action all answer in-language against the live project; a widget needed the CLI. That gap is WHY `mxcli widget init` generates markdown documentation at all — and why that documentation could drift from what the parser accepts. Fixing the generator treats the symptom; giving the widget a DESCRIBE removes the reason the generated file was the only answer. The description builder moves out of cmd/ into the executor, and both the statement and `mxcli widget describe` call it. One code path on purpose: two implementations would reopen exactly the drift this closes. cmd already imports executor, so the dependency direction was already right. DESCRIBE WIDGET works with NO project open, joining DESCRIBE FRAGMENT in the execDescribe exemption. A widget definition is not a document in the model — it comes from a package, or from mxcli's embedded set — and requiring a project would make the statement useless for the "what can I write here?" question it exists to answer. With a project the answer is better: the installed .mpk is version-accurate and is the only place a Marketplace widget appears. LIST WIDGETS is deliberately NOT added. `show/list widgets` already means widget INSTANCES on pages, and definitions are already listable as `select * from CATALOG.WIDGET_DEFINITIONS` — which the proposal's slice 4 had not accounted for, so the slice is smaller than written. The refactor is verified behaviour-preserving by byte-comparing `mxcli widget describe htmlelement -p …` before and after: identical, 34 lines. Getting that comparison right took two attempts — `git stash -- <path>` silently no-ops on an untracked file, and a fresh worktree cannot build because the generated embed dirs are absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…thorable Answers whether an agent can use DESCRIBE instead of the generated .md. Measured, it could not quite: the .md carried child slots and object lists, which DESCRIBE did not, so the two artifacts each held half the picture. DESCRIBE now reports both, with each object list's item properties — and marks which containers MDL can actually express today, which the .md never said and is the reason it misled. On a stock project 30 of 46 documented containers do not parse. Authorability is DERIVED by parsing a probe through visitor.Build, never from a list. A list here would be the same defect the proposal is about, one layer up — and deriving it means the marks correct themselves when slices 2-3 land, with no change to this code. The control is inside one widget: Gallery's `template` and `filter` come back authorable while its `emptyplaceholder` does not. Without a positive case, "NOT authorable" everywhere is equally consistent with a probe that always fails, which is the specific risk of deriving instead of stating. Gallery is also an embedded definition, so the test needs no project and cannot skip. The first version pointed at the fixture project and skipped every run — the fixture has no extracted defs — which is the trap mendixlabs#808 recorded: a test that only ever skips proves nothing. Caught by reading the run output rather than the exit code. The remaining gap to the .md is the MDL example, and that is the part that is wrong for most widgets, so it is not worth porting until slices 2-3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…n exposes
`drop association` reported success while a message definition still exposed
the association, naming neither the collection nor the definition. mxbuild
caught it as CE1613 "The selected association 'X' no longer exists." at the
definition, and `describe` went on emitting the dangling member, so a
describe -> exec round trip carried the break forward.
A definition is a selection over the domain model held by qualified name, with
nothing keeping the two in step. The drop already reconciles the entity access
rules that named the association; this one cannot be reconciled, because
removing the member changes a published contract and that is the author's
call. So it is refused — the same posture `drop message definition collection`
already takes from the other side.
The refusal prints the statement to run, rather than saying "remove the member
first", because the advice alone leads to a second wall: `drop member` matches
the member's ORIGINAL name (for an association, the target entity's) while an
`in` path segment matches the EXPOSED one, so the name written in the
definition is the one that fails. mxcli knows both. Verified by running the
printed text verbatim on ako/mxcli-rest's RestLab, after which the drop
succeeds.
Two things one example would have hidden, both covered:
- an association exposed in BOTH directions yields two definitions to clear,
and clearing one leaves the other dangling, so every hit is listed;
- the association that breaks is usually nested, so the whole element tree is
walked rather than the root's own children.
Scope is deliberate: after the guard, dropping the same association still
leaves CE1613 on a microflow retrieve and on an object mapping element. Those
are different consumers and are not covered here.
Controls: a collection that names a different association does not block the
drop, and the pre-guard binary reproduces the CE1613 at the definition.
ako/mxcli-rest FINDINGS #60
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
`alter page RestLab."RestLab_Home"` — no such page — passed
`mxcli check -p --references` and was then refused by `exec` with "page not
found". The switch in validateWithContext resolved the ALTER's MODULE and
never the document, so a misspelled module was reported and a misspelled
document was not, which is a distinction with no meaning to the author.
The finding that reported this called it harmless, and the wrong answer is.
The inverted contract is not: the check-syntax skill promises exec refuses
exactly what check rejects, and here exec was the stricter gate — so a script
passed every pre-flight and then stopped halfway through, with the statements
before the typo applied and none after.
Covers the ALTER statements whose target resolves from a listing that already
exists, each measured passing check and failing exec before it was added:
AlterPageStmt (PAGE, SNIPPET and LAYOUT — one statement type, three kinds,
set by the visitor from the keyword) and AlterEntityStmt. It is not extended
speculatively; a target that cannot be resolved is left alone, because a false
"not found" blocks a script that would have worked.
Two guards keep it from being worse than the silence it replaces, both with
controls:
- a module the SCRIPT creates has no listing to resolve against yet;
- an EMPTY listing means the backend could not answer, not that the project
has no pages.
Documents the script creates earlier count, which is the common
create-then-alter shape; `scriptContext` gained the `layouts` set it lacked.
The error offers the module's real names, since a typo is cheap to fix only
when you can see what is there:
page not found: RestLab.RestLab_Home (referenced by alter page)
— RestLab has CallLog_Detail, Home_Web, Mapped_Overview
Controls: the tests fail with the reported symptom when the validator is
stubbed, and 476 MDL examples still check clean.
ako/mxcli-rest FINDINGS #60
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…itten
The generated .md's "MDL Example" is the half of it that was WRONG — it
failed on its own first line. This is that section done so it cannot be.
Both halves are derived by probing the real parser through visitor.Build:
the head form (`gallery widget1` when the keyword is accepted, else
`pluggablewidget '<id>' widget1`) and each container. Whatever the grammar
cannot yet express is left OUT and NAMED, which is the information the
reporter's team never had — the .md silently included it, which is what
made it misleading rather than merely incomplete.
The whole example is then parsed before being emitted; if it does not
parse the caller is told rather than handed a broken snippet. So it cannot
promise syntax that fails, and it widens on its own when slices 2-3 land,
with no change to this code and no second list to keep in sync.
Two traps found writing it, both recorded:
- Numbering matters. Two child slots both named `slot1` PARSE — the
parser does not check names — but are invalid on one page. The .md
generator had the identical bug, fixed earlier in this branch.
- Required properties of type datasource/attribute/action/expression are
omitted and named, never filled in. An invented entity name parses
fine and then fails at exec, which is a worse failure than an honest
gap.
Verified by feeding each emitted example back through `mxcli check`:
htmlelement, gallery, accordion, datagrid and combobox all parse. The
tests assert the same round trip, plus that an unauthorable container is
omitted WHILE an authorable one is included — without that pairing,
"omits things" is indistinguishable from "omits everything".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…ration `alter enumeration Mod.E add value X` had no idempotency guard, so a script that adds an enumeration value was not re-runnable. The reportable harm is not the error message. It is that `exec` HALTS at the failing statement, so one already-present value silently truncates the rest of the script — the damage is in the statements that never ran, not in the one that failed. Measured by putting a guarded add after an unguarded one and looking for the second value afterwards: absent. A defensive drop-then-add is not a workaround, which is what forces a guard to exist rather than merely being convenient: the drop fails when the value is absent and the add when it is present, so neither ordering is re-runnable on its own. The same reasoning already put these guards on ALTER ENTITY's ADD ATTRIBUTE and ADD INDEX, and the grammar's `ifNotExists` / `ifExists` rules were already there to reuse. alter enumeration Mod.Lane add value if not exists Outbound caption 'Outbound'; alter enumeration Mod.Lane drop value if exists Retired; The bare forms are unchanged and still error; the ADD error now names the guard, so the fix is in the message rather than in the docs. Controls cover both: an unguarded duplicate is still refused, and the guard still writes a value that is genuinely new. One trap worth recording, since the first version of the example fell into it: `create or modify enumeration` REPLACES the value list rather than merging into it (an enum with three values, re-declared with one, keeps only that one). So a script whose CREATE precedes the ALTER resets the enum on each run and the ALTER re-adds instead of skipping — the example passed twice while never once exercising the guard. It now declares the value in the CREATE so the guard fires on every run, and says why. ako/mxcli-rest FINDINGS #60 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Part 1 of the plan for bindings. A widget's properties are "required" only where its editor shows them, and the example was using the required list raw: Combo box asked for ELEVEN bindings across options-source modes that are mutually exclusive. That is the .md's failure mode wearing different clothes — not syntax that fails, but work that is not needed. The rules to answer it were already in the output. They are now evaluated against the configuration the example itself describes, reusing types.WidgetVisibilityCondition.Hidden — the same predicate MDL-WIDGET10 uses, and with the same conservatism: an indeterminable condition does NOT prune, and a nested (object-list item) rule never prunes the widget's own property. Over-listing costs a moment; hiding a binding someone needs sends them to a build error. Eleven becomes six. It would become about two with full rule coverage — combobox reports "16 of 32 editor hide-rules recognized", and the two survivors with ZERO recognized rules (attributeEnumeration, ariaRequired) are hidden in Mendix for reasons the extraction does not capture. So the ceiling here is rule extraction, not the pruning, and that is its own piece of work. Also fixes a defect this uncovered, more general than the feature: the two description sources spell property types differently — a project .mpk gives "datasource", the embedded template "DataSource" — and the example's switch matched only lowercase. Described without a project, the example therefore emitted NO properties at all. That had been passing its test. "The example parses" is true of a bare head form, so the assertion was vacuous; the control that caught it asserts the example still ASKS for a binding nothing hides. The no-project limitation on pruning (no .mpk, so no editorConfig, so no rules) is now asserted rather than left to be rediscovered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…of its own Whether the def-driven body costs error quality. Answer: yes as written, and the fix is a prerequisite slice rather than a risk to accept. The question assumed the validator would have to match what the parser catches. Measured, it catches LESS than assumed, and the gap is already open: contaner c1 (…) parse error (the parser is the allow-list) pluggablewidget 'com.acme.NotAWidget' check PASSES; fails at exec group g1 (…) inside HTML Element check PASSES; fails at exec So widgetTypeV3 is currently the widget-kind validator, and the validator has no independent notion of a real widget kind because nothing else can parse. Slices 2-3 remove that enforcement, moving every container mistake into the hole the last two rows already occupy. But the detection is already computed. validateWidgetTreeIn holds the parent's declared object lists and looks the child up in them, and lookupWidgetDef says whether the kind is known; nothing REPORTS when both miss — the branch routes to validateStaticWidgetUnknownProps, which checks properties of a presumed static widget rather than questioning the kind. That reframes it. Closing the hole is an improvement today, independent of any grammar change: check would catch two mistakes that currently reach exec. And once check reports them, the question is answered by construction, because the semantic error exists before the parse error is given up. Adds Slice 0 — "the validator knows what a widget is" — which blocks slices 2-3. Its own control is that a correct widget and a correct container stay silent. Method worth reusing: to learn what a validator really catches when the grammar is doing the work, find an input that already bypasses the parser. `pluggablewidget '<unknown id>'` parses today and lands on exactly the path the generic form would create. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
Slice 0 of PROPOSAL_def_driven_widget_bodies.md, and the answer to its
Open Question 1. Two mistakes passed `mxcli check` and failed at `exec`:
pluggablewidget 'com.acme.NotAWidget' w1 -> MDL-WIDGET25
group g1 (…) inside HTML Element -> MDL-WIDGET26
The grammar was the widget-kind validator: widgetTypeV3 is an allow-list,
so an unknown kind could not parse and the validator never grew an
independent notion of one. Neither mistake is a keyword the parser checks,
and isUniversalObjectListKeyword actively SUPPRESSED the second by
treating a container keyword as always-an-item wherever it appeared.
Worth shipping on its own — check now catches what only exec caught. It is
also what makes slices 2-3 safe: those give up the parser's enforcement, so
the semantic check has to exist first.
Two guards keep this from being a false-positive storm, and a failing
control found each:
- LoadWidgetRegistry reads only .mxcli/widgets/*.def.json and, unlike the
page builder's registry, does NOT refresh from installed .mpk files. In
a project that never ran `widget init` it knows the nine embedded
widgets and nothing else, so an id whose .mpk IS installed is treated as
real — the same FindMPK lookup slice 1's error message uses.
- A container is never judged against a parent whose definition could not
be resolved, for the same reason.
isUniversalObjectListKeyword was a FOURTH incomplete keyword list — seven
against the grammar's nine, missing SCALECOLOR, CUSTOMBUTTON and
ALLOWEDFILEFORMAT. The replacement derives the set from the registry.
MDL-WIDGET23 was already taken by validate_widget_onclick.go; its own test
caught the collision, so these are 25 and 26.
Measured: both rules fire on the reported mistakes, and ZERO false
positives across the whole mdl-examples/doctype-tests/ corpus.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…l.mdl Two defects in slice 0 as pushed, both found by CI targets I had not run. MDL-WIDGET25 fired with NO project. The registry then holds only the nine embedded widgets, so every real project widget is "unknown" — one example file alone produced 14 violations, and `make check-mdl` broke seven. My measurement had used -p throughout, and `make check-mdl` deliberately runs check WITHOUT a project, which is the common case and the one I never tested. The guard I did write (is the .mpk installed?) cannot help there: with no project there is no widgets/ to look in. The rule now requires a project, scoped to the widget-id branch only — the container rule needs a resolvable PARENT rather than a project, and a blanket early return silenced it too. And the repro cannot be a .fail.mdl. Both rules need a project, so under `make check-mdl` the file passes and reports "negative test unexpectedly passed", making a working rule look regressed. The Makefile documents this exact case (mendixlabs#891, mendixlabs#892) and prescribes the fix: keep the repro a plain .mdl, cover the rule with unit tests. Renamed, with the reason in the file so the next person does not re-add the suffix. New control: no-project must claim nothing. That is the assertion my with-project measurement could not make, and it is what would have caught this before the push. Verified both ways now: `make check-mdl` exits 0 (no project), and with a project both rules fire on the repro with zero false positives across the doctype corpus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
A pluggable widget was the one MDL extension point with no edge in
CATALOG.REFS. "Which pages use this widget?" was unanswerable while the
same question about a Java action was one query — and it is the question
an upgrade asks: a .mpk in widgets/ that no page uses is dead weight, and
one used on forty pages is not something to swap lightly.
Both halves were already catalogued, so this is a projection and costs no
extra parse: widgets_data.WidgetType carries the widget ID for a pluggable
or custom widget, and widget_definitions_data is keyed by it.
buildWidgetDefinitions already runs before buildReferences.
Measured on testdata/expr-checker: 248 -> 263 refs, 14 PAGE and 1 SNIPPET
edge; IMAGE on 7 pages, DATAGRID 4, COMBOBOX 3, LANGUAGESELECTOR 1, and 38
of 42 installed definitions used by nothing.
TargetName is the MDL name, not the widget ID
---------------------------------------------
The obvious choice is wrong, and the measurement is the reason. With the
dotted widget ID as TargetName:
graph_module_coupling invents a module "com" carrying 14 edges from
three real modules
graph_god_nodes reports com.mendix.widget.web.image.Image with
ModuleName "com"
Those views take everything before the FIRST dot as the module — sound for
a qualified name, nonsense for a widget ID. The MDL name (IMAGE, COMBOBOX)
has no dot, so their existing instr(TargetName, '.') > 0 guard skips it and
graph_module_coupling comes out byte-identical to the baseline. It is also
the spelling a user has in hand after `describe widget combobox`. The ID is
not lost: it goes in TargetId, which is what that column is for.
That makes a widget the first non-dotted target in the table (measured: 0
of 248 existing rows). graph_god_nodes is amended to exclude WIDGET targets
from the asset side, since every other row there is a module-qualified
document and a widget would list with ObjectType NULL and its own name as
its module. A page's OUT-degree still counts the widgets it uses.
Only widgets that resolve to a definition
-----------------------------------------
The join is the filter. A built-in stores its BSON $Type in the same column
(Forms$DynamicText) and has no definition, so it gets no edge: an edge is a
pointer to something describable, and Forms$TextBox is a language primitive.
CATALOG.WIDGETS already answers "which pages have a text box".
DISTINCT collapses a page's seven comboboxes into one edge, matching the
four sibling projections. Per-instance rows would say nothing SHOW
REFERENCES or SHOW IMPACT could use.
Every test has a control, and each was run against the reverted fix:
dropping DISTINCT gives 3 edges where 1 is expected; reverting the
god_nodes exclusion lists 2 widgets as assets; the built-in test asserts
the pluggable widgets on that same page DO get edges, so "no built-in edge"
cannot pass by emitting nothing; and the TargetName test writes a dotted ID
into the same fixture to show the fake module does appear, so it asserts a
property of the choice rather than of the fixture.
The schema version is bumped because both changes are invisible to an
existing cache — refs are only written by REFRESH CATALOG FULL and a view
is CREATE VIEW IF NOT EXISTS — so without it a cached catalog answers `show
references to combobox` with "(no references found)".
Also corrects the REFS schema in docs-site, which described columns that do
not exist (SourceKind / TargetKind) and RefKind values in the wrong case
("Call", "DataSource"). Same drift as the STRINGS block in #386. Both new
query examples were run: the natural NOT EXISTS form does not parse in
MDL's SELECT subset, so the documented one is the anti-join that does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…n case `show references to combobox` — the spelling every MDL example uses, and the one a page body is written in — reported "(no references found)" while `show references to COMBOBOX` returned three pages. That is a WRONG answer, not a missing one: a user cannot tell it from the widget genuinely being unused, which is precisely the question the new `widget` edge exists to answer. The widget's TargetName is stored SHOUTED because widget_definitions_data holds MDL names that way, while MDL keywords are case-insensitive everywhere else in the language. SHOW REFERENCES TO and SHOW IMPACT OF match TargetName exactly. That was right while every target was a module-qualified name copied verbatim out of `show entities`; a widget is the first target a user types from memory. The fallback is deliberately second, never first: an exact match returns untouched, so no existing answer can change — only a query that would have returned nothing gets a second chance. It declines when the loose match is ambiguous, because two targets differing only in case are a question this cannot answer for the user, and it prints the spelling it matched so the result is not silently attributed to the name that was typed. Controls, run against a stubbed fallback: the three case-variant cases fail and the ambiguity test fails; the exact-match, genuine-typo and unknown-widget cases pass either way, which is the point — they assert that nothing else moved. Verified end to end on testdata/expr-checker that `show references to Administration.Account` is unchanged and prints no "(matched ...)" line, and that a real typo still reports nothing rather than being rewritten to a near neighbour. The two interpolated queries this touches now escape the target, which they did not before. The interpolation itself is pre-existing and not addressed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
Three details of the widget edge differ from the one-line sketch, each settled by measurement: one edge per container rather than per instance, the MDL name rather than the widget ID as TargetName (the dotted form invents a module called "com" in the module-derived graph views), and only widgets that resolve to a definition. Also records what was NOT done — a widget definition is not added to `objects`, so an unused .mpk does not surface in GRAPH_DEAD_ASSETS — and that the sketch's `show references to widget htmlelement` syntax was unnecessary, since `show references to <name>` already parses a bare word. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
`refresh catalog full` regenerates the per-widget skill docs when widget definitions change (executor.RegenerateWidgetDocs, deliberate since 1eda39c and reported in the command's own output). In this fixture that writes 43 markdown files nothing tracks, so running it against testdata/expr-checker leaves the tree dirty and invites someone to commit derived files. Scoped to `.claude/skills/widgets/` rather than `.claude/`, because the directory is only partly derived — `.claude/settings.local.json` already has its own line above, and commands or other skills placed there should still show up. Verified both directions: git check-ignore covers skills/widgets/combobox.md and SKILL.md, and does not cover .claude/skills/other.md or .claude/commands/. Measured: with the rule, a fresh `refresh catalog full` against the fixture leaves `git status` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
5 asked which source granularity a widget edge should use, and said to measure both on a real app. Slice 5 settled it as per (container, widget definition): per-instance rows say nothing SHOW REFERENCES or SHOW IMPACT can use — both list sources, and CATALOG.WIDGETS already holds the instances — so the extra rows buy nothing at any size. Records the question the draft did not ask and that turned out to matter instead: what goes in TargetName, where the dotted widget ID poisons every module-derived graph view. 6 asked whether the four scraped widget IDs were a complete census of Studio Pro's bundled widgets. Slice 1 established they are not bundled at all, so there is no list to complete. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
… order
`mxcli check` printed the same warnings in a different order from one run
to the next. Three validators — MDL-WIDGET07, WIDGET17 and WIDGET18 —
append one violation per property key while ranging over w.Properties
directly, and Go randomises map iteration.
Nothing was wrong with the diagnostics themselves, and no message changed.
What was wrong is that a before/after diff of `check` output is not usable
as a measurement, which is how this was found: sizing the grammar change
for slices 2-3 means diffing check output across mdl-examples/ before and
after, and two runs of the SAME binary disagreed on 11 of 515 scripts —
more noise than the change under test produced signal.
two runs, same binary, before 11 of 515 scripts differ
two runs, same binary, after 0 of 515
message SETS vs baseline unchanged on all 515
Sorted by property key, so the order is not merely stable but predictable.
Two other loops over w.Properties are deliberately left alone: they do a
case-insensitive LOOKUP and break on the first hit, so they are only
order-sensitive when a widget carries two keys differing solely in case,
and picking either is equally correct.
The regression test uses eight unknown properties — with unsorted
iteration the chance of Go returning the same order twice is 1/8! — and
asserts both stability across 50 calls and that the order is sorted. It
also asserts the fixture produces 8 violations, so it cannot pass by
producing none. Control: with the sort reverted, both tests fail on the
first comparison.
This is the checklist's "map iteration is deterministic" item; it is the
same defect class in a place nothing was watching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
… 2, 3) mendixlabs#1036. `mxcli widget init` generates widget documentation for agents to follow; fed back into `mxcli check` verbatim, the generated htmlelement.md failed on the first line of its own example: line 4:4 mismatched input 'tagcontentcontainer' expecting '}' The doc generator derived a keyword for every object list and child slot in a definition; the grammar accepted nine hardcoded ones. Two lists, nothing comparing them. 16 of 46 documented constructs parsed. It is 50 of 50 now. The cost was never the wasted sessions. Unable to express the `attributes` list, the reporter's team fell back to tagContentHTML, which executes same-origin — the exact risk the sandbox they wanted was for. Slices 2 and 3 land together because slice 2 alone is not a working state: it makes every name parse, which invalidates the way slice 4 derives whether a container is authorable, and three DESCRIBE WIDGET tests fail until slice 3 completes the picture. Splitting them would mean committing a red suite. The grammar is two alternatives, ordered last -------------------------------------------- `| IDENTIFIER` covers `htmlelement frame (...)`. `| keyword` covers a container whose name lexes as a keyword token — `attribute` is ATTRIBUTE and never IDENTIFIER, so slice 2 alone cannot reach the case that motivated the issue. No parallel body rule was needed after all: a container and a widget occupy the same position in a body. The write path was already def-driven — the page builder tries widgetRegistry.Get(ToUpper(type)) FIRST — so nothing about BSON changed. The validator is the load-bearing half, exactly as Open Question 1 warned ------------------------------------------------------------------------- Loosening a grammar moves a parse error into the validator, or it becomes a wrong answer. Measured, before that half existed: htmlelemnt frame (tagName: 'div') 0 errors, 1 warning (about `tagName`) htmlelement frame (tagName: 'div') completely clean So the AST now records WHICH alternative matched (WidgetV3.TypeIsGeneric), set from the parse tree in the visitor and never by comparing the type text against a list of known names — that would reintroduce the list this change removes. MDL-WIDGET25 gained a branch for it, MDL-WIDGET07 is suppressed for it, and inside a resolvable parent the message routes to MDL-WIDGET26, which names what the parent does declare: `attribut` is not a container of `htmlelement` — it declares: attribute, event, tagcontentcontainer, tagcontentrepeatcontainer Both rules still need a project, for slice 0's reason: the set of valid widget names IS the project's installed packages. With no -p a typo cannot be told from a widget mxcli has never seen, so it says nothing. That is a real cost of the change and it is stated in `mxcli syntax page.widget-any`. The measurement, and what it could not see ------------------------------------------ Open Question 2 asked how far the ambiguity reaches, and said to measure rather than argue. Diffing `mxcli check` across all 515 mdl-examples scripts: IDENTIFIER (slice 2) 0 verdict changes, 0 message changes + keyword (slice 3, 655 tokens) 0 verdict changes, 0 message changes The diff was necessary and NOT sufficient, which is the more useful result. SLOT, PLACEHOLDER and USE are all in `keyword`, and with widgetV3 ordered first in pageBodyV3 the generic alternative swallowed them: `slot body` became a widget of type `slot`, `placeholder Main { … }` a widget named Main. Both still parsed and still exited 0 — the damage is to the AST, not the diagnostics, so 515 scripts showed nothing and two visitor unit tests caught it at once. The specific alternatives now precede widgetV3, and a test says why so nobody reorders it for tidiness. Item 9 of the slice (delete the nine object-list keywords) is deferred, with the reason recorded in the proposal: they are redundancy rather than drift risk now, and removing them would flip nine keywords to the generic branch where a container inside an unresolvable parent would newly false-positive. Verified: full suite, `make lint`, `make check-mdl` (479 scripts, +1 new), `make check-skill-mdl`, `make check-findings`. Three findings appended, including the one worth reusing — a corpus diff of check output compares diagnostics and is blind to a construct that parses into the wrong shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…ilently
Item 6 of slice 2 in PROPOSAL_def_driven_widget_bodies.md, plus a defect
that item exposed.
DESCRIBE PAGE now emits `htmlelement frame (…)` rather than
`pluggablewidget 'com.mendix.widget.web.htmlelement.HTMLElement' frame (…)`,
which is the form a person would have written and only became possible when
slices 2-3 made the keyword parse.
It falls back to the id form rather than guessing. The bar is not "shorter"
but "rebuilds the SAME widget", and the guard is a round trip through the
lookup the builder itself uses: the registry is keyed BY MDL NAME, so two
definitions claiming one name leave Get and GetByWidgetID disagreeing while
All() cannot see the collision at all. Counting definitions would have been
dead code that looked like a guard.
The round trip was silently lossy
---------------------------------
Verifying the above on a page mxcli authored itself turned up worse:
written htmlelement frame { attribute a1 (…)
tagcontentcontainer body { dynamictext t (…) } }
stored BSON carries tagContentContainer WITH its DynamicText, and
attributes WITH the data — the write path is correct
described htmlelement frame (tagName: 'div', …) <- body gone
So describe -> edit -> exec deleted a widget's body and said nothing. That
is the mendixlabs#965 failure class, and slices 2-3 made it reachable by making the
construct writable in the first place.
Reconstructing those containers is its own piece of work. Silent data loss
is not acceptable in the meantime, so DESCRIBE now names the gap where the
body would have been — the same rule slice 4's usage example already
follows: say what was left out rather than look complete.
Two things had to be right for that note to be worth reading:
- getBsonArrayElements STRIPS the leading typed-array marker, so an empty
container is length 0 here and length 1 in the raw BSON. Checking the
raw length reports every widget as lossy.
- It warns on CHILD SLOTS HOLDING WIDGETS only. A widget template ships
DEFAULT entries in its object lists that are structurally identical to a
user's, and the first version duly named `event` on a page that never
wrote one. A note that fires on defaults is noise, and noise trains
people to ignore the notes that matter. A widget inside a slot has no
such ambiguity: a template never puts one there.
Measured both ways: 16 real pages in testdata/expr-checker produce 0 notes,
and the authored page produces exactly one, naming tagcontentcontainer.
Also fixes the wiring bug found on the way: GetWidgetRegistry passed
filepath.Dir(ctx.MprPath) where LoadWidgetRegistry wants the .mpr path
itself. A correct fallback hid it perfectly — output stayed valid, nothing
failed, no test went red.
Controls, each run against the reverted guard: forcing the id form fails the
name test; dropping only the WidgetID comparison fails the ambiguity test;
making the empty-container check never trigger reports `emptyslot` as lost.
Two findings appended.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
… dropping them
DESCRIBE PAGE now reads back both kinds of container on any pluggable
widget, so describe -> edit -> exec stops destroying a widget's body.
before htmlelement frame (tagName: 'div', …)
-- NOT SHOWN: tagcontentcontainer …
after htmlelement frame (tagName: 'div', …) {
attribute attribute1 (AttributeName: 'data-x', …)
event event1 (EventName: 'onClick', …)
tagcontentcontainer tagcontentcontainer1 {
dynamictext t (Content: 'hi')
container "inner" (Class: 'row') { … }
}
}
Measured end to end on a page mxcli authored: 4 widgets survive the round
trip, and `data-x` is still in the document. CONTROL: with the child-slot
extractor removed, the same round trip leaves 1 of 4 — describe -> exec
deletes three widgets.
Child slots: read from the document, not looked up by name
----------------------------------------------------------
DESCRIBE reconstructed a Gallery's `content` and `filtersPlaceholder` by
asking for those keys BY NAME, and had nothing generic. A child slot is any
property whose Value holds a Widgets array, so reading that shape off the
document makes a widget nobody has thought about round-trip for free.
Empty slots are skipped. getBsonArrayElements strips the leading typed-array
marker, so an empty container is length 0 here and length 1 in raw BSON;
emitting them would put a `slot { }` block on nearly every pluggable widget.
Object lists: the Action branch was eating every property
---------------------------------------------------------
`attributes` and `events` were dropped while a chart's `series` described
fine, which made it look widget-specific. It was not. extractObjectListItem
tests a value's fields in order, and the ACTION branch continued
unconditionally once `value["Action"]` merely EXISTED. A widget value
carries every field it could have, and Action is always present as a
Forms$NoAction — so it consumed all six sub-properties of an item, the item
ended with zero Props, and the caller's `len(item.Props) > 0` filter dropped
it, taking the whole list with it.
Isolated by reverting that one branch: object lists go 2 -> 0. DataSource
and AttributeRef are the same latent shape and are guarded the same way;
neither is load-bearing for the measured case, and the comment says so.
They consume the property when the sub-document is PRESENT AND NON-EMPTY,
not when it merely exists and not only when it renders — a datasource that
cannot be rendered must not fall through to the scalar branches, which would
describe it as its PrimitiveValue. That distinction was caught by a control
in my own first attempt, which let exactly that happen.
Two measurement lessons, both recorded as findings
--------------------------------------------------
My first control used a string replace with no assertion, matched nothing,
and was VACUOUS — it "passed" while changing no code. The second flipped
three conditions but missed that a `continue` had MOVED, so it reverted the
wrong thing and pointed at DataSource; only `git diff -U0` with comments
filtered out showed the moved line. A confident wrong root cause in a
comment would have been worse than none.
Also worth knowing: the round trip CONVERGES rather than being a fixed point
on the first pass — the writer emits an item's properties in a different
order than the original document, so describe #1 != #2 but #2 == #3.
Describe itself is deterministic (5 identical runs). And a fixed-point
assertion alone would not have caught the original bug, since an empty
describe is also a fixed point; the assertion has to be that the CONTENT
survives.
Verified: full suite, make lint, 479 MDL examples, check-findings. Describe
output for all 16 fixture pages is byte-identical to before, so nothing that
already worked moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
DESCRIBE PAGE upper-cased the first letter of every widget schema
sub-property key. That round-tripped — MDL property names are
case-insensitive, so `StaticName` and `staticName` both resolve — but it
made DESCRIBE PAGE the only surface using that spelling:
mxcli widget describe linechart staticName (read from the .mpk)
what you write in a page staticName
DESCRIBE PAGE, before StaticName
Three surfaces, two spellings, for no benefit. It was invisible while only
chart series reached this code; slice 3 put object lists on every widget,
which is what made it worth fixing.
Emitting the key verbatim also keeps a real distinction visible that
PascalCase erased. In a chart series both of these now read differently, and
should:
DataSource: database from ChartExamples.SalesByQuarter <- MDL's keyword
staticXAttribute: Period <- widget schema key
Measured: 9 of 26 pages in the fixture change, every one of them a page with
an object list (8 chart pages plus the HTML Element probe). Nothing else
moves. Round-tripped describe -> exec -> describe on the changed pages:
fixed point on HeatMap, Dashboard and Bubble, and the HTML Element's only
difference is the writer's own property-order convergence documented in the
previous commit.
Five tests asserted the PascalCase form and are updated. One of them,
TestObjectListMDLKey, gains an already-capitalised key so "verbatim" cannot
be mistaken for "lower-cased" — a widget is free to name a property `Foo`.
The two skills that teach this syntax are updated to match, since a skill
disagreeing with DESCRIBE is the same two-lists problem in documentation
form. Their `DataSource:` stays capitalised, which now illustrates the
distinction rather than hiding it. While there, one of them documented
`interpolation: linear | smooth`; the widget accepts `linear | spline`
(`describe widget linechart` reads it from the .mpk), so `smooth` was never
a valid value.
The example scripts in mdl-examples/ are deliberately NOT rewritten. They
are hand-authored and still correct — and keeping some in the other casing
is live evidence that case-insensitivity holds, which this change relies on.
NOT fixed here, found while round-tripping and pre-existing: exec of a
described ChartExamples.P_Line fails MDL-WIDGET08 because the stored page
carries `interpolation: 'smooth'`, which the widget does not accept. The
source example writes it at line 183. The pre-change binary fails
identically, so it is not this change; `make check-mdl` cannot see it
because that runs without a project and the enum values come from the
widget definition.
Verified: full suite, make lint, 479 MDL examples, make check-skill-mdl,
make check-findings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
`Interpolation: 'smooth'` is not one of the LineChart's values. The widget
accepts `{linear|spline}` — `mxcli widget describe linechart -p <project>`
prints the enum — and `spline` is the curved one the example meant.
Why CI never saw it: `make check-mdl` runs `check` WITHOUT a project, so no
widget definition loads and MDL-WIDGET08 has no enum to compare against. With
a project attached the file failed, and since exec refuses a script with
errors, it could not be run at all.
The file's `mx check`-CLEAN header claim is re-verified rather than assumed:
exec'd into a Mendix 11.6.6 project and run through `mxcli docker check`
(mx update-widgets + mx check) on mxbuild 11.6.6 -> "The app contains: 0
errors." The control that the check is not inert on that project: a view
entity with a deliberately wrong column type in the same app fails CE6770.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
… schema key
`ValueAttribute: Total` on a PieChart or HeatMap was reported as
widget `pieChart1` (piechart) has no property `ValueAttribute` [MDL-WIDGET01]
and, because exec refuses to run a script with errors, the page could not be
written at all. The value is not merely valid — it persists: the stored page
comes back from DESCRIBE as `seriesValueAttribute: Total`.
Root cause, measured on piechart.def.json:
{"propertyKey": "seriesValueAttribute", "source": "Attribute",
"operation": "attribute", "mdlAliases": ["ValueAttribute"]}
allowedWidgetProperties built its set from PropertyKey and Source only. Three
places read this field — the builder resolves through it (widget_engine.go),
the knownProperties set walks it (widget_defs.go), and the validator did not —
so the checker was the odd one out on the same def.json.
Tests carry their own controls: the schema key must STILL be allowed (a fix
that swapped one name for the other would pass the first assertion and break
DESCRIBE's own output), and the mode-scoped case uses an alias that is not a
case variant of its key, since `seriesName`/`SeriesName` collapses to one
entry once lowercased and would pass against the broken code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…mode Every chart series written the documented way drew series `sRegion` property `dynamicDataSource` is hidden when its own `dataSet` is "static" — the value will be ignored [MDL-WIDGET10] 11 of them on 34-chart-widget-examples.mdl, one per series, about a property no script in the file ever wrote. A chart's two datasource sub-properties SHARE the Source name "DataSource" (measured on linechart.def.json: staticDataSource and dynamicDataSource both declare it, neither declares an alias), so the item's dataSet mode is what selects between them. buildObjectListItem routes on exactly that — seriesDataSourceMatchesMode — and writes only the matching one. itemValueMap resolved by Source alone, so one friendly `DataSource:` marked both explicit and the hidden-property rule fired on the sibling. The gate is scoped to a chart series' datasource pair, which is the only place two item properties claim one Source; anything wider would blind MDL-WIDGET10 on every other object list. Three controls hold that line: the routing must follow `dynamic` as well as `static` (not just prefer static), a non-chart item property sharing a Source must still resolve, and an explicitly named `dynamicDataSource:` must survive the gate, because the builder honours the schema key before it consults the mode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…ment `check -p` reported, on a view entity whose source entity the same script creates: attribute 'Units': declared as Integer but OQL expression 'sum(s.Units)' returns Decimal. Fix: change to 'Units: Decimal' Following that hint breaks the build. Measured on mxbuild 11.6.6, two views over the same `sum(s.Units)` where Units is an Integer attribute: declared Integer -> 0 errors <- what mxcli flagged declared Decimal -> CE6770 <- what mxcli told the user to write So the diagnostic did not merely cry wolf; its Fix: walked a working project into a broken one. The control that makes those numbers mean something: a view declaring a String column over `sum(s.Amount)` in the same app DOES fail CE6770, so mxbuild really was validating view entities rather than ignoring them. inferAggregateType's SUM branch returned Decimal whenever the argument type could not be resolved — which is precisely when the source entity is created by the same script, since check skips references to script-created objects, i.e. the common shape for a view entity. inferTypeStatic's own SUM branch already says in as many words not to guess Decimal there; the project-aware path contradicted it. Now both return Unknown, and an unknown inferred type is skipped rather than asserted. Controls: sum() must still PROPAGATE a type it can resolve (or "return Unknown" degenerates into never checking sum() at all), and COUNT/AVG keep their own rules, so a fix that made every aggregate Unknown would be caught. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…0495 duplicate Three charts on one page, each with a series the author called `s`, failed: duplicate widget name 's' (used 3 times) — Mendix requires unique widget names per page (CE0495) mxbuild 11.6.6 reports 0 errors on that exact page. And the name is not stored at all: author `series sRegion (…)` and DESCRIBE returns `series series1 (…)`, because the stored WidgetObject carries no name and DESCRIBE has to synthesise one. A name the model does not hold cannot collide — which is the same reasoning that already excluded rows and columns in widgetKindsWithoutStoredNames, extended to object-list items. The container keywords are read from the parent's def.json `objectLists` rather than listed here. A keyword table would be the second list this line of work exists to remove, and it would drift the moment a widget ships a new one. That needs the registry threaded into validatePageContextTree; when there is none — `check` in CI runs with no project — nothing resolves, itemKinds is empty, and the rule behaves exactly as before. Controls, all of which fail if the guard is made broader than that: a genuine duplicate widget name is still reported; two widgets sharing a name inside a pluggable widget's CHILD SLOT (not an object list) are still reported; and with a nil registry a plain duplicate is still reported. Stubbing the guard fails the new test and leaves the other three green. The registry is built in memory for the test: no embedded widget definition declares an object list (0 of them), and the .def.json cache a project derives from its .mpk files is gitignored, so both alternatives would skip in CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
One record each for the .mpk/definition property split, the two-sided hide-rule disagreement, the placeholder item value, and the unwalked object-list items. Three of the four are the same shape as the PR they were found in: two readers of one fact, with nothing comparing them. The measurement notes are the part worth keeping — a stale catalog cache made a fixed binary look unfixed, and a growing unfiltered ref count looked like progress while the specific question still returned nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…t-dependent CI failed on 63d1614's own control: --- FAIL: TestGeneratedExamplesUseRealItemValues no widget has an authorable object list with item properties — nothing here would exercise the item literal The control was right to fire and the test was wrong to depend on the environment. .mxcli/widgets/*.def.json is derived and gitignored, so a fresh checkout has only the hand-crafted definitions in sdk/widgets/definitions/ — none of which declares an authorable object list with item properties. Locally the generated cache supplies 33 more, including the charts and DataGrid2 the test needs. Reproduced by moving the cache aside; the failure is identical. Worth naming: the neighbouring test's own comment already says the cache is gitignored and must not be depended on. I wrote that sentence and then wrote a control that depends on it. The end-to-end test now SKIPS when the environment cannot exercise it, saying why, and four hermetic tests carry the actual guarantee on a synthetic description: an enumeration sub-property is preferred over a free-text one and yields a real member; the DEFINITION beats the package (the PopupMenu case, where ParseMPKForWidget returns 0 children while the definition carries itemType's enumValues); an all-free-text container keeps the placeholder; and an empty container returns nothing rather than panicking. Control: with fix 63d1614 reverted, the first two fail and the other two pass — so they detect the defect rather than the shape of the code. The package suite is green both with the def cache present and with it moved aside, which is the condition CI runs in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
A test green locally and red in CI, because it read a derived .def.json cache that is gitignored. The reproduction is the useful part: move the artifact aside and the CI failure appears exactly, with no push needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
Slice 4 added a statement and every sibling DESCRIBE had a reference page — entity, association, enumeration, microflow, page — while this one had none. Covers both call forms, the two things that make its output trustworthy (the `authorable` mark is derived by parsing a probe against the live grammar, and the MDL example is parsed before emission with real enum members rather than placeholders), why bindings are named rather than invented, and why LIST WIDGETS deliberately does not exist. The sample output is copied from a real run against an installed HTML Element 1.2.2, not composed: the first draft said "Body containers (3)" from memory and the actual widget has four — `tagcontentrepeatcontainer` was missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
The page opened with "MDL supports a comprehensive set of widget types" over a closed category table — Layout, Data, Input, Display, Action, Navigation, Structure. That was true, and slices 2-3 made it false: every pluggable or custom widget installed in the project is now named by its own MDL name, with a body derived from its definition. A reader of the user manual would have concluded `htmlelement` does not exist. Adds an "Any installed widget" section covering the short form, where the three parts of it come from (the keyword is the widget's MDL name, the properties use the widget's own spelling, the containers are its object lists and child slots), how to find them with DESCRIBE WIDGET, the explicit id form as the FALLBACK rather than the norm, and what a name that resolves to nothing gets (MDL-WIDGET25 with suggestions, needing -p). Both examples were run through `mxcli check -p` before being written down, not composed from memory — the short form and the id form in one page, `Check passed!`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
Every example in the custom-widgets skill wrote `pluggablewidget 'com.mendix.widget.web.barchart.BarChart' chart1`. That still works, but since slices 2-3 it is the FALLBACK, not the normal way — and the skill is what an agent reads before placing a widget, so it was steering every generated page to the long form. Adds an opening section on naming a widget by its own name, with the body containers read from its definition, and three things worth knowing before writing one: `describe widget <name>` answers "what can I write here?" faster than reading the file and cannot go stale; the id form is for an ambiguous MDL name; an unknown name is MDL-WIDGET25 with suggestions rather than silence, and both new rules need `-p`. The frontmatter description is updated too — it is the routing mechanism, so a capability absent from it is a capability the skill will not be loaded for. The charts section kept its id table (it is what `describe widget` prints and what identifies a widget unambiguously) but no longer says charts are authored BY id, which stopped being true. `make check-skill-mdl` passes: 205 MDL blocks checked. The embed dir under cmd/mxcli/skills/ is gitignored and regenerated by `make sync-skills`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…rints Two small factual errors. MDL-WIDGET01's remedy in the error appendix said `mxcli describe widget <Name>`. That command does not exist — it is `mxcli widget describe <name>`, or `describe widget <name>;` in MDL. Someone hitting a property-name error would have been sent to a usage error. The `widget describe` Cobra help described properties and dynamic rules only, which was complete when it was written. The command now also reports body containers with whether MDL can express each, and ends with a runnable MDL example — the two things a reader most wants from it. The help says so, and says why bindings appear under "omitted" rather than being invented. Also trims the custom-widgets frontmatter description to 580 chars. TestEmbeddedSkillsCarryAgentSkillsFrontmatter caps it at 600 and my previous commit pushed it to 617 — skill listings truncate well before that, so an over-long description loses the tail that says when to use the skill. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
Widgets as first-class MDL: proposal and slices 0–5 (mendixlabs#1036)
Close ako/mxcli-rest FINDINGS #60: message-definition cardinality, and three gates that let a mistake through
`check --references` resolved the document and the entity a statement names
and stopped at the door. A member name inside one — the attribute on the left
of a create/change assignment — was never resolved, so a typo passed check,
passed exec, and surfaced at the far end of a build:
CHANGE $Order ("IsArchived" = true);
mxcli check -p app.mpr --references -> exit 0
mxcli exec -> "Created microflow"
mx check -> [CE1613] "The selected attribute
'Bench.Order.IsArchived' no longer exists."
exec does resolve the name, and on failure writes `<entity>.<member>` anyway;
that fabricated identifier is what mxbuild rejects. The check reports it where
it is cheap to fix, and names the entity's real members so the typo is visible
rather than merely asserted.
A CHECK needs three outcomes where exec needs two. exec can answer
resolved / not-resolved because it has a fallback either way; a check that
turns "I could not look" into "your attribute is missing" is a false error
that blocks a script which builds cleanly. So the walk has a third state for
"could not establish" and reports only the middle one. That covers a backend
that cannot answer — mxcli has several, and every lookup in the interface may
error — and it is also what makes an entity absent from the project silent
without a special case.
Coverage is bounded by whether the target's entity can be established: a
create (the entity is in the statement), a parameter, a database retrieve, an
association retrieve from a typed start, or a loop over any of those. A
variable bound by an activity this does not model is left unchecked rather
than guessed at. That boundary is documented and has its own test.
Verification, because three separate green results here were vacuous and only
perturbation caught them:
- a sweep of 14 real scripts reported 0 false positives while the check
never fired — every CHANGE target in that corpus is bound by an
unmodelled activity. Proven non-vacuous by perturbing one character of a
real attribute name in one of those scripts and watching it report.
- the example passed while exercising nothing: a "skip entities the script
creates" guard skipped the whole self-contained file. The guard was
removed as redundant (an absent entity is already unknowable), which also
widened real coverage.
- the example is inert against a virgin project, since members resolve
against the model. Its header now says exec-first and why.
Controls: stubbing the check fails the three positive tests; collapsing the
third outcome into "missing" produces exactly the false positive the design
prevents. 482 MDL examples still check clean.
mendixlabs#1048
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…mplate params Two more member positions reference checking walked past. Both passed `check --references`, passed `exec`, and failed the build with CE1613. XPATH CONSTRAINTS (mendixlabs#1049). The entity in `database from Mod.Entity` was resolved and the `where […]` never looked at: where [Bench.Order_Status = 'Open'] mx check -> [CE1613] "The selected association 'Bench.Order_Status' no longer exists." at Data grid 2 'gridOrders' Every step is now followed with the entity it lands on, the same traversal mdl/xpathrefs performs for renames: a bare step must be an attribute, a Module.Name step an association or an entity. TEMPLATE PARAMETERS (mendixlabs#1046). A parameter is evaluated against the widget's own context object, so a variable root has nowhere to resolve and the writer keeps it as part of the attribute name. This one needs NO project — the answer is in the statement — so it runs in the unconditional pass as MDL-WIDGET24 rather than under --references, where `mxcli check page.mdl` would have stayed silent on a mistake it can see. The template rule is not "a variable root is wrong", and reasoning rather than measuring would have shipped a false positive. Five shapes, each executed and built on 11.13.0: OrderNo clean Assoc/Attr clean $currentObject/Assoc/Attr clean <- the prefix IS stripped $currentObject/Attr CE1613 <- only on that branch $Var/Attr CE1613 Two things fell out of that table: the legal association form is two segments, not the XPath three (`Assoc/Entity/Attr` also fails), and `$currentObject` on an association path must not be flagged. The check then found a defect in the corpus meant to validate it. `ledger-27-consecutive-dynamictext.mdl` had shipped using `{1} = $currentObject/Amount`, had only ever been run through `mxcli check` with no project, and built with SIX CE1613s the first time it was executed. Fixed here, with its header saying what happened. One false positive was found by its own control and is worth naming: reporting a qualified XPath step requires the BASE entity to be known first. Without that, a page whose entity the project does not have reported a perfectly real association as missing. The three-valued discipline has to be applied at every reporting site, not once — the bare-member path inherited it from resolveMemberOnEntity and the qualified path did not. Controls: stubbing either check fails its positive tests; dropping the base-entity guard reproduces the false positive; an unparseable constraint is skipped rather than reported, since the XPath parse can return a tree that omits part of its input. 483 MDL examples and 205 skill blocks check clean. mendixlabs#1049, mendixlabs#1046 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
The command was inert on every MPR v2 project. It returned on the first module it could not reconcile, and one module always is: System, whose domain model is SYNTHESIZED rather than stored — there is no unit file behind the id the module carries, so the reconcile could not load it. $ mxcli -p app.mpr -c 'update security' Error: failed to reconcile security for module System: load domain model 00000000-…-002: …/mprcontents/00/00/…002.mxunit: no such file or directory That is the whole of the report's first half: it was filed as "UPDATE SECURITY does not fix the CE0066 it exists to fix", and the reason is that it never ran. Whether it had written some modules before dying depended on where the unreconcilable one fell in the list, so it was also non-atomic. System is now skipped by name rather than left to fail, because reconciling it is not merely impossible but wrong: its entities are the platform's and its access rules are not the project's to rewrite. Naming it is an error, not a silent no-op. Any other unreadable module is reported and stepped over, so a project with one still gets the rest reconciled and is told what was missed. A second defect in the same command: `update security RestLab` — without IN — reached the parser's error recovery, which consumed the module name silently. The statement parsed as ONE statement with no error, `mxcli check` reported "Syntax OK", and the run went project-wide. That is a silent scope escalation, which is worse than a parse error: the author asked to touch one module and the command touched all of them. IN is now optional before the NAME rather than before the whole clause, so both spellings scope. A typo is refused too — matching no module used to reconcile nothing and print "All entity access rules are up to date", a success message for a run that did nothing. Worth recording, because I conflated them once already this session: there are TWO System failures in this codebase. GetModuleByName/GetDomainModel answer for System perfectly well — System.FileDocument loads with its 6 attributes, which is why inherited-member resolution works — while loadDomainModelGen, which reads the unit by id out of mprcontents, cannot. Same module, opposite answers, different call path. What this does NOT claim: the report's precondition does not reproduce. Measured on 11.13.0, `alter entity … add attribute` on both engines and a whole-entity `create or modify` rewrite each left the project at 0 errors, because every mxcli write path already reconciles — so no stale rule could be produced to repair end to end. The integration example says that outright rather than implying coverage it does not have; the repair itself is covered by unit tests, which can construct the stale state. Control: restoring the abort and removing the System skip reproduces the reported error verbatim and fails three of the six tests. A project-wide run writes zero units when nothing is stale. mendixlabs#1047 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…arams Two defects that passed check, passed exec, and arrived as CE0117 "Error(s) in expression" at the far end of a build. A BARE WORD as a member's value (mendixlabs#1044): CHANGE $Order (Status = Closed); Mendix expressions have no bare identifiers — a value is a literal, a $variable, a qualified name or a function call — but the parser reads one as a variable reference, it resolves to nothing, and the kind comes out Unknown. Unknown is tolerated by every rule by design, which is exactly how this slipped through. VariableExpr now records whether it came from a plain identifier or from `$Name`, and E013 reports one standing alone as a member's value. The scoping is the design rather than caution: a bare name NESTED in a list-operation predicate is legal MDL — `FILTER($L, Status = 'Open')` resolves `Status` against the item under test — so the obvious rule ("no bare identifiers") rejects working scripts, and the report's own second example is one of them. A member's value is the one position where the bare word is the entire expression and can only be a mistake. Consequence, stated rather than glossed: the report's `FILTER($All, Status = Open)` is NOT covered, because the bad half is nested. A NON-STRING LOG TEMPLATE PARAMETER (mendixlabs#1043) turned out to be two gaps in series. The adapter walked LogStmt.Message and never the template parameters; and exprcheck's slot-expectation table had existed with NOTHING READING IT — slotKind() was defined and never called — so the pre-existing `LogStmt.Message: {Kind: KindString}` constrained nothing either, and `LOG WARNING 42` passed too. That second one surfaced only because adding the new slot changed nothing. Enforcement is limited to CONCRETE expectations. An entry carrying ResolveBy names a kind resolved per call site, and the adapter encodes that by appending the target to the slot path ("ChangeItem.Value:Sales.Order.Status"), which does not match the table at all — so those stay unenforced rather than being checked against a placeholder kind. Which kinds actually fail, measured on 11.13.0 by executing four microflows and building them — three errors for four parameters: String clean Integer CE0117 Decimal CE0117 Boolean CE0117 DateTime CE0117 Object CE0117 toString(anything) clean So the report's diagnosis ("the writer's template/parameter emission is wrong, not the caller's type") does not hold: toString(...) around the identical value builds cleanly, so the writer is fine and Mendix simply does not coerce. The check now agrees with mxbuild exactly — 3 reported for those 3 failures, String untouched. Controls: stubbing the two new checks fails three tests; a bare name nested in a predicate, an unmapped slot, a ResolveBy slot, `empty`, and every real expression spelling all stay silent. 484 MDL examples and 205 skill blocks check clean, and 14 real scripts from ako/mxcli-rest report nothing — proven non-vacuous by unquoting one string literal in one of them and watching E013 fire. mendixlabs#1044, mendixlabs#1043 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…ext variable
A datagrid whose datasource is an association path at PAGE level typed its rows
as the entity it navigates AWAY from:
datagrid gA (datasource: $Customer/Bench.Order_Customer) { … OrderNo … }
mx check -> [CE1613] "The selected attribute 'Bench.Customer.OrderNo'
no longer exists." at Columns (1/1) of data grid 2 'gA'
The destination is resolved as "the end opposite the context", and the context
passed was pb.entityContext — the ENCLOSING data container's entity. At page
level nothing encloses the widget, so it is empty, neither end matches, and the
resolver's last-resort fallback ("default to the child (TO) side") returns the
entity the grid started from. Every column then binds against it.
The report's own diagnosis is what localised this and deserves the credit: it
noticed the same path INSIDE a data view was correct. That rules out the
association logic and points at the context. A named context variable answers
the question directly — `$Customer/…` traverses from whatever $Customer holds,
enclosed or not — and pb.paramEntityNames already knew it. The enclosing-entity
path is untouched, so `$currentObject/Assoc` inside a data view behaves exactly
as before.
Worth generalising: a resolver taking an ambient context is correct exactly
where that context exists, and its fallback is what runs everywhere else. This
one carried a plausible comment ("matches the common FROM=context pattern") and
was silently wrong for the whole page-level case.
Two measurement notes. check-mdl cannot catch this — it only runs `mxcli check`
and the defect is in what the WRITER stores — so the regression net was exec +
mx check over the six examples that use an association datasource: five clean,
and the sixth failing identically before and after on an unrelated CE0106. And
the reverse-direction control first failed for the wrong reason: traversing a
Reference from its FROM end yields ONE object, so a grid over it is CE8812 "A
grid association path must result in a list" — a cardinality complaint, not a
resolution one. It is a data view now, so it tests what it claims to.
Controls: reverting to pb.entityContext fails the page-level test with the
reported symptom ("rows typed as Bench.Customer, want Bench.Order"); the
data-view case, an unknown variable, and the reverse direction all stay green.
mendixlabs#1045
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
… parse error a code Three things check accepted that exec or the build refused. AN UNQUALIFIED CREATE (mendixlabs#1050) is now MDL074, with no project needed. exec always refused it — "module name is required" — and the reportable cost is not the message: exec is NOT TRANSACTIONAL, so the statements before the failure are already applied and re-running then hits "already exists" on them. Driven off the existing stmtCreateInfo walker, so it covers every document type rather than just associations; a module is excluded, having nothing to be qualified by. validate_association_module.go had already documented this exact gap in a comment, and closing it association-only would have left every other document open. `RETURNS void AS $x` (mendixlabs#1041) is now MDL075, also project-free. An alias names the variable a flow returns, so pairing it with void is a contradiction — and mxcli believed the alias, writing `return $x` into a flow with no such variable: mx check -> [CE0109] "Undefined variable 'result'." at End event Refused rather than repaired. Emitting a bare `return` would also build, but an author who wrote an alias meant to return something, and silently dropping it leaves the flow returning nothing while the source still says otherwise. THE TRAILING-TOKEN PARSE ERROR (mendixlabs#1042) carried no code — printed as `[]` — no document and no microflow, and its fix line named only glued keywords. The reported input was `empty($Orders)`, where `empty` is a Mendix KEYWORD rather than a function, so the parser consumed it and stopped at the '('; people were sent hunting for an 'emptyor' that was not there. It now carries code E014, a registry entry, the microflow it is in, and a fix line naming that cause first. The line and column stay offsets into the expression fragment rather than into the file, so the flow name is the locator — a real limit, stated rather than implied away. A tier-ordering fact worth knowing when reproducing any E0xx, and now in the skill: the reference check runs BEFORE expression checking and exits on its first error, so one unrelated mistake anywhere in a file hides every expression hint in it. That cost real time here — a control microflow in the new example had `set $out = …` without declaring $out, and the resulting reference error made the E014 perturbation look like it was not firing. I guessed twice (needs no project; a duplicate-CREATE conflict) before bisecting to the real cause, and the example header records it. Controls: stubbing each rule fails its own test, and the stubbed E014 prints literally the `[]` the report complained about. A qualified CREATE, a bare `create module`, `RETURNS void` alone, a typed return with an alias, and both correct emptiness idioms all stay silent. 487 MDL examples, 205 skill blocks and 14 real ako/mxcli-rest scripts report nothing new. mendixlabs#1050, mendixlabs#1041, mendixlabs#1042 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
The perturbation note still said E014 needs "an app that does NOT already have this module, or the duplicate-CREATE check exits first". That was my second wrong guess at why the perturbation looked inert; bisecting showed the real cause, which the tier note a few lines above already records — the reference check runs first and exits on its first error, and the file had one. Leaving both in meant the example asserted two incompatible things about itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Every mxcli-authored Image widget failed the build. On a project at 0
errors, one image widget on one new page:
[error] [CE0463] "The definition of this widget has changed…"
at Image 'imgProbe'
A field-level diff of Atlas' own brand image against a describe → rename
→ exec copy of it differs in one line of 1480: `maxHeight` = '0' where
Atlas stores '250' (mxcli-ledger FINDINGS §142).
ee29546 named that property and did nothing, because it was verified
against an input that does not exist: its defaults fixture declared
`maxHeightUnit`'s default as "none", and Image 1.6.0 declares "pixels" —
the one value at which the rule does not fire.
The defect is which configuration the rule is asked about.
`maxHeight` is hidden when `maxHeightUnit` is "none". `maxHeightUnit` is
unmapped, so widgetValueMap cannot answer, and the fallback read the
DECLARED default "pixels" → condition false → maxHeight read as visible →
no reset → the template's captured 0 stood. But being unmapped is exactly
why the document gets the template's "none" and never "pixels": nothing
writes an unmapped property. The question is whether the property will be
hidden in the document about to be written, and the declared defaults
describe a document that will not be.
So the template's captured configuration goes into that fallback chain,
between the script's own values and the declared defaults —
builder.PrimitiveValues(), read before any mapping is applied.
Ground truth for the value, measured across the 69 Image widgets of a real
11.14.0 project: all 65 carrying a `maxHeight` store 250, at every
combination of heightUnit and maxHeightUnit; mxcli's was the sole outlier.
Proven both ways on the real package by patching the stored 0 to 250
(1 error → 0) and restoring it (0 → 1), and end-to-end: the same MDL
authored by the old binary and the new one, side by side in one project,
gives exactly one CE0463 — the old one. The §142 layout-copy recipe now
produces a build-clean layout whose only remaining difference from Atlas'
is the Forms$SidebarToggleButton describe already flags.
The test's control is the reverted state: with the template's values
dropped, the rule falls back to the declared defaults and writes nothing,
which is what the shipped binary did.
Note for anyone reproducing this: use `mx check` directly, not `mxcli
docker check` — the latter runs `mx update-widgets` first, which
reconciles the widget and reports 0 errors while the stored value is
still 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Microflows, nanoflows and rules share one catalog table, so LintContext.Microflows() yields all three. Eleven rule call sites hardcoded DocumentType: "microflow", four of them also putting the word in the message, so `mxcli lint` announced ⚠ Microflow 'Rule1' has no activities [MPR002] about a rule, and the same about a nanoflow. The wrong noun also reached the JSON and SARIF documentType field, where it is not cosmetic. The noun is now derived from the catalog's MicroflowType by one shared helper rather than written literally at each site, so the next rule to iterate flows gets it right by default. An unrecognised type falls back to "microflow": a finding with an imprecise label beats no finding. Only the labelling changes. Which documents each rule applies to is untouched — that is a separate question from what the report calls them. Two things worth knowing for the next change here. Widening a format string is not a compile error, so `go vet` is the gate that catches the missing argument (it caught two). And the existing fixtures in empty_test.go spell the type title-case where the catalog writes uppercase; harmless while nothing read the column, but a fixture with the wrong case now exercises the fallback instead of the mapping, so the new test uses the catalog's own spellings. Control: revert the two lines in empty.go and TestEmptyMicroflowRule_NamesTheDocumentType reports the symptom verbatim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
narrate.js makes a take watchable. Nothing made it true — that the beat
you filmed happened, and that the timestamp you cut on points at it.
These two files close that, generalised from the recorder and cutter in
ako/mxcli-intro-video (videos/sudoku-demo), where each guard cost a take.
take.js records both clock anchors, marks beats, paces actions to the
runtime rather than the script, guards the error dialog, applies the zoom
fix, and asserts per beat. cut-clips.js maps marks onto file time with
both anchors, refuses implausible ones, and writes a contact sheet.
The four findings behind them:
- The recorder's clock is not the video's clock, and is wrong twice: an
offset (recording starts at context creation, before the first
navigation settles) AND a scale (~1.065 measured — capture drops
frames while the page is busy). Correcting only the offset is the
trap: a constant right at the start was four seconds wrong by the end.
- A click can be swallowed while the previous request is in flight, so
the payoff never arrives and the beat is worthless. Assert the state
the beat is about. This is a check on the recording, not a verdict on
the app — the demo still never gates the build.
- Acting faster than the runtime commits deadlocks two microflows in
Postgres; the resulting modal swallows every later click. Hence the
pacing floor and the dialog guard.
- recordVideo.size pads a smaller viewport rather than scaling it. The
fix is viewport = video size plus CSS zoom.
Verified end to end against ffmpeg and a real headless Chromium, not just
parsed: a synthetic take with a burned-in timer cuts to 00:00:07.300
where the map predicts 7.294s, and all three refusals fire (implausible
scale, short clip without freeze, dead beat).
Testing found one bug worth recording: the dialog guard tested presence,
and Mendix ships the error-dialog container in the DOM hidden, so it
fired on every click — 800ms of dead time each and a meaningless count.
It now tests visibility, with a visible dialog as the control.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
The skill covered pace, the compositor and the audio pipeline, but not
what makes a take usable: seeded data, asserted beats, an anchored clock.
All of this is measured on ako/mxcli-intro-video's films.
Added:
- Seed the data before recording. The first ContactBook capture was an
empty grid reading "0 to 0 of 0" with a header rendering as
colActions — it reads as a broken app, not a new one. The journey
passed and the recording was still worthless.
- Hold = max(caption read, screen read), floor 2.5s. narrate.js already
computes the caption term; the screen term is the one authors
underestimate, and it has no way to know it.
- About two events per ten seconds.
- Zero Mendix vocabulary, with the sharper test: if the visual needs
those words to make sense, the visual is wrong.
- Do not invent a persona for a single-player product.
- The four capture-integrity findings, and the scripts that carry them.
Also a boundary note. video-system/ in ako/mxcli-intro-video already says
this skill owns the capture and it owns the framing and cut; saying so
from this side keeps them from drifting, and keeps a film to one caption
system rather than two layered in the edit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
Fix mxcli-ledger §142: every authored Image widget failed the build (CE0463)
Mendix's navigation sidebar collapses to an icon rail, and that is the state most users leave it in. A collapsed item shows its icon; one without falls back to the first few characters of its caption, which is rarely enough to tell "Orders" from "Order lines". Nothing said anything: the icon is optional in the grammar, the model builds, `mx check` passes, and the only symptom is a column of truncated words in a browser. Covers every item at every depth, in BOTH statements that carry menu items — `create navigation`'s `menu (...)` block and `create menu`. They share one AST node (NavMenuItemDef) precisely so the two cannot diverge, and the rule walks it once rather than twice. Uniform rather than top-level-only. A sub-item renders in the flyout the collapsed rail opens, and a submenu's PARENT sits directly on the rail, so it needs one most of all. If a deep menu proves noisy, narrowing the walk is a one-line change — reporting too little is the failure that is hard to notice. A WARNING, not an error: the project builds and runs, `exec` refuses only on errors, and a rule that blocked the script would turn a design opinion into a gate. It needs no project. Only the icon's TARGET has to be resolved, and MDL-ICON01 already does that separately — so this runs in the project-free pass and fires under `make check-mdl`, rather than being inert in CI the way four of the widget rules were. Controls, because "flag every item" would pass the first assertion: a fully iconed menu stays silent (asserted both as a unit test and end-to-end on the new example script, which checks clean); an iconed sub-item beside two iconless ones is not reported; a statement with no menu, and an unrelated statement type, produce nothing. Verified the rule really fires in the corpus rather than being absent — `make check-mdl` reports pass/fail only, so its zero MDL074 lines mean nothing on their own; running check directly on an existing navigation script shows the warnings. The example is a POSITIVE one. MDL074 is a warning, so `check` still exits 0 and a `.fail.mdl` would be reported by CI as "negative test unexpectedly passed" — the trap the Makefile documents for mendixlabs#891/mendixlabs#892. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
DESCRIBE -> exec was destroying menu icons. Measured on testdata/expr-checker,
whose Home item carries a glyph icon:
describe menu item 'Home' page …;
-- icon a numeric glyph code (Forms$GlyphIcon) is not reproducible …
exec Navigation profile 'Responsive' updated.
describe menu item 'Home' page …; <- the icon was gone
Exit 0, success message, silent loss — the same shape as the pluggable-widget
body loss in mendixlabs#1036, and reachable the same way: describe -> edit
-> exec is the editing loop for a project whose rule is "never hand-edit the
.mpr".
The giveaway was already in the output and read as harmless. That comment was
written to make the loss visible, and it did — in the DESCRIBE output, not in the
exec that then acted on it. A "cannot reproduce this" note beside a
FULL-REPLACEMENT statement is a note saying "running this deletes it".
Mendix stores THREE icon elements, and they are not spellings of one value:
Forms$IconCollectionIcon a name in an icon collection
Forms$GlyphIcon a numeric character code, and NO name
Forms$ImageIcon a name in an IMAGE collection — a different document
Every layer handled only the first. The reader captured the $Type and the Image
but never the glyph's Code, so mxcli knew a glyph had been there and not which
one; the AST and the write spec carried a bare name with no kind at all; and both
engines' writers emitted IconCollectionIcon unconditionally.
Now each variant has its own MDL form, and the BARE form still means the
icon-collection icon, so every existing script keeps its meaning:
icon Atlas_Core.Atlas.home Forms$IconCollectionIcon
icon glyph 57377 Forms$GlyphIcon
icon image MyModule.Images.logo Forms$ImageIcon
Keyword-led rather than widening the bare form, because a bare name written for
an image icon would rebuild it as a COLLECTION icon — a silent variant swap, the
failure mode the original code avoided by emitting nothing. Nothing is inferred:
the kind comes from the author or from what the reader saw in storage. The two
keyword alternatives are ordered FIRST in the grammar, since qualifiedName
accepts a keyword as a name segment and `ICON IMAGE …` also matches the bare form.
NOT preserve-when-silent, which is what I proposed before writing the syntax.
Preserving is right only while a construct is INEXPRESSIBLE — an omission cannot
be a choice if the author had no way to say it. Once `icon glyph N` exists,
omitting it is as deliberate as omitting a collection icon, and preserving would
make it impossible to remove a glyph icon from MDL and make omission mean
different things for different kinds. The stale-script path is covered by MDL074
instead: verified, an old-style script against the fixture warns per item rather
than silently destroying anything.
Fixes MDL074 in passing, and it was a real false positive: the rule tested
`Icon == ""`, and a glyph has a code and no name, so it reported items that
plainly have icons — the fixture's own Home item among them. It asks the kind now.
Controls: with the describe fix stubbed, the collection case still passes while
glyph and image fail, so the tests detect the defect rather than the shape of the
code. A bare name with no kind must still write a collection icon, or every
script predating this silently loses its icons. A glyph with no code, and an
unknown $Type, are still flagged rather than guessed at — without a case that
still declines, "emits everything" and "reproduces everything" are
indistinguishable. Visitor-level tests assert the AST, per the mendixlabs#1036 lesson that
a corpus diff of check output is blind to a construct that parses into the wrong
shape.
Round trip on the fixture is now lossless: `icon glyph 57377` before, exec,
`icon glyph 57377` after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
feat(demo): capture integrity for record-narrated-demo, from the intro-video films
Navigation menu icons: stop destroying them, and make all three authorable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fifty-five commits since the last sync. Each was reported by a real symptom, reproduced first, and carries a control showing the fix is what changed the outcome.
Two of them are worth reading even if you skip the rest:
update securitywas inert on every MPR v2 project, and every mxcli-authored Image widget failed the build.Pluggable widgets become a first-class MDL extension point
The largest strand, driven by #1036 and its proposal. A pluggable widget's object lists and child slots were reachable from MDL only via a hand-maintained list of nine keywords, while
mxcli widget initderived documentation for all of them mechanically — two lists, and nothing comparing them. Measured on the fixture project's 33 widget defs, most documented slots were unreachable.The consequence was self-inflicted:
mxcli widget initgenerates widget documentation for agents to follow, and feeding it back intomxcli checkverbatim failed on the first line of its own example.DESCRIBE WIDGET— a widget was the only MDL extension point with no DESCRIBE, so it needed the CLI where a microflow, nanoflow, Java action and JavaScript action all answer in-language. It reports body containers, what is authorable, and emits an MDL example that parses as written, with bindings narrowed by the widget's own rules.checkand failed atexec(MDL-WIDGET25, MDL-WIDGET26).CATALOG.REFS. "Which pages use this widget?" was unanswerable while the same question about a Java action was one query — and it is exactly the question an upgrade asks: a.mpkno page uses is dead weight, one used on forty pages is not swapped lightly.Two that were quietly broken
update securitywas inert on every MPR v2 project. It returned on the first module it could not reconcile, and one always fails:System, whose domain model is synthesized rather than stored, so there is no unit behind the id. The command now runs, and honours its scope.CE0463 "The definition of this widget has changed…"on a project otherwise at 0 errors. A visibility rule was evaluated against the wrong state; it is now evaluated against what will actually be written.Correctness in what we author
CE1613on every column. Now resolved from the context variable.drop associationreported success while a message definition still exposed it, naming neither the collection nor the definition. mxbuild caught it asCE1613, anddescribewent on emitting the dangling member — so a describe → exec round trip carried the break forward. Now refused.ReferenceSetis a list in both directions. Cardinality was derived from traversal direction alone, which is right for a Reference and wrong for a ReferenceSet, so a set reached forward was exposed as a single object.DESCRIBE → execwas destroying navigation menu icons. All three icon kinds now author and round-trip, andMDL074warns when a menu item has none — Mendix's sidebar collapses to an icon rail, and an item without one falls back to a few characters of its caption, which rarely distinguishes "Orders" from "Order lines".ALTER ENUMERATION … ADD VALUEhad no idempotency guard, so a script adding a value was not re-runnable. The harm is not the message:exechalts at the failing statement, so one already-present value silently truncates everything after it. AddsADD VALUE IF NOT EXISTS/DROP VALUE IF EXISTS.A run of check false positives, and why they survived
Four false positives — a widget property's documented alias, a chart series' datasource routing, a
sum()Decimal guess, and an object-list item counted as a widget for CE0495 — all found by runningcheckunder a real project.The common thread is the interesting part:
make check-mdlrunscheckwithout a project, so no widget definition and no domain model load and the rules are inert. A file's "check-clean" header proved nothing about them. Also fixed: a widget's property warnings depended on map iteration order.The validator gained real capability alongside: it resolves member names rather than just the entity they belong to, resolves them inside widgets (XPath steps and template params), enforces expression kinds, refuses unqualified CREATEs and void-with-alias, and resolves an ALTER's target document rather than only its module.
Test hygiene