Skip to content

Sync ako/mxcli: image widgets, the local test runner, aggregates and describe round-trips - #1016

Merged
ako merged 63 commits into
mendixlabs:mainfrom
ako:main
Sep 1, 2026
Merged

Sync ako/mxcli: image widgets, the local test runner, aggregates and describe round-trips#1016
ako merged 63 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

24 commits from the fork, all driven by findings from four Mendix projects built with mxcli (formula1, ledger, sudoku, chat). Grouped by theme; each bullet is one defect and how it was established.

The recurring shape across most of these: mxcli check and mx check disagreed, or a describe produced output mxcli's own parser or checker then refused. In several cases the checker already held the answer the writer needed.

Image widgets and CE0463

  • A hidden widget property must hold its DECLARED default, not the template's captured value. Every Image widget mxcli authored on 11.13 failed the build with CE0463. The engine skipped the mapping for a property the widget hides, which leaves whatever the extraction template happened to be set to — image.json stores width 48 against a declared default of 100. The writer now reads widgetPropertyDefaults, the same source MDL-WIDGET10 reads, so checker and writer cannot drift apart again.
  • MDL-WIDGET22, the error CE0463 was masking: an image widget with the default source and no image builds into "No image selected.". It found the same breakage in four of the repo's own examples.
  • MDL can now name the imageimage imgLogo (Image: 'Module.Collection.ImageName') — through a new image operation in the widget engine, mapped in both embedded defs and emitted by DESCRIBE. This closes a describe → rename → exec copy of an Atlas layout losing its brand image.
  • That new reference is resolved at check time, since a typo previously passed mxcli check --references and failed the build with CE1613. The name has three parts, so a missing collection and a missing image get different messages.
  • The last CE0463 field. A full diff of Atlas' own brand image against a copy differed in one line of 1480: maxHeight, mxcli's 0 against the package's declared 250. The reset ran over the definition's property mappings, and a mapping is what gives a property an MDL keyword — width has one, maxHeight has none. The set of properties that must be default-valued is the widget's editorConfig to decide, not mxcli's.
  • widget sync now reports what it compared. It said "every stored widget instance already matches its installed package" about a widget mx check was erroring on; both were right, about different planes — sync compares the stored schema, the error was a value.

The local test runner and the dev loop

  • mxcli test --local could not start a runtime for any project (a regression in the fork, reported independently by two projects). A change gave the test boot its own deployment tree; mxbuild writes the deployment to <app dir>/deployment and has no option to move it, so this moved where the runtime reads and not where the build writes. Measured, not inferred: --target=deploy on a project whose deployment/ had just been deleted recreated it there. A DeployDir the build will not populate is now refused, naming mxbuild as the constraint.
  • The hazard that change was for is fixed the way the constraint allows. A headless test boot's packaging pass deletes deployment/web/dist, leaving a concurrent mxcli run --local serving HTTP 200 over a blank page with nothing reported at either end. The bundle is copied aside before the boot and put back after — a few MB against a ~30s re-bundle — and only when it is actually gone, so a newer one is never clobbered.
  • An end-to-end guard, asked for by name in one of the reports: after a real build, the directory the runtime will boot against must hold model/bundles. The four unit tests that covered the original change all passed against a build that could not start, because each asserted the option while the symptom lived in what is on disk after mxbuild runs.

XPath and list-operation expressions

  • Uppercase AND reached stored XPath and failed the build with CE0161. XPath 1.0 spells its operators lower case; MDL's lexer accepts any case. mxcli already reconciled that on one of two rendering paths — the other freezes the raw source whenever the clause contains a /, which every variable path does, so the casing survived exactly when a variable was present. Fixed at the choke point all three constraint writers share; the replacement is token-based and string-literal-aware, so 'A AND B' and identifiers like Brand are untouched.
  • A bare attribute in FILTER/FIND built an invalid expression. Mendix evaluates the predicate once per item with the item bound to $currentObject, where a bare attribute name is not valid — mxbuild reported CE0117 while mxcli check passed. Plus documentation of what those predicates refuse and why.

Microflow aggregates

  • A Reduce's fold was being deleted on rewrite. Mendix keeps it in two properties beside the expression (ReduceInitialValueExpression, ReduceReturnDataType); the semantic model had a field for neither, so both engines read them as nothing and wrote them back as nothing — leaving a model that still passed mx check.
  • reduce, all and any in the aggregate grammar, and DESCRIBE now emits every aggregate as MDL that parses back.

DESCRIBE round-trips

  • Quoting and escaping in one place. DESCRIBE WORKFLOW emitted single-quoted payloads without doubling interior quotes, so its own output was a syntax error.
  • Workflow annotations are emitted as comments, not statements — the describer was producing annotation '…';, the exact construct MDL-WF04 exists to refuse, giving 13 errors from one unmodified describe.
  • A jump activity is no longer named after its target. Mendix resolves TargetActivity by name, so the jump could resolve to itself and the build failed CE6681, which describes a different fault than the real one.

Skills and documentation

  • A headless Mendix version-upgrade skill, with the two ways of getting it wrong that both look like success (--loose-version-check reports BUILD SUCCEEDED on the old version; editing _MetaData reports the new version while every unit is read against the wrong schema).
  • Bug findings moved out of fix-issue.md into per-area JSONL shards, with a target that measures how far the docs-wiki/bug-patterns/ digest has fallen behind, and nine new digest pages for mdl/executor.

Verification

Every fix carries a control that passed before the change — for the build-affecting ones, measured against mxbuild 11.13/11.14 on real projects rather than only in unit tests. go test ./..., make vet, make lint and make check-mdl are clean on the fork's main.

