Skip to content

Sync ako/mxcli: default styling with light/dark, and authoring fixes - #857

Merged
ako merged 34 commits into
mendixlabs:mainfrom
ako:main
Aug 6, 2026
Merged

Sync ako/mxcli: default styling with light/dark, and authoring fixes#857
ako merged 34 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

23 commits since the last sync (#853) — 129 files, +9,383/−386. Two themes: a
new default-styling feature, and a batch of authoring fixes found by building
real apps in MDL.

Default styling (mxcli theme, mxcli new --theme)

A generated app now looks like a product on first boot instead of a blank Atlas
app. Three themes ship in the binary — signal (default), ledger,
console — each with light and dark palettes.

  • A theme is files under theme/ only; the model is never touched, so it
    hot-applies under run --local --watch and cannot affect a build. Atlas Core
    is untouched, so projects stay upgradable.
  • Re-branding is one line (--mxt-brand); Atlas derives the whole colour ramp
    from it with color-mix().
  • --variant auto (default) follows prefers-color-scheme before first
    paint
    and honours a theme-light / theme-dark class. Mendix ships the
    :root.theme-dark slot but nothing that applies it, so
    mxcli theme switcher install adds the JavaScript actions and a nanoflow for
    a toggle — the one theme subcommand that writes to the model.
  • Fonts are vendored (SIL OFL 1.1) rather than fetched from a CDN, so generated
    apps render correctly air-gapped.
  • Generated regions are digest-fenced: a block carrying local edits is refused,
    not overwritten.

Three placement rules were settled against a real Mendix 11.13 project rather
than assumed, and two contradicted the existing skills: themesource/<name>/ is
only compiled when <name> matches a real module; theme/web/main.scss compiles
last, after Atlas Core and every module theme source; and
custom-variables.scss is imported once per module, so it holds declarations
only.

An independent test build (a full RSS-reader app, ~1,900 lines of MDL and ~1,400
lines of custom SCSS) then found three defects, all fixed in this range: a bare
theme remove targeting the built-in default instead of the installed theme, a
block orphaned in the shared Atlas map when switching themes, and a topbar
language selector measuring 1.13:1 contrast in every dark palette — now 17.79:1
light and 19.47:1 dark.

Authoring fixes

  • JavaScript action sources were written to the wrong directory — the module
    folder must be lowercased. MxBuild found nothing there, bundled a stub that
    throws JavaScript action was not implemented, and the action threw at runtime
    after passing check and building cleanly. Only reproduced on a
    case-sensitive filesystem.
  • GRANT now covers every entity member, including both-owner associations and
    audit members; audit-member rights are rejected at check time (MDL-SEC01).
  • Pages — cross-module association paths resolve in bindings; a parameterized
    microflow datasource gets its arguments bound; an unresolved association
    DestinationEntity is never written.
  • Workflows — context references resolve in every authored expression;
    DESCRIBE stops inventing a comment on jump to; a standalone annotation is
    refused rather than writing an unloadable model.
  • Microflows — the false branch of a conditional break/continue in a loop
    is wired.
  • ALTER PAGE — ambiguous DataGrid2 column references are rejected with the
    real names listed, detected page-wide rather than per-grid.
  • Mappings / REST — quoted identifiers are stripped

claude and others added 30 commits August 6, 2026 06:50
A blank Mendix app looks like a blank Mendix app. PROPOSAL_atlas_design_system
already established the method for fixing that, but its Layer-1 scaffold is a
template full of "TODO: your brand colour", so every generated app either stays
stock or gets a palette an agent invented on the spot.

This adds the default. `mxcli new` now applies the `signal` theme (cool slate,
one teal signal colour, 4px radius, 32px density, IBM Plex with monospace
numerics, a focus ring that is never suppressed, 44px touch targets below
768px); `--theme none` opts out, and `mxcli theme apply` adds it to an existing
project.

A theme is files under theme/ only — the model is never touched, so it hot
applies under `run --local --watch` and cannot affect a build.

Placement was settled against a real 11.13 project rather than assumed, and two
findings contradicted the existing skills:

  - themesource/<name>/ is compiled ONLY when <name> matches a real module.
    An invented folder is silently skipped — build succeeds, rules absent.
  - theme/web/main.scss compiles LAST, after Atlas Core and after every module
    theme source, so a partial imported from it wins without !important. That
    is the correct home for app-level styling.
  - theme/web/custom-variables.scss is imported once per module, so it holds
    declarations only.
  - Mendix 11 Atlas is CSS-custom-property-first; the derived brand ramp is
    color-mix() against var(--brand-primary), so one token re-brands the app.

Files the project already owns are written as digest-fenced blocks: the closing
marker records a hash of the body, so a block carrying local edits is refused
rather than overwritten (guard-don't-drop, as in ADR-0005). remove restores the
originals byte for byte and prunes only empty directories.

Fonts are vendored (IBM Plex, SIL OFL 1.1) rather than @import-ed from a CDN:
no @import-ordering trap, no third-party request per page load, and generated
apps render correctly air-gapped. The embed needs `all:` — a plain go:embed
skips _-prefixed files, which is how SCSS spells a partial.

Verified beyond the build log: mxbuild --target=deploy on a real 11.13 project
(tokens present, 7 @font-face blocks, Layer-2 rules after all Atlas
components), then run --local + Playwright against the running app
(body font IBM Plex Sans, --brand-primary #0f6e6b, --border-radius-s 4px,
input height 32px, woff2 served 200). That loop caught one real defect: the
`num` class rendered a field's label in monospace, because Mendix nests the
label inside the widget root the class lands on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEZmExJUvn2nWTWE9mrd4i
Quoting identifiers is the documented way to avoid MDL keyword
collisions, and quotes are stripped generically everywhere else. Inside
an import or export mapping body the entity and association names were
read with ctx.QualifiedName().GetText(), which returns the raw parse text
including the quotes, so the reference was stored as `ZZB."Routing"`.

mxcli reported success with no warning; Mendix then failed the build:

    [error] [CE1613] "The selected entity 'ZZB."Routing"' no longer
      exists." at Object mapping element 'Root'
    [error] [CE1613] "The selected attribute 'ZZB."Routing".RouteId' no
      longer exists." at Value mapping element '_id'

That second message names the bug: it mixes a quoted entity with an
unquoted attribute, because the attribute half already went through
identifierOrKeywordText. A half-stripped stored name means one of the two
readers is raw.

Route every qualified name in a mapping body through buildQualifiedName,
the helper the rest of the visitor already uses — five sites across the
import and export builders: root entity, nested association and entity,
and the value-transform converter.

Verified end to end on Mendix 11.13.0: the reporter's script went from
3 errors (CE1613) to 0 under mx check.

Closes mendixlabs#842

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUToAkUx54bNkNjsBpufRH
A widget datasource bound to a microflow that takes parameters dropped
its argument bindings, so mxbuild reported:

    [error] [CE1571] "No argument has been selected for parameter 'Name'
    and no default is available." at Data grid 2 'dg1'

while mxcli check and exec both reported success.

The grammar already parsed `microflow Mod.MF(Name: $x)` into
DataSourceV3.Args, but the builder never read them and
pages.MicroflowSource had no field to hold them — so nothing was dropped
by any single line; the struct in between simply could not carry the
value. The writer then passed a literal nil at all three datasource
sites, under a comment asserting datasources never have mappings. That
was true when only actions could take arguments, and stale once the
grammar accepted them on a datasource.

MicroflowSource and NanoflowSource now carry ParameterMappings, the
builder fills them from ds.Args, and the writer passes them through.
The conversion is factored into flowArgsToParameterMappings and shared
with the call-action path, so the $-variable-vs-expression rule cannot
drift between the two — if it did, the same binding would reach Mendix
through different BSON fields depending on where it was written.

Known remaining gap: `describe page` does not yet emit the mappings, so
a describe -> exec round-trip still loses them. That is a read-back bug
of the same shape as mendixlabs#839; the write path this issue reports is correct
and the repro now builds clean.

Verified end to end on Mendix 11.13.0: the reporter's construct went from
CE1571 to 0 errors under mx check.

Closes mendixlabs#835

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUToAkUx54bNkNjsBpufRH
…bs#843)

A `create rest client` operation reported success, but `describe rest
client` omitted Query/Parameters/Headers and always printed
`Response: none`. Dumping the stored BSON showed the query parameters and
headers written correctly while ResponseHandling was
Rest$NoResponseHandling — the response mapping was gone. `mx check`
reported 0 errors, so nothing anywhere complained.

Three independent defects behind the one symptom.

Write path. model.RestClientOperation documents BodyType/ResponseType as
upper-case tokens, and every consumer compares against that spelling: both
serializers and the REST-call microflow builder. The MDL executor stored
the visitor's lower-case source text, so `op.ResponseType == "MAPPING"`
never matched and the mapping fell through to the else-branch — which
legitimately writes "no response handling". Nothing errored because that
is a real outcome for an operation without a mapping; the mismatch was
laundered into a plausible model. Normalized with strings.ToUpper at the
one place the AST becomes the semantic model, and made the two serializer
comparisons EqualFold so the same landmine is not left armed for the next
producer (the OpenAPI import path already emits the upper-case form).

Read path. restOperationFromGen populated only Name/HttpMethod/Path/
Timeout and type-asserted Rest$RestParameter for both parameter lists,
while the writer emits Rest$OperationParameter and Rest$QueryParameter —
two different gen types, so both assertions failed to ok=false and
skipped every item. Headers and ResponseHandling were not read at all.
Now reads parameters (with DataType), query parameters, headers, the
response handler and the body, including the Import/ExportMappings
element trees. This also repairs shouldSetBodyVariable, which could never
see EXPORT_MAPPING because BodyType was never populated.

Unsupported reference syntax. `Response: mapping Mod.IMM_X` — what the
reporter wrote — names an import mapping *document*. The clause expects a
target entity plus a `{ ... }` body, and Mendix has nowhere to store a
document reference: Rest$RestOperationResponseHandling has exactly two
implementations, inline-mapping and none. It parsed, contributed no
entries, and was written as "none". Now refused at exec time and at
`mxcli check` time (MDL-REST01, no project required), with the inline
form spelled out in the message.

Rest$QueryParameter stores no DataType, so the MDL type is decorative and
dropped at write time. describe now re-emits query parameters as String
rather than an empty type, which did not re-parse.

Verified on Mendix 11.13.0: reporter's script now fails loudly instead of
silently; the corrected script stores
Rest$ImplicitMappingResponseHandling with a full
ImportMappings$ObjectMappingElement tree, mx check reports 0 errors, and
describe -> exec -> describe is byte-identical. Each of the three fixes
was reverted independently to confirm it is the cause of its test's
failure.

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

Fix: strip quotes from identifiers in import/export mapping bodies (CE1613)
Fix: bind arguments for a parameterized microflow datasource (CE1571)
Fix: persist and read back consumed REST operation mappings (mendixlabs#843)
…mes on miss (ledger #78)

DataGrid2 columns carry no stored name in the Mendix model, so the authored
MDL name (`column colFoo (...)`) is dropped on write and a column is addressed
by a derived name (the bound attribute, else the caption, else col{N}). Two
hazards followed:

- a bare `ON <name>` that matched more than one column (duplicate captions on
  dynamic-text/custom columns collide) silently mutated the first and reported
  "Altered page", leaving the second unreachable;
- addressing a column by a name that resolves to nothing gave a bare
  "widget not found" with no hint that columns use a derived name.

Persisting the authored name is not possible — columns have no name slot, and
inventing one is the Studio-Pro-won't-open hazard ADR-0005 guards against. So
implement the finding's fallbacks:

- the bare-name resolver (findInWidgetArray) now counts all column matches
  (bsonWidgetResult.matchCount) instead of returning the first, and the
  explicit-column resolver (findBsonColumn) returns an error;
- every mutating entry point (SetWidgetProperty, DropWidget, InsertWidget,
  ReplaceWidget, InsertColumns, ReplaceColumn) rejects matchCount > 1 with an
  ambiguity error, and a miss lists the addressable derived names + explains the
  model (run DESCRIBE PAGE).

Verified on Mendix 11.12.1: ON an authored name errors with the available
column list; ON a duplicate-caption name errors instead of silently mutating
(A/B: the pre-fix binary reports "Altered page"); ON a unique derived name
still works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…-grid (ledger #78 follow-up)

The within-grid ambiguity guard shipped in 751fd8e missed the cross-grid case:
two different grids on one page each carrying a same-named column (e.g. an
attribute column bound to the same attribute) still resolved to the first grid's
silently. Consolidate onto a single page-wide count (columnMatchCount, built on
the existing collectColumnNamesBson walk) used by every mutating entry point,
replacing the per-grid matchCount field. The ambiguity message now names both
causes (duplicate captions in one grid, or a same-named column across grids) and
both remedies (distinct captions, or a grid-qualified `ON gridName.column`).

Verified on Mendix 11.12.1: `ON Merchant` with a Merchant column in each of two
grids now errors instead of mutating the first; `ON dgA.Merchant` disambiguates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…e in a loop (ledger #52)

`loop { if <cond> then break; }` where the if is the last statement built an
ExclusiveSplit with only its `true` outgoing flow (→ break event). The `false`
case was deferred to a following statement that never came, so mx check reported
CE0079 ("the 'false' condition value should be configured in properties for an
outgoing sequence flow") and the microflow would not deploy. This is distinct
from mendixlabs#791 (a dropped Break/Continue event → project-load crash); here the event
serializes fine but the decision is missing a flow.

Root cause: the loop-body flow builder (addLoopStatement) is a simplified copy
of buildFlowGraph that connected body statements with a plain newHorizontalFlow
and never honoured the deferred nextFlowCase a merge-less split leaves for its
false branch. Mirror buildFlowGraph: carry pendingCase between body statements,
and for a leftover pendingCase at the end of the loop body synthesize a
ContinueEvent and wire the split's false flow to it — the valid Mendix
representation of "didn't break, continue to the next iteration".

The pre-existing acceptance test only checked that MDL051 no longer rejects the
source; it never ran mx check on the output, so the CE0079 microflow shipped
green. Add a builder-level test asserting the split carries both a true (→Break)
and a false (→Continue) flow.

Verified on Mendix 11.12.1: break-last, continue-in-conditional, break-then-more,
and nested-if-break all pass raw mx check with 0 errors (was 1× CE0079).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…able model (issuetracker #15)

`annotation '...'` in a workflow body passed `mxcli check`, executed, and then
produced a project Mendix could not LOAD:

  System.InvalidOperationException: Type Mendix.Modeler.Workflows.Model.Annotation
  does not contain a constructor with a parameter of type ...Model.Flow

Not a build error — Studio Pro would not open the project and `mx check` died
before validating anything, so the whole project went down rather than one
document.

Root cause is placement, not the storage name: mxcli writes the annotation into
the workflow's activity flow, and Mendix constructs every child of that list
with a Flow parent. Neither Workflows$Annotation (Description only — it attaches
to a Flow) nor Workflows$FloatingAnnotation (the canvas sticky note, which has
exactly the RelativeMiddlePoint/Size fields already being written) accepts one;
swapping the $Type reproduces the identical error with the new name. No struct
in the generated workflow model owns a FloatingAnnotation list, so the correct
container is not determinable without a Studio Pro reference.

Refuse the construct instead of emitting a structurally invalid unit: MDL-WF04
at check time, and a hard error in execCreateWorkflow so skipping `check` cannot
corrupt a project either. The workflow skill that documented the statement now
warns against it and points at an MDL comment.

Verified on Mendix 11.12.1: exec refuses with an actionable message and the
project still checks 0 errors, where the pre-fix binary left it unloadable.

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

A page datasource navigating an association whose other end is a System entity
wrote `DestinationEntity: ""`. That is a by-name reference Mendix resolves to
null, so the project could not be LOADED:

  System.InvalidOperationException: An error occurred when trying to set the
  'DestinationEntity' property of a Entity ref step in a Page with ID ...
  ---> System.ArgumentNullException: Value cannot be null. (Parameter 'value')

Not a build error — Studio Pro would not open the project and `mx check` died
before validating anything, so the whole project went down rather than one page.

Root cause: resolveAssociationDestination resolves both ends through
entityQNByID, which only sees the project's own domain models. An association
ending in System yields "" for that side, the context then matches neither end,
and the fallback returned the empty child.

Two changes: prefer whichever end actually resolved and is not the context, and
— decisively — refuse to write an unresolved destination instead of emitting a
structurally invalid unit. The error points at the explicit
`Assoc/Module.Entity` form, which is verified to build 0 errors and is exactly
the construct the reporter had to abandon.

Narrower than reported: the finding attributed this to *nesting*, but a
single-step probe with the same association reproduces it identically — nesting
was incidental.

Verified on Mendix 11.12.1: the explicit form checks 0 errors, an ordinary
same-module association datasource is unchanged, and the original repro is now
refused instead of leaving the project unopenable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
MDL spells the same concept differently per document type — binding a parameter
to a call has three spellings (microflow `(p = expr)`, page `(p: expr)`, workflow
`with (p = 'string')`), and `annotation` means "attach a note" in microflows but
was a separate, project-corrupting statement in workflows.

This violates the project's own rule in design-mdl-syntax.md ("never create a
second syntax for the same concept") and has a measurable quality cost: every
workflow defect from the two external test projects — issuetracker #15/#16/#17
and ledger #39/#41 — sits in a workflow-only construct or its bespoke write
path, i.e. code with no second consumer keeping it honest.

The proposal argues for aligning only where the syntactic difference does not
track a semantic one:

  in scope   — call-argument binding, activity notes/decorators, boolean
               decisions (all route into write paths that already exist, so no
               new BSON and no change to any stored shape)
  out of scope — outcomes (first-class named model objects), boundary events,
               targeting, due dates, and `{}` vs begin/end (cosmetic churn)

Guiding rule: same spelling for the same concept; keep distinct spellings where
the semantics differ. Notably a workflow body has no variables or assignment, so
making it look like imperative microflow code would mislead — which is the case
against over-unifying.

All proposed changes are additive with permanent aliases; the standalone
`annotation` statement stays refused (MDL-WF04) because its container is still
unknown. Records the rejected SQL-shaped alternative (unify on `comment`) and
four open questions, including the canonical DESCRIBE spelling.

Status: draft — open questions remain and the direction is not yet signed off.

The README index is regenerated via scripts/gen_proposals_readme.py, which also
picks up three proposals that had drifted out of it (microflow_debugger,
hub_authentication, marketplace_module_upgrade); 84 -> 88 active.

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

The reported CE6680 ("Target is required") and CE0495 ("Duplicate name") no
longer reproduce: the activity deduplicator already renames the Jump (Name
"Triage2" alongside the user task "Triage") and TargetActivity resolves, so the
workflow builds 0 errors on Mendix 11.12.1.

What remains is the round-trip artifact the finding leads with. buildJumpTo
defaults Caption to the TARGET name, and DESCRIBE echoed Caption unconditionally
as a `comment` clause, so a plain

  jump to Triage;

came back as

  jump to Triage comment 'Triage';

— a comment nobody authored. Emit the clause only when the caption carries
information the author wrote (non-empty, and not the derived target or activity
name). Re-applying the shorter form rebuilds the same Caption, so dropping it is
lossless.

Two existing tests had codified the old behaviour and were updated: the
"name fallback when caption empty" case in the describe test asserted the
phantom comment, and the two issue-619 quoting tests matched `... comment`,
which coupled a quoting assertion to it. The quoting checks now pin the whole
clause (`jump to "List";`), which is a stricter test of what they were about.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
A workflow `decision '<expr>'` referencing the context passed `mxcli check`,
executed, and then failed the Mendix build with CE0117 "Error(s) in
expression.". Only `$WorkflowContext/X` in exact casing worked.

mxcli always stores the context parameter as `WorkflowContext`, and Mendix
expressions are case-sensitive. Two spellings therefore reached Mendix as
undefined variables:

  * `$workflowContext` — the spelling write-workflows.md documented.
    `normalizeWorkflowContextExpr` existed but was wired into
    `autoBindCallMicroflow` only, so `with (...)` mappings were normalized
    while a decision's condition was written through verbatim.
  * `$Ctx` — whatever the author declared in `parameter $Ctx:`. The visitor
    parsed it into `ast.CreateWorkflowStmt.ParameterVar` and nothing ever
    read that field, so the declared name resolved to nothing.

Replace the single point fix with a `contextExprNormalizer` that aliases the
declared name onto the stored one (whole-word, so `$CtxItem` is untouched) and
normalizes casing, threaded through `autoBindActivitiesInFlow` so it reaches
every expression an author can write: decision conditions, user task due dates
and XPath targeting, wait-for-timer delays, and call-microflow mappings.

Verified on Mendix 11.12.1: pre-fix the project reports CE0117, post-fix
`mx check` reports 0 errors and all three spellings round-trip through
`describe workflow` as `$WorkflowContext`.

Repro: mdl-examples/bug-tests/it-17-workflow-context-expression.mdl

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
A widget bound through an association whose target lives in another module —
`Attribute: Issue_Assignee/Name` — passed `mxcli check`, executed, and then
failed the build with CE1613 "The selected attribute
'IT.Issue.Issue_Assignee/Name' no longer exists". The error text is the raw MDL
path glued onto the context entity: resolution failed and the writer fell back
to a flat attribute name instead of an AttributeRef carrying an
IndirectEntityRef of hops.

A domain model keeps associations in two lists. `Associations` holds the
intra-module ones (both ends BY_ID); an association targeting another module is
a DomainModels$CrossAssociation in `CrossAssociations`, where only the local end
is BY_ID and the remote end is the BY_NAME `ChildRef`. Both resolvers searched
only the first list.

The finding blamed the System module, but a plain second app module reproduces
it identically — the trigger is cross-module, not System.

Two sibling defects in the same area:

  * A ComboBox's `Association:` was qualified with the module of its own option
    list, because the `DataSource:` mapping runs first and moves
    pageBuilder.entityContext. A bare `Issue_Assignee` on a ComboBox over
    System.User became `System.Issue_Assignee` → CE1613. An association belongs
    to the containing entity, so it now resolves against the context as it
    stood when the widget's Build started.
  * `CreatedDate: AutoCreatedDate` is the spelling mxcli requires when declaring
    an audit member — it rejects any other name and tells you to use this one —
    but the member is stored as `createdDate`, so binding the name you just
    declared failed CE1613 while the undocumented lowercase form worked.

Also routes resolveAssociationDestination, entityQNByID, and moduleNameByID
through the same cached hierarchy/domain models the sibling resolvers use.

A/B on Mendix 11.12.1: pre-fix binary writes 4 x CE1613, fixed binary checks 0
errors.

Repro: mdl-examples/bug-tests/it-19-cross-module-attribute-path.mdl

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

GRANT rejected members Mendix does recognise, and a rule that looked complete
still failed the build:

  Error: entity IT.Label has no member(s) Issue_Label; ...
  Error: entity IT.Issue has no member(s) changedDate, createdDate; ...
  [error] [CE0066] "Entity access is out of date."

CE0066 makes partial coverage worse than no rule at all, so this pushed users
toward declaring no entity access.

Two unrelated gaps in what counts as a member:

  * `OWNER Both` makes an association a member of BOTH ends. The writer emitted
    the MemberAccess only for the FROM entity (ParentID), and
    ReconcileMemberAccesses independently applied the same FROM-only rule — so
    it stripped the entry back out on the next write even when the executor had
    added it. Both places now include the TO side when the owner is Both;
    OWNER Default keeps the FROM-only behaviour, since an entry on the TO side
    is itself a CE0066.
  * Audit members are entity flags, not entries in entity.Attributes, so the
    member walk never yielded them and naming one was reported as "no member".

Emitting a MemberAccess for an audit member looked like the symmetric fix for
the second case, but mxbuild rejects it with CE0066 and an entity storing them
checks clean with no entry — their access comes from the rule's default. So
naming one is accepted, and asking for rights that differ from the default is
refused with that reason instead of being silently dropped.

A/B on Mendix 11.12.1, same module in the same project: pre-fix binary produces
CE0066 plus the bogus rejection, fixed binary checks 0 errors. Control: the
identical model with OWNER Default checks clean pre-fix, so the owner mode is
the trigger, not the reference set.

Repro: mdl-examples/bug-tests/it-20-grant-member-coverage.mdl
       mdl-examples/bug-tests/it-20-grant-audit-member-rights.fail.mdl

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
main fixed the same defect independently (issue mendixlabs#835, PR #98) and already
carries a row for it. The row this branch added described an implementation
that is not in this change and cited a repro file the rebase dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
CI caught the gap: `it-20-grant-audit-member-rights.fail.mdl` is a negative
test that must fail `mxcli check`, but the refusal added for issuetracker #20
lived only in the exec path, so `check` passed a script `exec` rejects — the
round-trip `check` exists to avoid.

Add MDL-SEC01 as a no-project validator, mirroring the check/exec parity the
MDL-WF04 fix used. No project is needed to recognise the member: mxcli already
reserves createdDate/changedDate/owner/changedBy, so a GRANT naming one always
means the audit member. Naming one at the rule's own default stays legal (it is
a no-op); only rights that differ from the default are flagged, which is
exactly what the executor refuses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Three -tags integration round-trip tests asserted that a standalone
`annotation` in a workflow body survives write → read → describe → re-execute,
and went red on the MDL-WF04 guard from issuetracker #15.

It does survive that loop — mxcli's own reader is tolerant — but the loop never
loaded the project in Mendix, so it proved nothing about validity. Re-settled
against real mxbuild 11.12.1 by stubbing the guard and writing the construct:
`mx check` dies at "Loading the mpr file" with

  System.InvalidOperationException: Type ...Workflows.Model.Annotation does not
  contain a constructor with a parameter of type ...Workflows.Model.Flow

so the guard is right and the tests were pinning the defect in place.

TestRoundtripWorkflow_AnnotationActivity and _AnnotationBeforeActivity are
replaced by TestCreateWorkflow_StandaloneAnnotationRefused, which locks in the
refusal for both shapes. _Comprehensive keeps all its other coverage with the
two annotations removed.

This is the second time in this branch that a green test was codifying a real
bug (the first was the phantom `jump to` comment), so the symptom-table row now
carries that as a generalisable warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Fix ledger + issuetracker findings: two unloadable-model defects, CE0117/CE1613/CE0066
A JavaScript action created by mxcli parsed, passed `mxcli check` and built
cleanly, then threw "JavaScript action was not implemented" the moment it ran.

mxcli wrote the source to javascriptsource/<ModuleName>/actions/ using the
module's own casing. Mendix reads a lowercased directory — a blank Mendix 11 app
ships javascriptsource/nanoflowcommons/, /datawidgets/ and /webactions/ for
modules named NanoflowCommons, DataWidgets and WebActions. Finding nothing at
the path it reads, mxbuild generates a stub whose body is
`throw new Error("JavaScript action was not implemented")` and bundles that.

Both writers carried a comment asserting the opposite ("unlike javasource, which
is lowercased"), so the belief was documented rather than tested. It only
reproduces on a case-sensitive filesystem: on macOS and Windows the two
spellings are the same directory, which is why it survived.

Nothing short of running the app catches this — parse, check and build all pass,
and the generated stub is valid JavaScript. Found while wiring a theme toggle;
verified end-to-end by clicking the button in a browser.

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

Dark mode was deferred in the previous commit on the strength of the finding in
PROPOSAL_atlas_design_system that Atlas widgets are light-only, so a
prefers-color-scheme flip yields a half-dark app. That finding does not hold on
Mendix 11 and the deferral was wrong.

Measured by adding `theme-dark` to <html> on a running app and changing nothing
else: page ground, cards, form controls, sidebar, buttons and DataGrid2 all
followed. Mendix 11's Atlas is CSS-custom-property-first, so the cascade
genuinely propagates. The class sits on <html>, which also disposes of the old
objection about popups and modals rendering at <body>.

Everything that broke in that experiment was Signal's own fault — it had pinned
--font-color-default, the pill tints and the neutral surface to literal colours.
That drove the restructure: a theme is now a palette of --mxt-* tokens, a shared
Atlas map expressing ~60 Atlas variables in terms of them, and a theme partial
holding the second palette. A variant restates ~30 tokens, never the wiring.

  mxcli theme apply <name> [--variant auto|light|dark]
  mxcli theme switcher install -p app.mpr [--module M]

--variant auto (default) follows the OS before first paint and honours a
theme-light / theme-dark class. The explicit block must be declared after
Mendix's own _theme-dark.scss — same specificity, later wins — or the app
reverts to stock Mendix blue whenever the class appears. `light` needs no block:
the media query carries :not(.theme-light), so it falls through to the base.

Adds ledger (warm paper, hairline rules, Source Serif over Source Sans, 2px
radius) and console (dark-first, Space Grotesk over JetBrains Mono, 6px radius).
Applying one removes the other; the Atlas map is shared and a test asserts the
three copies have not drifted.

`theme switcher install` is the only theme command that writes to the model, and
it has to be: Atlas ships the :root.theme-dark slot but nothing that applies it,
and there is no theme-level hook to run script before first paint (index.html is
generated by mxbuild, settings.json accepts only cssFiles). Known limit: a
reload falls back to the OS, because Mendix has no page on-load event and the
usual substitute — a data view with a nanoflow data source — is not authorable
on either engine (modelsdk refuses NanoflowSource; legacy writes it without the
nanoflow reference and fails the build). ApplyStoredTheme ships ready for it.

Two Atlas constraints found by measuring, not reading:

  - Several topbar widgets paint text with --color-base, assuming white because
    they assume a dark rail. Ledger's paper rail (faithful to the concept) made
    the language selector invisible. All themes keep a dark rail and the shared
    map forces color: inherit on those widgets.
  - --font-color-contrast drives topbar text, not only text on a brand fill, so
    it tracks the rail; button text comes from --btn-*-color.

Verified in a browser across all six theme/variant combinations: correct palette
under each prefers-color-scheme, the toggle flips and persists, fonts resolve,
and mxbuild builds clean for each theme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEZmExJUvn2nWTWE9mrd4i
Documentation and help pass over the theme work, plus the two defects the
review turned up.

Docs:
  - atlas-design.md taught a hand-rolled SCSS `!default` scaffold with TODO
    placeholders. That is wrong twice over now — Mendix 11 Atlas is
    CSS-custom-property-first, and a theme already occupies that file, so an
    agent following it would write a Layer 1 that fights the shipped one and
    trips the digest guard. Replaced with the token architecture and how to
    re-brand it. This skill is synced into every project by `mxcli init`.
  - Its dark-mode section still said "commit to one theme" on the strength of
    the Atlas 3 finding. Rewritten around what was measured on 11.13.
  - `mxcli theme --help` described the two-file layout from the first commit
    and never mentioned variants, the other themes, or that `switcher` writes
    to the model. `theme switcher install` had only a one-line Short.
  - CHANGELOG had no Unreleased entry, so `mxcli changelog` showed nothing.
  - Added mdl-examples/doctype-tests/37-theme-switcher-examples.mdl (the MDL
    `switcher install` generates) and the bug-test the checklist wants for the
    javascriptsource fix, with the assertion that catches it.

Review findings:
  - SwitcherStorageKey sat beside four hardcoded copies of the same string in
    the template, so the constant could drift from the JavaScript that reads
    it — and the test asserting Contains(mdl, SwitcherStorageKey) would have
    kept passing, because it matched the literal rather than the link. The key
    is now substituted, and the test checks expansion and occurrence count.
  - "Copy the scaffold below" survived two paragraphs above the new "do not
    hand-roll a scaffold" section, contradicting it outright.

Also adds a test asserting each theme defines the variant mixin it includes and
reaches its alt palette from both the media query and the explicit class. SCSS
is never compiled by the Go tests, so a name mismatch would otherwise ship and
fail at the user's build.

Recurring-findings table gains rows 18-20 for these classes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEZmExJUvn2nWTWE9mrd4i
`retrieve $L from M.T where [Amount > -7]` failed to parse:

  Parse error: extraneous input '7' expecting {',', ')'}

`xpathWord` — the rule for a name part inside XPath — is a negated token set
that did not exclude MINUS, so the sign was consumed as a name word and the
digits were left stranded. That is why the report described it as "negative
numeric literals truncate (-7 becomes -)": from the outside the sign appears to
swallow the number.

The lexer deliberately keeps '-' out of NUMBER_LITERAL (a leading sign there
mis-tokenises `$x -2`), leaving negation to the parser. The general expression
grammar has unaryExpression for exactly this; the XPath grammar never got the
equivalent.

Two halves are required, and the grammar half alone is worse than the bug: with
the parser accepting -7 but buildXPathValueExpr having no case for the new
alternative, the constraint parses and silently serializes to `[Amount > ]` — a
dropped operand instead of a loud parse error. Reverting either half is proven
to fail the new tests.

  - MDLPage.g4: xpathValueExpr gains `MINUS xpathValueExpr`; MINUS added to the
    xpathWord exclusion set (a hyphen inside a name already lexes as a single
    HYPHENATED_ID, so a standalone '-' is never part of a name).
  - visitor_xpath.go: build a UnaryExpr for it.
  - visitor_page_v3.go: serialize unary minus as `-7`, not `- 7`.

Verified on Mendix 11.12.1: negative integers, negative decimals and compound
constraints write with the sign intact and `mx check` reports 0 errors.
Hyphenated XPath functions (starts-with) still parse.

Scope note — the finding's own example line,
`[DueDate > addDays([%CurrentDateTime%], -7)]`, still fails. `addDays` is a
microflow expression function, not an XPath one, and it fails CE0161 with a
POSITIVE argument too, so the sign was never its problem. The xpath-constraints
skill now documents that, with the variable-first workaround.

Addresses issuetracker finding #18.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
fix(xpath): accept a negative numeric literal in a constraint (issuetracker #18)
feat(theme): default styling with light/dark palettes and runtime switching
claude and others added 4 commits August 6, 2026 19:59
The per-session endpoint tables were auto-laid-out, so each session card
sized its columns from its own longest cell and no two cards lined up.
Give the tables `table-layout: fixed` and a shared colgroup, with a
min-width on both the card and the table so a narrow viewport scrolls the
whole set together in `.wrap` rather than squashing cards independently.
URL takes the flexible remainder (it is the column worth reading in full);
overlong cells ellipsise and carry the full value as a tooltip.

Add a "First seen" column. A live backend's RegisteredAt restarts every
time a reaped container reconnects, so it cannot answer "how long has this
URL existed?" — EndpointView.FirstSeenAt is resolved from the durable
session log instead, taking the earliest sighting of the endpoint slot.
SessionView.FirstSeen now derives from that too. Timestamp cells show the
absolute time on hover.

Also add an attribute escaper for the values interpolated into HTML
attributes; esc() escapes markup but leaves quotes alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKdcQRPJZxTWq87SH34WYy
…ck, dark topbar unreadable

Three defects reported from the RssReader test build (MXCLI-FINDINGS 15-17),
each fixed with a regression test proven to fail against the old behaviour.

15. `mxcli theme remove -p app.mpr` — the invocation the docs show — targeted
    the built-in default rather than the theme actually installed. On a project
    themed with ledger or console it reported every file as unchanged, exited 0,
    and left the theme fully in place: a silent no-op on the documented command.
    Both apply and remove now resolve the target from the mxcli:theme markers.
    Remove has no fallback — an unthemed project is an error, not a no-op.
    Apply keeps one, since a project with no theme is exactly when installing
    the default is right; a bare apply on a themed project now refreshes that
    theme instead of silently switching it to signal.

16. Switching themes left the outgoing theme's block in _mxcli-atlas-map.scss
    and appended the incoming one beside it, doubling the file. Mine: the
    protected-path branch in remove() returned without ever writing the
    truncation it described. Harmless while the three Atlas maps are identical,
    but it broke the "only one theme at a time" invariant that exists precisely
    so two maps cannot fight in the cascade.

17. The topbar language selector measured 1.13:1 contrast in every dark palette
    — invisible, not merely low. Two mistakes stacked. The guard was a bare
    .current-language-text at (0,1,0) against Atlas's
    .navbar-brand .widget-language-selector .current-language-text at (0,3,0),
    so it never won. And `color: inherit` was the wrong value anyway: it
    inherits body ink, which is dark, while the rail is dark in both palettes.
    Now re-declared at matching specificity and resolved through the rail token.
    Measured in a browser: 17.79:1 light, 19.47:1 dark.

The command-level bug in #15 needed a command-level test. cmd_theme_test.go
drives the real cobra command, because a test calling theme.Resolve directly
would keep passing while the CLI stayed broken — the same shape as the
"grep the call sites, not the helper" lesson already in fix-issue.md.

My own verification of #17 was too shallow to catch it: it read
getComputedStyle(el).color once, saw white and stopped. The probe now computes
the WCAG ratio against the first non-transparent ancestor background, which is
what turns "looks fine" into a number. Recorded in fix-issue.md.

Also documents a behaviour the report flagged but which is not a defect: apply
appends its block to the end of main.scss, after any @import the project already
had there, which matters to a project relying on import order rather than on
specificity.

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

Track endpoint first-seen time across reconnects
fix(theme): remove targeted the wrong theme, switching orphaned a block, dark topbar unreadable
@ako
ako merged commit 6937ba5 into mendixlabs:main Aug 6, 2026
4 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 7, 2026
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