Not included: one further commit is still on an open PR in the fork (a warning when a local test run will recompile a running app's classes, which cannot be prevented — mxbuild owns the compile and the deployment directory cannot be moved)

claude and others added 30 commits August 30, 2026 15:06
Every Image widget mxcli authored on Mendix 11.13 failed the build with
CE0463 "the definition of this widget has changed" — including the exact
pluggablewidget form that mdl-examples/bug-tests/image-ce0463-stale-default.mdl
documents as fixed (it was verified on 11.12).

Case B, established before any hypothesis:

  baseline blank project, mxcli never ran     -> 0 CE0463
    ...and it ships 10 Studio Pro-authored widgets of the SAME widget id
  the same project + one mxcli-authored Image -> 1 CE0463, naming that widget

So the tool is the variable, and those 10 are a known-good reference in the
same project — better than a template extraction, and no Studio Pro needed.

The exhaustive path diff against them came back with every path present on
both sides and four differing values, two of which were content. The Type
(PropertyTypes schema) subtree and the TypePointer -> PropertyKey mapping were
identical.

Two hypotheses were tested and falsified, recorded because the elimination is
worth as much as the fix:

  - Key order. mxcli's widget node is not in the reference's alphabetical
    order, which IS a documented CE0463 cause. It is not this one: the same
    page's mxcli-authored Datagrid and Badge carry the identical key order and
    pass.
  - The width value. Authoring Width: 48 explicitly passes, so 48 is not
    rejected on its own.

The cause is the interaction. The widget hides `width` when `widthUnit` is
"auto", and a hidden property must hold its DECLARED default. The engine
skipped the mapping for a hidden property, which leaves the widget TEMPLATE's
captured value — image.json stores 48 while its own ValueType declares the
default as 100.

mxcli already knew. MDL-WIDGET10 says it in as many words — "a non-default
value there fails the build with CE0463 (the default is "100")" — so `check`
was refusing what `exec` emitted. The writer now reads widgetPropertyDefaults,
the checker's own source, so the two cannot drift apart again.

Skipping was never the invariant: the engine's comment states it correctly,
"the hidden ones at their default, so hidden means default-valued, not
absent", and skipping only coincides with that when the template happens to
already be at the default. Where no default can be looked up (a datasource has
none) it still skips, so the File Uploader pruning from mendixlabs#956 is unchanged.

Verified end to end on 11.13: CE0463 gone, and stubbing the SetPrimitive back
out brings it straight back.

Reported as mxcli-formula1 FINDINGS §69/§142.
Found by fixing the Image CE0463: with the definition error out of the way,
the bare form's real problem is visible.

  image i (Responsive: false)
    -> [error] "No image selected." at Image 'i'

The Image widget's `datasource` defaults to `image`, an entry from an image
collection — and MDL cannot name one: the widget's `imageObject` property is
of type `image`, an operation the pluggable widget engine does not implement.
So the default spelling of `image` always writes a model mxbuild refuses.

CE0463 was masking it, which is why it went unnoticed for so long: the
definition error fired first, and `mx update-widgets` "fixed" the page by
clearing that one and leaving the real error behind. It is also what makes a
describe -> exec copy of an Atlas layout lose its brand image.

Reported at check time because that costs a second rather than a build, and as
an ERROR because the build does not pass — accepting it would be mxcli waving
through a script it knows produces a broken app. The message quotes what
mxbuild says and the suggestion names the spelling that does work.

The rule immediately found the same breakage in four of this repo's own
examples, which are corrected here rather than the rule weakened. One of them,
P_Image_OnClick, could not simply be given a URL: `DisplayAs` is hidden unless
the source is an image collection, so setting both is CE0463 — MDL-WIDGET10
caught that during the edit. It now demonstrates the URL source without
DisplayAs, and says why.

Verified: the corrected doctype script applies 12 statements and mx check
reports no new errors.
`RETRIEVE ... WHERE a = $Var/Attr AND b = $Var/Attr` passed `mxcli check` and
failed the build:

  ERROR at Formula1Backend, Microflow 'ZZ_AndProbe2',
    Retrieve object(s) activity 'Retrieve list of LiveForecast from database':
    Error(s) in XPath constraint.                                   (CE0161)
  BUILD FAILED

XPath 1.0 spells its operators in lower case only, and MDL's lexer accepts any
case (`AND: A N D;`), so the two have to be reconciled on the way out.

mxcli already did that — on one of its two rendering paths. expressionToXPath
lowercases the operator while walking the parse tree; but
buildRetrieveWhereExpression freezes the RAW SOURCE whenever the clause
contains a `/`, which every variable path does, and expressionToXPath's
SourceExpr case hands that text straight back.

So the casing survived exactly when a variable was present. That correlation is
the whole difficulty, and the reporting project got there first: their earlier
note blamed the casing alone, and the literal-only reproducer they wrote to
file it upstream BUILT CLEANLY. Measured, all three:

  WHERE SessionKey = '1' AND AtLap = 2                  -> stored `and`, builds
  WHERE SessionKey = $N/SessionKey AND AtLap = $N/AtLap -> stored `AND`, CE0161
  ...the same with lowercase `and`                      -> builds

Fixed at FormatXPathConstraint, the single choke point all three constraint
writers share — retrieve, page data source and entity access rule. The latter
two were broken in the same way and measured as such before the fix, which is
why the fix is not in the retrieve builder. It runs BEFORE the width test,
because that branch returns the caller's own bytes and is where the casing hid.

The replacement is token-based and string-literal-aware. Both are cases where
being careless would be a worse bug than the one being fixed: rewriting inside
`'A AND B'` silently changes which rows the constraint matches, and an
identifier that merely contains the letters (Brand, Andrew, NOTES,
Order_Andon, Module.Handover) is not an operator. `div` and `mod` are XPath
keywords too and are deliberately left out — nothing in MDL emits them, and a
rewrite nothing needs can only be wrong.

Verified on 11.13: the example applies 8 statements and mx check reports no
CE0161, with `'A AND B'` intact inside its quotes; stubbing the normalisation
back out returns the uppercase operator in all three writers.

Reported as mxcli-formula1 FINDINGS §80.
mxcli-formula1 FINDINGS §79 asked for this, having spent most of a session
working the upgrade out by hand. Both ways of getting it wrong look like
success and neither prints a warning:

  mxbuild --loose-version-check        BUILD SUCCEEDED   still on the old version
  UPDATE _MetaData SET _ProductVersion reports the new   every unit read against
                                       version           the wrong schema

§79 proposed it as a skill PACK. It goes in mendix/ instead. The packs README
is explicit that packs are opt-in because they are not free — bulk-oql-dml
ships MDL that adds Java actions, vega-charts needs a widget installed — and
all three existing packs carry mdl/, java/, scripts/ or widget/ trees. This
ships nothing and touches no model, so there is nothing to opt into; and §79's
own argument for packs/ ("none of it is specific to this solution") is the
argument for shipping it to every project, which is what mendix/ does. The
inversion is easy to explain: in that project the only two skills installed
were packs, so "a skill" looked like "a pack".

Everything load-bearing was measured here rather than transcribed, and doing
so corrected the source twice.

Confirmed, and matching §78 byte for byte across two machines:

  11.13.0  {SHA256}5Fk35jOyzj+cWnJe9ZkGWQjMEzsge3nIzS2zxH9jp6M=
  11.14.0  {SHA256}o9B9S8lorV9RD5gY9B6j1bJp4ALW87u4newnreIbRAg=

Their 517-unit application and a blank app carry identical hashes at the same
version, which is what makes _SchemaHash a usable test — and is the thing a
single project could not demonstrate. A full 11.13 -> 11.14 `mx convert` on a
copy landed exactly on the 11.14 reference value, and `mx check` came back with
only the base project's pre-existing error.

Two corrections to the source:

  - §78 verifies MPR v2 survived by counting units and .mxunit sidecars either
    side. Measured here, .mxunit went 391 -> 386 while v2 was perfectly intact,
    so an unchanged count is a coincidence rather than the invariant. The skill
    checks the storage format instead: mprcontents/ still present, .mpr still
    tens of KB rather than tens of MB.
  - `mx convert` takes the app DIRECTORY. Given the .mpr it fails with "The app
    directory '/path/App.mpr' does not exist", which reads like a missing
    project.

Also verified: --loose-version-check leaves _ProductVersion and _SchemaHash
untouched; `mx create-project` needs --app-name/--output-dir and fails with
PathTooLongException under a deep directory.

The commands are the ones that were actually run. The sqlite3 CLI is NOT
present in this container, so the skill uses python3's sqlite3 module, and
points at mxcli's own connect banner where only the version is wanted.

The version-pair specifics are marked as what happened once — the two hashes
are facts about two releases, not a pattern, and the skill says to re-derive
the reference from a blank project for any version not listed.
MDL could not say WHICH image an image widget shows, so two things went
wrong at once (mxcli-formula1 FINDINGS §142).

Every mxcli-authored `image` was incomplete. Its default source is an
entry from an image collection (`ImageType: image`) and MDL had no way to
name one, so the model mxbuild got was "No image selected.". And a
describe -> rename -> exec copy of an Atlas layout came out with no brand
image: describe emitted `image staticImage1 (Responsive: false)` because
the reference was in the stored document with no MDL spelling.

    image imgLogo (Image: 'MyFirstModule.Images._1')

The name is three parts like an icon reference, Module.Collection.Image,
stored as a plain string on the WidgetValue's `Image` key. Both halves are
needed: with only the write, a round trip still loses it.

Write: `SetImage` on the shared widgetobj builder (both engines) plus the
mcp builder, an `image` operation in the widget engine, and `imageObject`
mapped in both embedded image defs. `operationForType` learns the type,
so a generated def picks up image-typed properties on any widget.
`setImageValue` REPLACES the existing key and never adds one - adding a
key the definition does not declare is the CE0463 shape.

Read: `extractCustomWidgetPropertyImage` resolves the property through
its TypePointer (the Image widget has two image-typed properties, so a
positional read returns the wrong one), and describe emits `Image:`.

MDL-WIDGET22 now reports a genuinely incomplete widget rather than a
missing capability, and leads with the remedy that keeps the author's
intent instead of telling them to switch source.

Verified on 11.13.0: the page builds at 0 errors (base project's CE0117
aside), and `describe layout Atlas_Core.Atlas_Default` now emits
`image staticImage1 (Image: 'Atlas_Core.Layout.logo', Responsive: false)`
- the copy keeps its logo and builds clean. Controls: stub
`setImageValue` and TestSetImageValue_SetsTheQualifiedName fails; stub the
describe emission and TestDescribeImageWidget_EmitsTheImageReference fails.
Wiring `Image:` gave MDL a new qualified name to get wrong, and the first
thing a typo did was slip past the reference check and fail the build:

    image imgTypo (Image: 'MyFirstModule.Images.NoSuchImage')

    mxcli check --references -> Check passed!
    mx check                 -> [CE1613] "The selected image
                                'MyFirstModule.Images.NoSuchImage' no longer exists."

Every other name a widget carries - microflow, nanoflow, page, snippet,
entity - is already resolved by validateWidgetReferences. This is the same
check for the one reference that has THREE parts, so which half is wrong
decides where the author has to look and the two mistakes get two
messages: a missing collection sends them to `show image collections`, a
missing image to that collection's contents, which the diagnostic lists.
A name that is not three parts is reported as the shape mistake it is
rather than as a missing image.

A nil name set means the collections could not be read at all and nothing
is reported - calling every image missing on a failed read is the wrong
direction to be wrong in. An empty non-nil set is a real answer: the
project has no image collections, so the reference is wrong.

No same-script exemption, unlike every other branch: MDL cannot create an
image collection entry, so the reference can only resolve against the
project.

Measured against a real 11.13 project - all three mistakes reported with
the right message. Controls: TestImageRefErrors_KnownImageIsClean (an
existing image, and a case difference, are not reported) and
TestImageRefErrors_NoCollectionsMeansNoOpinion.
DESCRIBE WORKFLOW emitted single-quoted payloads without doubling the
quotes inside them, so its own output was a syntax error:

  targeting users xpath '[System.UserRoles = '[%UserRole_Banker%]']'
  -> line 6:48 mismatched input '[%UserRole_Banker%]' expecting ';'

The string terminates at `= '` and every later statement cascades, so
DESCRIBE WORKFLOW output could not be fed back to check or exec for any
workflow with an XPath-targeted user task.

The reported case is one of six. The emitters wrote

  fmt.Sprintf("... '%s'", v)

with the quotes in the format string and the escaping, when present, as
a separate statement at the call site — one thing to remember per site,
and 6 of 23 sites did not do it: both `targeting … xpath` variants, the
user task caption, the workflow-level due date (the task-level one two
functions away does escape), and both outcome values. The caption needs
no XPath to reach: `Manager's review`.

mdlQuoted(s) now returns the complete literal, quotes included, so an
unescaped emit cannot be written by omission, and all 23 sites go
through it.

Tests assert the emitted MDL PARSES rather than that it contains a
particular escape — a substring assertion would encode the escape under
test and pass for the wrong reason. A source scan guards the class,
because emit tests only cover the positions they construct and the real
failure mode is a new site added later.

Reported as mendixlabs#1006 (the XPath variant only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DESCRIBE WORKFLOW emitted `annotation '<text>';` — the exact construct
MDL-WF04 exists to refuse, and that execCreateWorkflow refuses again. So
the describer produced output mxcli's own checker rejects: 13 MDL-WF04
errors from one unmodified describe of a 23-activity workflow.

Two parts of the tree disagreed, each stating its position in a comment.
The emitter: "emitted as a parseable MDL statement so it survives
round-trips." The validator: it "produces a model Mendix cannot load
(the annotation is placed in the activity flow, which accepts only flow
elements)". The emitter's comment was the stale one.

The reporter read these as canvas annotations. They are not:
formatAnnotation is called at 10 sites, always with an activity's
ATTACHED Annotation, and the describer converted attached to standalone
— which is the refused form. That matters for the fix, because
MDLWorkflow.g4 has only the standalone workflowAnnotationStmt: no MDL
input can express an attached annotation, even though the write path
stores one. Commenting it out therefore loses nothing that was
reachable.

Both emit paths become comments — the attached one and the standalone
WorkflowAnnotationActivity read back from a model. The standalone branch
also has to mark itself a comment, or the terminator logic appends `;`.

The microflow domain already has an attached form (`@annotation 'text'`)
that round-trips properly. Giving workflow activities the same prefix
would preserve the annotation instead of commenting it out; that is a
grammar change and deliberately not bundled here.

Tests assert the emitted MDL parses, passes ValidateWorkflow, AND still
contains the text — the last one because dropping the annotation
entirely would satisfy the first two.

Reported as mendixlabs#1007.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
buildJumpTo set the jump's Name to its target's name. Mendix resolves
TargetActivity BY NAME, so the jump could resolve to itself; the build
then fails CE6681 ("not possible to jump to end activities or jump-to
activities"), which describes a different fault than the real one.

Nothing caught it first. A jump target is the only INTRA-document
reference a workflow has: validateWorkflowStatementRefs resolves
microflows, pages and entities, and never looks at activity names. So
`check --references` passed, `exec` reported success, and the defect
surfaced only under the native validator.

The reported trigger is an unresolved target. There is a second case,
not in the report: a FORWARD jump to a target that DOES exist breaks the
same way, because deduplication renames the second activity carrying a
name. Measured:

  backward (target first)  jump -> StepB2, target keeps StepB   worked
  forward  (jump first)    jump KEEPS StepB, target -> StepB2   broke

which is why loops looked fine and the reported table says a valid
target is safe. Fixing only the unresolved case would have left half the
bug in place.

Both ends are fixed. A jump is named JumpTo (deduped: JumpTo, JumpTo2,
...) so it can never carry a target's name, and deduplication now runs
in two passes with jumps LAST, so a jump never claims a name a real
activity wanted whatever it is called.

MDL-WF05 refuses an unresolved target at check time and, via the same
function, in execCreateWorkflow — exec is reachable without check (the
mendixlabs#833 lesson). The valid-target set comes from running the real builders
over the AST rather than re-deriving names, because a `call microflow
M.SUB_X` activity is named SUB_X and a second copy of that rule would
drift into false refusals. The suggestion lists the valid names: they
are not visible in the script, which is most of why the wrong one is
easy to reach.

Reported as mendixlabs#1005.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mendix keeps a Reduce's fold in two properties beside the expression —
ReduceInitialValueExpression (the seed for $currentResult) and
ReduceReturnDataType (what the fold produces). The semantic model had a
field for neither, so both engines read them as nothing and wrote them
back as nothing: rewriting a microflow containing a Reduce deleted the
user's fold and left a model that still passed mx check.

Both properties now round-trip through AggregateListAction and all four
read/write paths.

The shape is measured against Studio Pro, not inferred, and the reference
documents contradict Mendix's own reference guide in two places:

  - Both keys are written on *every* AggregateAction, not only on Reduce.
    The All and Any activities each carry an empty initial value and a
    Boolean return type, though the guide says a return type is "not
    applicable" to them. So the pair is written for Reduce/All/Any, and
    for the five older functions only to carry back what a stored
    document already had — no reference document exists for those, and
    inventing a key is what produces a document mxbuild accepts and
    Studio Pro cannot open.

  - Attribute is stored as "" when unused. It was omitted, which made a
    freshly described Studio Pro aggregate rewrite on its first execution
    for no semantic reason.

With all three, the Reduce/All/Any activities in ako/TestApp
(Microflows.MicroflowReduce, Mendix 11.14) re-serialize byte-identically
to what Studio Pro wrote.

TestReduceFoldReachesStorage asserts on the stored BSON rather than the
semantic model that produced it, because everything above this layer
looked right while the document was wrong. Its control: revert the two
setters and it reports both keys missing.

Refs mendixlabs#1004

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
Mendix has eight aggregate functions; listAggregateOperation had five.
The three it lacked are the ones DESCRIBE was already emitting, so a
Studio Pro-authored Reduce described as MDL that would not parse — it
fell through to a plain Change Variable, and MDL044 then correctly
reported that Mendix has no reduce() expression function.

  $Folded   = reduce($list, expr, initial: seed, returns: Type);
  $AllMatch = all($list, boolean-expression);
  $AnyMatch = any($list, boolean-expression);

REDUCE names its seed and result type because Mendix requires both and
neither is inferable from the expression — guessing either is how the
fold gets silently lost. ALL and ANY take neither: they never accumulate
and always fold to Boolean, so writing a type would only let an author
contradict Mendix.

REDUCE, ANY and INITIAL are new lexer tokens, so they are also added to
the `keyword` rule to stay usable as identifiers; ALL and RETURNS already
existed. Reusing ALL in expression position introduces no ambiguity —
ANTLR generates the parser without warnings.

ast.AggregateReduce and its flowBuilder case already existed, unreachable,
with no grammar rule and no visitor mapping behind them.

The regenerated completions also pick up CUSTOMBUTTON and ALLOWEDFILEFORMAT,
which have been missing since dbc26ff added them to the lexer without
regenerating the file.

Refs mendixlabs#1004

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
DESCRIBE rendered an aggregate with strings.ToLower on whatever Mendix
had stored, which silently assumed every value of the AggregateFunction
enumeration was also an MDL keyword. Five were. Reduce, All and Any were
not, so mxcli emitted MDL its own checker rejected (mendixlabs#1004) — and would
keep doing so for every function Mendix adds.

The mapping is now explicit, and a function with no keyword is reported
as an unrenderable activity rather than emitted as a plausible-looking
lie. REDUCE additionally renders its seed and result type, without which
the described statement cannot be executed back.

The sibling formatListOperation switches on concrete types and cannot
drift this way; stringifying an enum into a keyword is the shape to
avoid.

TestDescribedAggregateParsesBack drives every function in
microflows.AllAggregateFunctions through describe and back through the
parser, so a ninth function fails a test rather than a user's script.
Control on origin/main: reduce/all/any each parse to a Change Variable
rather than an aggregate, with sum as the positive control.

Closes mendixlabs#1004

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

Aggregates were undocumented in the quick reference and the
write-microflows skill, and the syntax topic listed three of the eight.
All three now cover the full set, with reduce's mandatory initial/returns
and the reason they are mandatory.

The bug test records what the reference documents showed that Mendix's
own guide does not — that a return type is stored for All and Any too —
and is re-runnable, so a second exec against an in-sync project must
report "Unchanged microflow".

Refs mendixlabs#1004

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
Four findings from the formula1 project: two build failures, a new check, and the upgrade skill
fix(describe): quote and escape MDL strings in one place (mendixlabs#1006)
fix(microflows): DESCRIBE emitted aggregate MDL that would not parse back (mendixlabs#1004)
fix(workflow): stop naming a jump activity after its target (mendixlabs#1005)
…ments

fix(describe): emit workflow annotations as comments, not statements (mendixlabs#1007)
main is red. TestDescribeWorkflow_NoAnnotationEmitsNoComment fails with

  unexpected violations [MDL-WF05] for a plain jump

and both PRs that produced it were green.

#351 added the test with a fixture that is a lone JumpToActivity whose
target does not exist, and asserted "no violations at all". #350 then
added MDL-WF05, which reports exactly that dangling target. Neither CI
run could see the other's change.

The rule is right; the fixture was not. It now carries the jump's target
as a real activity, so the workflow is valid and the assertion means
what it says rather than "no rule has been written yet that notices
this".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…L shards

fix-issue.md had reached 1.05 MB across 630 findings. That is past a
context window, past what GitHub's web editor will open, and past what
the wiki's own /mxcli-dev:wiki-sync can consume — its Phase 2 requires
reading a source in full, so the step that digests findings into
docs-wiki/bug-patterns/ pages has been blocked since the day those three
pages were written. The skill instructed everyone to read a file that
could not be read.

It had also stopped being a table. Only 227 of the 630 rows were still
under the header; the other 403 had accreted in twelve separate runs
through the document, most rendering as literal text rather than table
rows.

The findings are now one JSON object per line under
.claude/skills/fix-issue/findings/, sharded by area into nine files
(largest 423 KB). fix-issue.md keeps the procedure and drops to 37 KB.

Nine shards, not one file per finding: 630 files would trade a
too-large file for a directory nobody can scan, and the conflicts that
motivated sharding only occur between fixes touching the same area.

Records carry area / symptom / cause / file / insight, plus refs, ce
codes and MDL rule ids extracted from the text. 68 of the 630 rows could
not be split into four columns (an unescaped pipe, or a row that never
had four cells); those keep a verbatim `raw` field, and the format
documents that a record has either the four fields or raw.

The extraction is verified lossless: every record regenerates its
original row, and the multiset of 630 is byte-identical to what was
committed.

merge=union moves from fix-issue.md to findings/*.jsonl. That is the
right driver on the right unit — union applies file-wide, so on the old
mixed prose-and-table file two branches editing the same prose line
would have silently kept both. The note that shipped with it predicted
this exact fix. It does not stop PR conflicts either way, since GitHub's
server-side merge does not run merge drivers; sharding by area is what
reduces them.

make check-findings (scripts/check-findings.sh, wired into CI) validates
every line: DuckDB rejects a whole file on one bad line, so a typo in an
appended finding would take out every query over that area.

Pointers updated in CLAUDE.md, CONTRIBUTING.md, docs-wiki/README.md,
maintain-wiki.md and the four wiki pages that cite the table as a
source. SYNC_LOG.md is append-only history and is left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s-out-of-skill

# Conflicts:
#	.claude/skills/fix-issue.md
fix(test): make the no-annotation control a valid workflow (main is red)
refactor(skills): move the bug findings out of fix-issue.md into JSONL shards
`filter($L, Amount > 0)` was stored as `Microflows$FilterByExpression` with
the authored text verbatim. Mendix evaluates that expression once per item
with the item bound to `$currentObject`, where a bare attribute name is not
valid — so mxbuild reported CE0117 while `mxcli check` passed.

This is the unfinished half of bug #343. That fix rerouted `attr = value` to
`Microflows$Filter` (filter BY ATTRIBUTE), which takes a member name rather
than an expression and so accepts the bare form. Every other predicate still
fell through to the expression shape, which made the split turn on the
OPERATOR and be invisible to the author: `Status = 'x'` built and
`Status != 'x'` did not.

Not a Mendix version change, despite the report: the same 19-microflow probe
produces the identical 7 errors on mxbuild 11.11.0 and 11.13.0.

- A bare name that provably resolves to a member of the list's element entity
  is rewritten to `$currentObject/<member>` (an association keeps its module
  qualifier, an attribute does not). A name that does not resolve is refused
  rather than left to surface as CE0117. When the element entity cannot be
  determined nothing is proven either way, so the predicate is passed through.
- The predicate can reach the builder as a frozen `SourceExpr`, so the rewrite
  patches the source text too, skipping single-quoted literals — the `'Amount'`
  in `filter($L, Qty > 0 and Status != 'Amount')` must survive untouched.
- MDL-LISTOP01 refuses an iterator variable that is not in scope, pre-empting
  CE0109. It keys on scope rather than on the name, so `$item` stays valid as
  an enclosing loop's iterator — the shape of CLAUDE.md's O(N) `find` idiom.
- The `syntax microflow.list-operations` example no longer teaches a form that
  only compiles through the `=` reroute, and notes that SORT is not an
  expression, so a bare attribute is the only spelling there.

Control: stubbing the qualifier takes the repro from 0 to 7 × CE0117 with the
two `=` cases staying green, which is also the #343 regression guard.

Repros: mdl-examples/bug-tests/1002-filter-find-bare-attribute.mdl (0 errors
on mxbuild 11.13.0) and 1002-filter-bad-iterator.fail.mdl.

Closes mendixlabs#1002

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
ako and others added 28 commits September 1, 2026 07:40
87 of the 95 cmd/mxcli findings (92%) now belong to a named class. This
is the tooling surface rather than the model, and the classes look
nothing like the executor's.

- test-runner-cannot-fail (14). The one class whose cost is confidence
  rather than debugging time. `@expect 1 = 2` passed; `@verify` was
  parsed and read by nothing but --list; --require-assertions exited 0
  on a suite that asserted nothing. The mechanism is silent ABSENCE —
  an annotation parsed into a field no consumer reads — amplified by
  two result-assembly paths. The remedies that held are structural:
  fail closed on an annotation that cannot be honoured, one constructor
  and one pre-run verdict function, each pinned to a single call site.

- local-loop-silence (16). The warm loop orchestrates mxbuild, a JVM, a
  bundler, PostgreSQL and a browser, so the thing that breaks is not
  the thing that reports: a missing client bundle answers HTTP 200, a
  dead runtime leaves the CLI spinning, and the log being captured is
  not the log the user needs.

- styling-compiles-to-nothing (11). Nothing validates CSS. Location
  decides whether SCSS compiles at all, an unrecognised token is
  indistinguishable from a design never applied, and contrast is a
  correctness property no check measures.

- package-operations-damage (11). Handing the project to tools mxcli
  does not control, and being told it went fine — the MPR v2 to v1
  collapse reporting 0 errors is the worst of them.

- cli-contract-defects (14). The class with no Mendix document in it.
  It matters disproportionately because agents take the tool's word:
  help that teaches unparseable syntax, an unqualified "Check passed!"
  that resolved nothing, a flag that parses and does nothing.

Every sources: path and [[wiki-link]] verified before committing. Seed
table extended and five rows appended to SYNC_LOG.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82 of the 83 mdl/backend findings now fall under a named class. Only
three pages are new: most of the area reuses classes written for
mdl/executor and cmd/mxcli, which is the digest working rather than a
shortcut.

- engine-divergence (30). The dominant class here. Two implementations
  behind one interface, with the newer, less complete one as the
  default — so a gap is the behaviour most users get while tests formed
  against legacy still pass. A gap on one engine is invisible from
  inside that engine: everything is self-consistent, and the field that
  never existed is never missed. The cross-engine DESCRIBE matrix
  (write with A, read with B, all four) is the only check that sees it.
  Two failure modes and only one is honest — a refusal naming
  MXCLI_ENGINE=legacy costs a flag, a `-- Empty action` placeholder
  makes describe→exec delete the construct. The worst instance was a
  read that under-reported page access.

- mutator-addressing (19). ALTER edits a stored document in place,
  which means naming a node — and DataGrid2 columns, object-list items
  and layout regions store no Name at all. Derived names are unstable,
  ambiguity is refused rather than resolved, and hand-built BSON drifts
  from codec-built BSON in ways only a dump-diff shows. Getting a
  typed-array marker wrong turns a silent no-op into an unopenable
  project.

- access-rule-reconciliation (6). Small and graded high: GRANT looks
  additive and is a read-modify-write, so both widening and narrowing
  report success. Records two method points — the writer was innocent
  and a later reconcile removed the rules, and the reported trigger
  (WHERE) was a red herring that a scoped fix would have satisfied.

The ~33 MCP/PED findings are deliberately NOT given a page: that
subsystem already has architecture/mcp-backend.md and
models/ped-mutation-constraints.md, and restating them would break the
never-restate rule. Recorded as its own SYNC_LOG row so the omission
reads as a decision rather than an oversight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(skills): measure how far the bug-pattern digest has fallen behind
docs(wiki): digest the mdl/executor findings into nine bug-pattern pages
49 of the 53 mdl/grammar findings (92%) now fall under a named class.
Three pages are new; the rest reuse describe-round-trip-gaps and
silent-property-drop.

- capability-gap-as-parse-error (36 touched, ~20 primary). The
  distinctive one. Someone tries to express something Mendix supports,
  the parser says `no viable alternative`, and they conclude the FEATURE
  is impossible — a parse error is indistinguishable from a mistake they
  made. So the reports in this class are not feature requests, they are
  workarounds: an admin screen hand-rolled as five pages because a tab
  container was believed not to exist, "use a Java action" for a binary
  upload, "go to Studio Pro" as the standing answer to translation, a
  headless pipeline ending in a manual step.

  The gap is bidirectional — DESCRIBE of a document that HAS the
  construct must do something, and in the mapping findings it emitted
  MDL that parsed and rebuilt a different document, which is worse than
  a parse error. Also records that a narrow/wide statement pair (SET vs
  REPLACE) is a whitelist extended one bug report at a time, and that
  "not in the metamodel" is not a conclusion until the namespace is
  right.

- keyword-collisions (22 touched, ~8 primary). The distinguishing
  question is how a collision fails, not whether one happens: a parse
  error is recoverable, a different valid parse is not — a widget
  conditional calling trim() was silently dropped. Records that a
  grammar alternative and its visitor case are ONE change (accepting -7
  without the AST case serialized `[Amount > ]`), and the control-binary
  sweep for proving a relaxation causes no regressions.

- scripts-that-cannot-rerun (5). Statement-level idempotence, kept
  deliberately apart from ADR-0008's write-level idempotence, which is
  what people usually mean when they say mxcli is idempotent. `exec`
  halting on the first error turns "90% already applied" into "none of
  the remaining 10% applied", and the silent variant — a duplicated
  index — is worse than the error.

Coverage stated in CLAUDE.md was corrected to the measured 92% before
committing; the first draft claimed 98% from memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
27 of the 28 mdl/visitor findings (96%). Two pages are new and one is a
re-sync — the first any page has had since the initial synthesis on
2026-05-24.

- expression-translation-drift (13). Deliberately distinct from
  platform-semantics-gaps: there the MDL is illegal Mendix, here the MDL
  is correct, the emitted Mendix expression is well-formed, and it says
  something else. The worst instance is the quietest — an additive chain
  rebuilt with its operators reordered, so a microflow computed a
  different number than its source said with mxcli check, mx check and
  the build all green, and the corruption in the stored document rather
  than in DESCRIBE.

  Three ways meaning is lost (a literal changes type, an operator
  changes, a function resolves to the wrong overload), plus the ANTLR
  trap behind the standard fix: GetText() excludes hidden tokens and a
  source-interval slice includes them, so preserving raw source inherits
  every comment in that span.

- misleading-diagnostics (6). Graded above "the message could be
  clearer" because a wrong hint costs however long the reader spends
  acting on it: they blamed their quoting, renamed an attribute that was
  fine, or concluded a construct was unsupported. A hint's precision
  matters more than its coverage — the "unescaped apostrophe" hint
  matched any short lowercase word and so fired on every genuine error
  at `on`, `in`, `as`, `to`, `by`.

- visitor-wiring-gaps — RE-SYNCED. It described one size of gap (a
  field); the findings show three. A field, a structure (ELSIF arms,
  which Mendix has no native form for and which must be lowered into
  nested ifs), and a whole statement that parses, exits 0 and dispatches
  to nothing — the last presenting as an empty result rather than a
  missing feature, which is what lets it survive. Added the neighbouring
  failure where a field is wired to the WRONG thing, which reports
  success and changes meaning rather than losing it.

Every sources: path and every [[wiki-link]] across all 25 pages verified
to resolve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`mxcli test --local` could not start a runtime for any project:

    Error: local runtime: runtime admin API did not come up:
           runtime process exited during startup
    java.lang.IllegalArgumentException: Path
      '<project>/.mxcli/deployment-test/model/bundles' cannot be resolved
      in base path '<project>/.mxcli/deployment-test'

The tree had data/ and no model/ at all, because **mxbuild writes the
deployment to `<app dir>/deployment` and has no option to move it**.
Measured rather than inferred: `--target=deploy` on a project whose
deployment/ had just been deleted recreated it there, `mxbuild --help`
lists no deployment-path flag, and BuildRequest carries none.

So giving the test boot a deployment tree of its own moved where the
RUNTIME reads and not where the BUILD writes. StartLocalApp now refuses a
DeployDir the build will not populate, naming mxbuild as the constraint,
so a caller finds out at the point they can still act on it rather than
inside the JVM against a path they never chose.

The blanking that the scratch tree was meant to prevent — a headless test
boot's packaging pass deleting deployment/web/dist, leaving a concurrent
`run --local` serving HTTP 200 over a blank page — is fixed the only way
the constraint allows: the bundle is copied aside before the boot and put
back after. That costs a few MB of copy against the ~30s re-bundle that
made warning the earlier choice, and returns the exact bundle the dev loop
built. The restore writes only when the bundle is actually gone, so a
newer one built during the boot is never clobbered.

The tests that did not catch this are the lesson. Four of them asserted
DeployDir was set, was under .mxcli/, was per-project and was not the dev
loop's — and all four passed against a build that could not start, because
every one was about the option value while the symptom lived in what is on
disk after mxbuild runs.

Controls, both end-to-end on a real 11.13 project rather than in a unit
test: the pre-fix binary reproduces the reported JVM error byte for byte
while the fixed one runs the suite green; and with preserveWebClientBundle
stubbed out, a sentinel bundle is destroyed by a test run that still
reports every test passed — which is the silent failure §62 described.

Reported as mxcli-ledger FINDINGS §150; supersedes the fix for
mxcli-formula1 FINDINGS §62.
A copied Atlas layout still failed mx check with CE0463, and the reporting
project took it apart to one field: a full diff of Atlas' own brand image
against a describe -> rename -> exec copy, GUIDs masked, differs in one
line of 1480.

    === ONLY IN ATLAS ===  Object/Properties/21/Value/PrimitiveValue = '250'
    === ONLY IN MINE  ===  Object/Properties/21/Value/PrimitiveValue = '0'

Resolved through its TypePointer that is `maxHeight`, whose declared
default in Image 1.6.0 is 250.

Same class as the width/height fix that shipped alongside it, which did
not cover it — and why is the point. The reset was applied in a loop over
the definition's PROPERTY MAPPINGS, and a mapping is what gives a property
an MDL keyword. `width` and `height` have one; `maxHeight` has none, so it
was never visited and the widget template's captured value stood. The set
of properties that must be default-valued is the widget's editorConfig to
decide, not mxcli's; making it a subset of what MDL has words for was the
mistake.

Three things had to be true, and each was independently wrong:

  1. The reset must reach properties that have no mapping.
  2. A rule whose CONDITION is an unmapped property was always
     indeterminable, so it never fired — `maxHeight` is hidden when
     `maxHeightUnit` is "none", and nothing knew what maxHeightUnit was.
     The declared default is the right fallback precisely because MDL
     cannot name the property, so nothing can have moved it off it; a
     value the script set still wins.
  3. `def.PropertyVisibility` is EMPTY for every widget whose rules are
     lifted live from the .mpk, which is most of them. Keying the new
     lookup on that field found nothing at all, and only the end-to-end
     run showed it — both consumers now share visibilityRules().

Verified end-to-end on a real 11.13 project with the Image package patched
to declare maxHeight 250, since the version that does is not obtainable
here. Pre-fix binary writes 0 and all four mxcli-authored Image widgets
fail CE0463; fixed binary writes 250 and all four are clean. `minHeight`
stays at its own declared 0 throughout, so this is per-property from the
package rather than a blanket value. The project's ~60 Studio Pro-authored
Images stay stale under both, which is the package change itself and not
mxcli — the Step 0 discrimination the CE0463 skill asks for. On the
unpatched package, three Image configurations plus a datagrid still check
at 0 errors.

Reported as mxcli-ledger FINDINGS §142.
A reporting project ran `mxcli widget sync` on a widget that `mx check` was
reporting CE0463 on at that moment, and got:

    Every stored widget instance already matches its installed package.
    Nothing to do.

Both were right, about different things. The comparison is over the stored
SCHEMA — each property's declared type attributes — and the CE0463 was a
VALUE, in the widget's Object, which this command does not look at. The
message claimed the wider of the two.

It now says which plane it checked and points at `mxcli fix widgets` for
the rest. The command's own help already says it is partial; this stops the
one line a user actually reads from contradicting that.

Noted in mxcli-ledger FINDINGS §142.
…s against

The end-to-end guard two projects independently asked for, in the same
words. mxcli-sudoku §51 names it exactly: "an end-to-end assertion that one
`mxcli test --local` run leaves a model/ directory where the runtime is
told to look."

Both reports drew the same lesson, and it is about the tests rather than
the code. Four unit tests covered the change that broke `mxcli test
--local` for every project, and all four passed against a build that could
not start, because each asserted that the OPTION was set — under .mxcli/,
per-project, not the dev loop's — while the symptom lived in what is on
disk after mxbuild runs.

So this asserts the artefact: after a real build, the directory the runtime
will be booted against holds model/ and model/bundles — the exact path the
JVM named when this broke. It stops at the build, since the mismatch is
fully visible one step before a JVM and a database get involved.

MXCLI_IT_PROJECT points it at an existing project. Without it the test
scaffolds one with `mx create-project`, which produces a project at the
INSTALLED mxbuild's version — whose JDK may not be present, and the test
then skips. A guard that only ever skips proves nothing, which this repo
has already paid for once. For the same reason the scaffold does not use
t.TempDir(): it names the directory after the test function, and Mendix's
toolset rejects the result with PathTooLongException, which arrives as
another skip.

Control: reinstating the regression (runtime pointed at
.mxcli/deployment-test, guard disabled — main's current state) fails the
test naming both missing paths and the directory the runtime would boot
against; the fixed code passes.

Asked for in mxcli-sudoku FINDINGS §51 and mxcli-ledger FINDINGS §150.
Fix the mxcli test --local regression, the last CE0463 field, and add the guard both reports asked for
§62's blanking is fixed, and the deployment tree has another half that is
shared. A local test run recompiles the project's Java into
`deployment/run/bin`, which is the classpath a live `mxcli run --local` is
holding open. Measured on a real 11.13 project: after one test run all 134
class files have NEW INODES and byte-identical content — every one deleted
and rewritten. A JVM loads classes lazily, so one it has not reached yet
can fail afterwards:

    java.lang.NoClassDefFoundError: odatapushdown/QueryObject

What that costs is diagnosis, not the breakage. The microflows behind it
answer HTTP 200 with an EMPTY BODY — not a 500, not an error page — while
source-backed resources keep working, so half the app is fine and half
returns nothing, which is not a shape that suggests a test run did it. In
the reporting project it surfaced as 21 of 34 tests failing in a DIFFERENT
app, and 108 log lines went by before the two were connected.

mxcli cannot prevent it: mxbuild's Gradle pass owns the compile, and the
deployment directory cannot be moved — mxbuild writes it to
`<app dir>/deployment` and takes no option to change it. So it reports the
collision, which is the part that was missing, and says what the symptom
looks like and that the remedy is restarting that app.

It warns rather than refuses. The warm loop exists so an app can stay up
while you work on it, and the reporting project runs two apps that way as
a matter of course; refusing would break the workflow the feature is for.
Neither --attach nor --skip-build builds, so neither warns.

The fact needed — is a dev loop serving this project? — was already
published. `mxcli run --local` writes devLoopHandshake to
.mxcli/run-local.json for `mxcli constant set --apply`, carrying the pid
liveness check and the project identity this wants. A second state file
was written before that was noticed, and it would have CLOBBERED the
existing one, dropping the admin password and boot config that --apply and
--attach depend on. Reading what is already there is both correct and less
code.

Staleness is the whole feasibility question, not a detail: a `run --local`
killed, crashed, or ended by its development licence (§60, measured
lifetimes under six hours) leaves the file behind, and a warning driven by
the file alone would fire forever — a warning that is always wrong teaches
the reader to skip it. readDevLoopHandshake already refuses a dead pid, a
corrupt file and an absent one.

Verified end-to-end against a real `mxcli run --local`: the warning names
that app's actual pid and port, and the handshake still carries its
adminPass and 9 bootConfig keys afterwards. Controls: with the dev loop
stopped (handshake removed on exit) the same command is silent, and so is
a stale handshake with a dead pid.

Reported as mxcli-formula1 FINDINGS §81.
…as wrong

#259 shipped claiming no demo app contains an XML schema. It is 3
documents in 1 of 9 (OneHarness: XML_ECO, XMLRequest_Diagram,
XMLRequest_DiagramId).

The way the zero was produced is the part worth keeping. `grep -rl "XmlSchemas"
<extracted app>` can only work for MPR **v2**, where units are files under
mprcontents/. OneHarness is MPR **v1**: its units live in the SQLite
Unit.Contents blob, where the type string is not greppable from the file. The
grep silently answered "does any file contain this string" instead of "does any
project contain this document", and returned 0 for a reason unrelated to the
question.

Nothing about the fix changes. Failing open on an empty list is still right —
8 of 9 apps have none — and the reader is now verified against the three REAL
Studio Pro documents as well as the synthetic unit that first established the
$Type and Name keys: mxcli lists them with the correct module and names.

Corrected in the three code comments, the bug-test fixture, and the finding
record, which also now carries the read-units-properly lesson.

Refs #259

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Warn when a local test run will recompile a running app's classes
fix(mappings): correct the XML-schema corpus figure, and say how it was wrong
…d spot

Two problems, found by running `make digest-status` on main after the
stack merged.

**13 pattern pages never reached main.** All six PRs report MERGED and
only #356's content is on main: the stacked PRs each merged into their
BASE BRANCH, which by then was no longer on the path to main. GitHub was
telling the truth — they merged into what they targeted. This branch is
docs/bug-patterns-visitor, which holds all 25 pages, merged with current
main. Verified: 25 pages, 883 findings, every sources: path exists and
every [[wiki-link]] resolves.

**The digest report said 0% outstanding while 249 findings were
undated.** Undated records were skipped by the "since last sync"
comparison entirely, so an area could go completely undigested and the
headline would still read current — the exact failure the report exists
to prevent, in the report itself.

Three changes:

- the 249 undated findings are backfilled from git blame on the shards
  (mdl/executor gained 250 records from other sessions since the last
  pass);
- an undated finding now COUNTS AS NOT DIGESTED rather than being
  skipped, and the headline says "not yet digested" rather than "added
  since", which is what it always meant;
- `date` is now required by check-findings. Warning was not an option:
  249 records accumulated undated, and nobody saw the warning that was
  not there.

Two limits are documented in the script header rather than engineered
away, because both make the number look better than reality: `date` is
day-resolution and the comparison is strictly greater-than, so 256
mdl/executor findings carrying the sync's own date read as digested when
~250 arrived after the pages were written; and the blame-based backfill
invalidated the blame it read, so the field is now the only record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(findings): recover 13 lost pattern pages; close the digest's blind spot
The five bug-pattern PRs were stacked because they shared three files.
Everything else in them was disjoint — no two touched the same page — so
95% of the work was independent and all of it got serialised anyway.
Then the chain merged into itself and 13 pages never reached main.

Two of the three shared files did not need to be conflicts.

SYNC_LOG.md is append-only by its own stated rule ("Append only. Never
edit historical rows."), which is exactly the case merge=union already
covers for findings. Two syncs appending a row each now merge cleanly.

The coverage percentages in CLAUDE.md were a number that goes stale on
its own, and had: the sentence claimed 83% for mdl/executor while that
area had grown from 248 findings to 498. Replaced with a pointer to
`make digest-status`, which computes it.

That leaves the seed table in maintain-wiki.md as the only shared file —
one appended row per page, a trivial conflict that `git merge main`
resolves in seconds. File-wide union is deliberately NOT applied there:
it is prose with a table in it, and union on prose silently keeps both
sides of an edited line. That trade-off is already recorded in
.gitattributes from the fix-issue.md split.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The seed table was the last file forcing wiki PRs to be serialised: one
appended row per page, in the middle of a prose file where merge=union
would be a hazard rather than a help. It is now a table and nothing else
in .claude/skills/maintain-wiki/pages.md, with union in .gitattributes.
The prose stays behind and explains why the split exists.

Also carries 62b2525, which missed #364's merge by minutes: the union
rule for SYNC_LOG.md and the removal of the coverage percentages from
CLAUDE.md.

Two things found while doing it, both the same shape as the findings the
wiki digests.

The page list had DRIFTED. architecture/mcp-backend.md and
models/ped-mutation-constraints.md were added in June 2026 and were
still missing from it in September — three months, unnoticed, because a
table of contents has no failure mode of its own. Both rows added, and
scripts/check-wiki-pages.sh now asserts BOTH directions: a page absent
from the list is invisible to anyone choosing what to sync, and a row
naming no file is a page someone believes exists. Controlled by
truncating the list and watching it fail.

check-findings was NOT in CI. The findings README said it was, and a PR
body said it was, and neither was true. Both it and the new page check
now run in push-test.yml. The README's correction says what happened
rather than quietly editing the claim, because "we said it was wired and
it was not" is exactly the class of finding this corpus exists to hold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Height: n` on a domain-model annotation was a bare parse error —
"mismatched input 'Height' expecting {POSITION, CAPTION, WIDTH}" — which
reads like a missing mxcli feature, and was reported as one.

It is not. Mendix stores no annotation height: DomainModels$Annotation has
exactly Caption, ExportLevel, Location and Width, so the note auto-sizes to
its caption and there is nowhere to write a height into. Accepting the
property would mean inventing a key the platform does not have.

The metamodel in this repo could not settle that on its own — it is an
11.6.0 snapshot and the report was against 11.12.2, so a later property
would be invisible to it. Three further sources agree, measured on 11.13.0:
`mx dump-mpr` (Mendix's own serializer) emits those four keys on real
projects; `mx convert -p`, which rewrites the model through Mendix's own
object model and would materialise a property that merely had a default,
adds nothing; and the published Model SDK's domainmodels.Annotation lists
caption, exportLevel, location and width.

So the deliverable is an error that says which it is and names the levers
that do exist — the reporter's actual goal, a note that does not overlap the
entities below it, was reachable already:

- WIDTH is the height lever, because the box wraps: narrower is taller.
- ALTER ENTITY … SET POSITION moves what the note overlaps, so nothing has
  to be nudged by hand.

Any other unsupported property gets the four-property statement without the
height-specific advice. Both branches key on the `expecting {POSITION,
CAPTION, WIDTH}` token set rather than on the word "Height" — a page
widget's Height is valid MDL, and a name-keyed hint would misfire on it
(control: TestWidgetHeightIsNotAnnotationHinted).

Control: disabling the branches fails both hint tests and leaves the widget
control passing.

Refs mendixlabs#1014

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Clears code-scanning alert 2 (go/unvalidated-url-redirection) at
handleCallback. The vulnerability behind it was already fixed: the
look-alike-domain hole in the return-URL check went away when the guard
started comparing against the registrable domain instead of the
operator's spelling of it. The alert outlived the fix.

The query never saw the guard. go/unvalidated-url-redirection credits a
validator only when two things hold at once, and neither is about whether
the validation is correct:

  1. the callee name matches
     (?i)(is_?)?(local_?url|valid_?redir(ect)?)(ur[li])?
  2. the guard's true-branch dominates the read that reaches the sink

safeReturn failed (1), and reassigning `ret` inside the if-block and
falling through to one Redirect failed (2) -- the value reaching the sink
is a merge of the guarded value and the fallback constant, so the barrier
cannot attach to it.

Measured against CodeQL 2.20.3, all four combinations:

  safeReturn      + reassign      -> alert fires  (main today)
  isValidRedirect + reassign      -> alert fires
  safeReturn      + early return  -> alert fires
  isValidRedirect + early return  -> CLEARED

Both changes are required; neither alone does anything. The three firing
rows are the control -- without them this would be two edits that might
each be doing nothing.

Behaviour is unchanged: same hosts accepted, same fallback, and the
existing table test passes untouched apart from the rename. Both edits
stand on their own merits anyway -- the name says what the function
decides, and the early return handles the two outcomes explicitly instead
of mutating `ret` and relying on a later read.

The requirements are recorded in a comment at the function, because they
are invisible from the code and a well-meant rename or re-merge of the two
Redirect calls silently brings the alert back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
docs: move the wiki page list to its own file, and make it self-checking
docs(annotation): explain that Mendix has no annotation height (mendixlabs#1014)
fix(tunnelhub): let CodeQL see the redirect guard it already has
@ako
ako merged commit 8982492 into mendixlabs:main Sep 1, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants