Sync ako/mxcli: OData publishing + consumption, lint coverage and noise, dev-loop diagnostics - #864
Merged
Conversation
…re it
Every page in a locally-run app rendered as a black screen, intermittently and
at the same mxcli version. `run --local` bundles the browser client at step 5b
and it succeeds; then the runtime boot runs Gradle `clean-custom-classes compile
package`, whose package pass repopulates deployment/web and takes dist/ with it.
The bundle was deleted 51 seconds after the same command wrote it.
It only bites when Gradle has work to do — a new Java action, a full recompile —
which is why an app boots fine for weeks and then stops with nothing changed.
Nothing reported it. `mxcli check` passes, the build succeeds, the runtime log
is quiet, `curl /` returns 200 with a valid HTML shell, and every OData service
answers. Only a browser sees it.
A pre-condition established before a step that rewrites the same directory is
not a post-condition, so the bundle is now verified after the boot:
- WebClientBundled / EnsureWebClientBundle re-bundle only when it is gone,
and say so. When Gradle had nothing to do this is a stat.
- Re-ordering the bundle to after the boot instead was rejected: it would
leave the app reachable-but-blank for ~30s on every cold start.
- A failed re-bundle warns and leaves the runtime up — the app's services
still work, only the browser is broken — and names the blank page.
`mxcli test --local` boots the same way and destroys the bundle too, which is
why a test run between a boot and a browser looked like a rendering bug. Tests
are headless, so that path reports the loss and the remedy instead of spending
~30s on a loop whose point is two seconds.
Both controls run: with the guard never firing the wipe test fails, and with it
always firing the survivor test fails.
mxcli-formula1 §35
…citly
A grid drill-down written as
linkbutton btnWeekend (Caption: 'Weekend',
Action: SHOW_PAGE Mod.Race_Weekend(Race: $currentObject))
described back as `show_page Mod.Race_Weekend`, with no argument.
The mapping was never missing from the model. mxcli writes a page action's
ParameterMappings as an empty array on purpose: Studio Pro infers the current
row object from the enclosing widget, and an explicit mapping whose Argument is
"$currentObject" is rejected as CE0115 (mendixlabs#296). That decision stands. What was
missing is its other half — DESCRIBE read only explicit mappings, so the
implicit argument had nowhere to come from. The writer's comment claimed
DESCRIBE recovered it; it did not, and now it does.
The cost is not cosmetic. DESCRIBE is what you reach for once you have stopped
trusting the model, so its output arrives already looking like a conclusion: the
mapping was dropped, that is why the page gets an empty object. It is a
plausible, wrong answer at the worst possible moment, and it cost three
debugging cycles replacing a button that was correct.
Recovery reads the target page's own declared parameters, since that is where
the information lives. Both ends are guarded: an explicit mapping still wins,
and an unresolvable page yields no arguments rather than invented ones.
Control: with recovery removed the test reproduces the reported output verbatim.
mxcli-formula1 §39
`create or modify odata service` silently revoked the service's access, so the next build failed with "At least one allowed role must be selected for the published OData service to be accessible." Grants are made by a separate statement and cannot be re-stated in the create script, so nothing in the script could put them back — only a manual re-grant, until the next modify. serializePublishedODataService never wrote AllowedModuleRoles. The document is serialized wholesale and written with updateUnit, so a field the serializer omits is not left alone: it is deleted. A wholesale re-serialization makes the writer's field list a data-retention policy, and this one was missing an entry. The array uses storage marker 1 (BY_NAME references) — the same shape the working GRANT path writes via makeMendixStringArray, rather than a marker reasoned out from an unrelated type. An earlier pass looked for this loss at model level, found the grants present and correctly carried through the modify branch, and recorded it as "reported but does not reproduce". That conclusion was wrong: the loss only exists after the round trip to BSON, which a model-level check cannot see. The carry-through guard added then was a no-op; it is kept for a caller that clears the slice, and its comment now says which layer actually held the bug. Control: with the field omitted again the test reproduces the empty-grants document. mxcli-formula1 §26
…es them
The skills are embedded in mxcli and written exactly once, by `mxcli init`, so
upgrading the binary did nothing to them: a project initialised on Monday still
served Monday's guidance from Tuesday's mxcli, with no warning. Confirmed with a
binary rebuilt at 12:05 beside skills stamped the previous day.
Stale guidance is worse than missing guidance — an agent reads it with the same
confidence either way, and the point of shipping skills inside the binary is
that the two versions agree.
Fixed where the files are consumed rather than where they are authored: the
SessionStart bootstrap script already runs on every session and can fetch the
binary, so it is the one place guaranteed to execute immediately before an agent
reads them. It now runs `mxcli init --sync-skills`, a new flag that refreshes
only .ai-context/skills/ and exits.
Two properties make an every-session job acceptable:
- it writes only the files that differ, so mtimes keep meaning "when did this
guidance last move" (a test asserts an unchanged skill is not rewritten);
- it is silent when the project is already current.
It is never fatal — a skills refresh must not block a session. A test asserts
the bootstrap calls it before the exec'd setup, since a step ordered after an
exec never runs.
Control: with write-once restored, the stale file survives the sync.
mxcli-formula1 §16
…ng true `publish entity … (TopSupported: No)` parsed, described back as No, and published as Yes: serializeEntitySet hardcoded all three QueryOptions to true and never read the *bool fields the model already carried. The AST, model and DESCRIBE halves of the feature had shipped without the writer half — which is exactly the shape a DESCRIBE round-trip test cannot catch, since DESCRIBE reads the model, not the published document. For a microflow-backed resource this claim is load-bearing rather than decorative. Mendix applies no query options to a read-microflow resource: it hands the request to the microflow and returns what comes back. The annotation is therefore the only thing a client has to go on, and an over-claim is not cosmetic — a client that believes $top works reads a whole collection as though it were a page. nil keeps Mendix's own default of true; only an explicit false opts out, which is what the model's comment already promised. Control: with the constants restored the test reproduces the over-claim. mxcli-formula1 §20
…omises
A published OData resource backed by a read microflow could silently return
the wrong thing, twice over:
- `?$top=5` returned the whole collection with a 200, because Mendix applies
NO query options to a read-microflow resource — it hands over the request
and returns what comes back. TopSupported/SkipSupported describe the
microflow, not the platform, and default to true when unspecified.
- a client re-reading a row it holds sends `?$filter=key eq '…'` unprompted.
With no branch for it the request falls through to the collection default
and the client adopts the FIRST row as that object's identity. No error:
well-formed request, valid collection, correct $count, 200.
Both are promises the service makes on the microflow's behalf, and nothing
checked either. MDL-ODATA02 flags a declared KEY the microflow cannot answer;
MDL-ODATA03 flags capabilities it cannot implement.
The read path has no other way to be safe. Unlike an OData action or an
insert/update/delete microflow, a read microflow has no System.HttpResponse
parameter and cannot answer 400, so declaring `TopSupported: No` is its only
substitute for the refusal it cannot send. That contract is now documented per
capability in odata-data-sharing.md and in `mxcli syntax odata.publish`, which
neither covered before.
Both rules fire on one provable condition — the microflow takes no
System.HttpRequest parameter, so it cannot see a key or a query option at all.
A microflow that does take it gets the benefit of the doubt: proving which
options it parses needs real analysis, and a rule that guesses gets switched
off.
The rule shipped dead once during development: the visitor stores ReadMode as
`MICROFLOW Module.Name` upper-cased and the prefix match was case-sensitive.
Caught by running it against a real script rather than a hand-built AST, and
now pinned by its own test.
Swept mdl-examples/ and the skill MDL blocks for false positives: 0 failures.
mxcli-formula1 §37, §20 (suggested issues 2 and 3)
Everything logs at INFO, so the detail you need is usually not in the log at
all — and raising the whole runtime to TRACE is unusable on a busy app. The
M2EE admin API has exposed per-node levels all along; nothing in mxcli drove
them.
mxcli log list [--filter x] [--json]
mxcli log set <node> <level>
mxcli log set A=TRACE B=DEBUG # one admin call, applied together
This was proposed as `mxcli odata trace`, for "what is my published resource
being asked?". It is deliberately not that: set_log_level takes a LIST of nodes
and the runtime reports 57 of them, so the primitive is subsystem-agnostic and a
per-subsystem command would have wrapped it one subsystem at a time. The OData
knowledge lives in the skill instead — including the finding that there is NO
log node for a published OData service (ODataConsume is the client side), so
that particular question still needs a LOG in the read microflow. That gap is
Mendix's.
Every API fact was probed against a live 11.12.1 runtime, because the HTTP
response for an AdminException says only "See logging output for details" — the
real message is in the runtime log:
- get_log_settings requires one of node/subscriber/sort in params
- sort accepts exactly "node" and "subscriber"
- set_log_level takes {"nodes":[{name,level}],"force":bool}
- force means "allow a node that does not exist yet", and permanently
registers the name — so it is opt-in and an unknown node is an error by
default, making a typo an error rather than a setting that never applies
- an invalid level is refused
A level typo is caught locally with the valid set named, rather than becoming an
AdminException whose detail the caller never sees. "Cannot reach the admin API"
is distinguished from "the runtime refused the request", so the connection hint
does not appear on an unknown-node error and bury the sentence that matters.
Verified end-to-end against a booted runtime: both argument forms, multi-node in
one call, a typo refused, --force accepted, and an unreachable port.
mxcli-formula1 suggested issue 4
The previous commit claimed there is no log node for a published OData service, and that "what is my published resource being asked?" therefore still needed a LOG in the read microflow. That was wrong. `OData Publish` — with a space — exists whenever the project publishes a service, and at TRACE it logs the full incoming URI: TRACE - OData Publish: Incoming request from 127.0.0.1: GET .../Rows?$top=5&$filter=rowKey eq 'abc' DEBUG - OData Publish: Responding to client with status code 400. which is precisely the question the command was built for. The mistake is worth naming, because it is the same one in a new place: the node list is a property of the APP, not of Mendix. Nodes appear once something registers them. The first probe ran against a project with zero OData services, saw 57 nodes and no publish node, and generalised. Adding one service makes it 58. Enumerating a platform's capabilities from one sample app and calling the result "Mendix does not have this" is exactly what `log list` against your own app is for — the command was right, the conclusion drawn beside it was not. The same probe also showed Mendix rejecting `$filter` on a property not declared Filterable, with 400 "Property 'x' is non-filterable", BEFORE the read microflow runs — so the platform does enforce declared filterability, which is more than §20 credited it with. What it still does not do is apply $top/$skip/$orderby for a read-microflow resource. Corrected in analyze-runtime.md (node table + a section on the publish node), odata-data-sharing.md, the command's own help, and the fix-issue row. mxcli-formula1 suggested issue 4
…unloadable page Upstream mendixlabs#854 reported a cross-module association datasource writing an empty DestinationEntity, which Mendix resolves to null so the .mpr will not open. That half no longer reproduces — the destination resolves through CrossAssociations, and an unresolved one is refused. The association half of the same EntityRefStep was still written as authored. Both halves are BY_NAME references and Mendix nulls either one it cannot find, so a bare `Order_Line` produced the identical unopenable project, one property over: ArgumentNullException at EntityRefStep.set_AssociationId The explicit-destination form is what made it reachable: supplying the destination satisfies the empty-DestinationEntity guard, so nothing else stood between a bare name and the crash — and `Assoc/Module.Entity` is exactly the spelling that guard's error message tells the author to use. Qualify a bare association with the context entity's module, the rule attribute-path hops already follow, and verify an author-supplied destination's association exists rather than taking it on trust (a misspelling would otherwise be written qualified-but-nonexistent and null the same way). Verified on Mendix 11.13.0: all five spellings resolve and mx check reports 0 errors. The pre-fix binary reproduces the crash on the same script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
…logged into Under --hub the runtime boots with the public https root URL, so it marks its session cookies Secure and prefixes them __Host-. A headless browser reaching the app over http then cannot hold a session, every screenshot silently shows the login page, and rendering defects survive every other check. The login browser context now sends X-Forwarded-Proto: http when the target is http. On Mendix 10.24+ that header takes precedence over ApplicationRootUrl, so the runtime drops both Secure and the __Host- prefix. This is accurate rather than a workaround — the request genuinely is http — it is a no-op when the root URL is already http, and real users arriving over https through the hub are unaffected. Verified at the layer the bug lives in, against a live 11.12.1 runtime booted with an https root URL: the captured Playwright storage state goes from __Host-XASSESSIONID(secure=true) to XASSESSIONID(secure=false). The reported cause did NOT reproduce as stated, which is worth recording. On 127.0.0.1 an https root URL blocks nothing: loopback is a trustworthy origin, so Chromium accepts Secure and __Host- cookies there, and the app rendered clean with no console errors and no failed requests. The mechanism is real only from a NON-loopback http origin — a container hostname, a LAN address — where the origin is not trustworthy and the session cannot be held at all. The fix ships because it is correct and free, not because it was shown to repair the reported symptom. mxcli-formula1 §38 / suggested issue 7
…t in
declare $Msg String = 'a' +
-- explain the second half
'b';
stored the comment INSIDE the expression, and the build failed CE0117
"Error(s) in expression". Nothing before mxbuild objected: `mxcli check` passed
and DESCRIBE round-tripped the comment back out.
extractOriginalText reads the raw input stream between two token positions.
That is exactly what preserves an expression's spacing — and it also drags in
every token the lexer sent to a hidden channel. MDL's `--` and `/* */` are
`-> skip`, so they never appear in ctx.GetText() and always appear in a source
slice. Mendix expressions have neither form.
Expression sites now go through extractExpressionText, which strips MDL comments
first. Two properties matter:
- a comment becomes whitespace, never nothing, so `1 --c\n+ 2` cannot become
`1+ 2` and `'a'--c\n'b'` cannot weld into one token;
- single-quoted strings are respected, because a Mendix string may legitimately
contain `--` or `/*` and stripping those would corrupt the value. The `''`
escape that keeps a string open is handled too.
OQL keeps extractOriginalText on purpose: `--` is legitimate SQL comment syntax
in a view entity's query, and stripping it would change a different language's
meaning.
Verified end to end against mxbuild 11.12.1 on the same project and script: 1
error before, 0 after. The control matters here — with extractExpressionText
bypassed the unit tests still passed, because they exercise the function rather
than the wiring, and only the mxbuild run showed the call sites were converted.
Swept mdl-examples/ and the 189 checkable skill MDL blocks: 0 failures.
mxcli-formula1 §34 / suggested issue 11
CI caught this: the doctype gate failed on 10-odata-examples.mdl with
[CE6585] "Published entity 'OdTest.LiveRow' must have a key defined."
at Published OData service 'OdTest.BulkAPI'
The example demonstrated an "honest contract" resource by omitting the KEY —
the alternative MDL-ODATA02 offers to answering a key lookup. Mendix does not
permit that: a published entity must have a key. So the rule shipped in this
branch was recommending something the platform rejects, and the example
demonstrated it.
Corrected in all three places the claim appeared: the rule's suggestion, the
example, and odata-data-sharing.md. The resulting position is sharper than the
one it replaces — query options you may decline, the key you may not. A
microflow-backed resource whose rows a client can hold must answer the key
lookup; there is no opt-out.
I validated that example with `mxcli check`, which is parse-only and cannot see
a CE code, rather than the integration gate — the same mistake made earlier in
this branch on 18-folder-examples.mdl, and already written down in fix-issue.md
at the time. The new symptom row states the rule plainly: a change to
mdl-examples/doctype-tests/ is not done until the gate has run on it.
mxcli-formula1 §37 follow-up
Fix an unqualified association datasource that wrote an unloadable page (mendixlabs#854 follow-on)
Formula1 findings: blank-page boot, OData read contract, log levels, and four data-loss fixes
The fixture for MDL-ODATA03 omits the KEY to isolate it from MDL-ODATA02, and the comment described that as "the other way to be correct". It is not: Mendix requires a published entity to have a key (CE6585), which is the same mistake the previous commit fixed in the rule's own suggestion text — this copy of it was left behind. The comment now says why the fixture is shaped that way and what it must not be read as. Query options are declinable; the key is not.
A navigation menu authored in MDL had no way to carry an icon: the grammar had no ICON clause and buildMenuItemBson hardcoded `Icon: nil`, so an MDL-built menu was flat text beside a Studio Pro-built one. The reason this stayed unimplemented is the interesting part. The metamodel names the element Pages$IconCollectionIcon, but Mendix *stores* it as Forms$IconCollectionIcon -- one more case of the "Form was the original term for Page" rename CLAUDE.md documents for ShowFormAction. That is not derivable from the metamodel, and inventing a $Type for a polymorphic child is precisely the failure that builds green and leaves a document Studio Pro cannot open. A Studio Pro-authored project supplied the reference: storage name, the Image field (a 3-part Module.Collection.name), and the exact key set of $ID/$Type/Image and nothing else. The written element is byte-shape identical to Studio Pro's. All three icon variants coexist in one real document -- IconCollectionIcon and ImageIcon both carry an indistinguishable qualified Image (the latter points into an *image* collection), and GlyphIcon carries a numeric Code. Only the icon-collection form is written. DESCRIBE reads all three back and emits a `-- icon ... is not reproducible` comment for the other two, rather than an `icon` clause that would silently convert the variant on replay. The icon is a STRING_LITERAL, not a qualifiedName: Atlas icon names contain hyphens (align-center), which IDENTIFIER cannot lex. Also fixes a read-path divergence found by running DESCRIBE against a real project under both engines: codec.Decode returns a bare *element.Base for an unregistered $Type but a generated struct for a registered one, so asserting on *element.Base dropped the name for exactly the variants that exist in practice -- legacy printed every icon, modelsdk printed none. Both engines now agree. convertNavMenuItem is a field-by-field copy and needed the new fields too. Verified: mxbuild doctype gate 0 errors on both engines; the written BSON diffed against the Studio Pro original; DESCRIBE output replayed and the icon-collection icons survive the round trip. Each test has a revert control proving it reports the reported symptom. mxcli-formula1 #9
An icon is a reference into the model, so it should be spelled like every other reference (ADR-0003), not quoted as a string: ICON Atlas_Core.Atlas.home ICON Atlas_Core.Atlas."align-center" No grammar machinery was needed: qualifiedName is already arbitrary-depth and identifierOrKeyword already accepts QUOTED_IDENTIFIER. Atlas names are hyphenated and `align-center` lexes as HYPHENATED_ID, so that segment is double-quoted -- the same escape MDL already uses for a name that collides with a keyword. The DESCRIBE emitter now has to decide quoting per segment, and deliberately does NOT reuse mdlIdent. mdlIdent quotes anything that does not lex as a bare IDENTIFIER, which is right where the grammar wants an IDENTIFIER -- but a qualifiedName segment is an identifierOrKeyword, and that rule accepts keywords unquoted. Most short Atlas names are keyword tokens (home, user, add, folder), so mdlIdent would have quoted nearly every icon and buried the reference in punctuation. quoteQualifiedName instead asks the real parser whether a segment is accepted, so it stays correct as the keyword set moves. I guessed the quoting rule wrong twice writing this, so the test that matters is not an expectation about which names get quotes: it feeds the emitted statement back through the parser and asserts it re-parses to the same icon. Controls both ways -- stop quoting and the hyphenated case fails to parse; quote everything and the keyword case regresses. Verified: mxbuild doctype gate 0 errors on both engines; DESCRIBE against a real Studio Pro project emits the new form and replays clean. mxcli-formula1 #9
The role-grant fix (28ce821) went into sdk/mpr/writer_odata.go. The other writer had ZERO references to AllowedModuleRoles, so a `create or modify odata service` on the modelsdk engine kept revoking a service's access however carefully the mpr writer was fixed: mxcli-prefix / legacy: grant KEPT mxcli-prefix / modelsdk: grant LOST <- the reported defect mxcli / legacy: grant KEPT mxcli / modelsdk: grant KEPT GRANT is a separate statement and cannot be restated in the service body, so once dropped nothing in the script can put it back. Two parts: write the field (marker 1, matching the mpr writer), and register it in MandatoryListMarkers so a service with no grants still emits the empty marker. ByNameRefList only marks dirty on Append, so without the default an empty list omits the key entirely -- and omitting is what deletes, because the document is serialized wholesale. Answering the follow-up question in the report -- whether the two writers diverge elsewhere -- the remaining four keys legacy writes and modelsdk does not name directly (Enumerations, Microflows, MetadataReferences, ValidatedEntities) are all supplied by TypeDefaults. OData writer parity is now complete. Adds TestODataService_EngineWriteParity: run the same script through both engines, read the stored BSON, and assert modelsdk writes every key legacy does. That is the general property, so it catches the next dropped field rather than only this one. Two measurements changed what this commit does: - `mx check` CANNOT gate this. A published service with no allowed roles is 0 errors on Mendix 11.12 -- CE0307 does not fire -- so the doctype gate is blind to the symptom. The example added here exercises the path and says plainly that it does not gate it; the BSON comparison is the gate. - The parity test needs the `create or modify` step. GRANT patches the stored document, so create+grant alone passes on both engines even with the bug present. My first version omitted the modify and the revert control passed -- the test proved nothing until that step was added. mxcli-formula1 §41
`authentication microflow Module.Auth` parsed, checked clean and reported "Created OData service" -- then failed the build with CE0333 "Please select a microflow to use for authentication", because nothing ever assigned the name. The plumbing existed at both ends and only the middle was missing: the grammar already accepted `MICROFLOW qualifiedName?`, both writers already serialize AuthenticationMicroflow from svc.AuthMicroflow, and both readers already read it back. parseODataAuthTypes recognised the keyword and threw the name away, so the field was only ever populated by reading a model Studio Pro had written. DESCRIBE emitted it as a comment (`-- Auth Microflow: …`), which is the same defect class as the page-parameter loss: output that looks complete and replays into a model the build rejects. It is now part of the clause, and the round-trip test asserts the emitted text re-parses to the same microflow. Adds MDL-ODATA04: the name is optional in the grammar, so `authentication microflow` alone still parses -- and that is exactly what produces CE0333. This matters beyond syntax completeness. Custom authentication is the only way to avoid a full password hash on every OData request; basic auth pays it per call and even pays it on a wrong password, which measured as 60-80% of a page turn. The consumer needs no change: the microflow can read the Authorization header itself. Verified by writing a custom-auth service entirely from MDL into a real project: DESCRIBE round-trips it, and `mx check` reports 0 errors. A modify that omits the authentication clause preserves the microflow on both engines. Deliberately NOT in the doctype gate: those projects run with security off, and Mendix reports CE6600 "App security is off, but custom authentication is enabled" for any custom-auth service there, regardless of correctness. The skill documents both that rule and CE0333. mxcli-formula1 §40
MDL could not declare an OData action at all.
`createODataServiceStatement` admitted `publishEntityBlock*` and nothing else,
so every variant of `publish microflow …` was a parse error at
`missing ENTITY` -- and the published $metadata had an EntityContainer with
EntitySets and no ActionImport.
publish microflow AcTest.RecordPrediction as 'RecordPrediction'
expose ( DriverId as 'driverId', Points as 'points' );
Parameter data types and the return type are NOT restated in MDL. They are
read off the microflow, which already declares them -- the same thing Studio
Pro does, and the only way the two cannot drift. An omitted expose clause
publishes every parameter under its own name.
Unlike the custom-auth gap, the write path did not exist either: neither
sdk/mpr/writer_odata.go nor mdl/backend/modelsdk/odata_write.go had a single
reference to PublishedMicroflow. Both now serialize it.
Nothing here was guessed. Every shape came from something already proven:
- property names and kinds from the generated metamodel
(modelsdk/gen/odatapublish: ExposedName, AlternativeExposedName, a BY_NAME
Microflow ref, a Parameters part list, a ReturnType part, Summary,
Description);
- the Module.Microflow.Param form of the MicroflowParameter ref from the
published-REST writer, which already ships it;
- the DataTypes$ObjectType/ListType `Entity` key and
DataTypes$EnumerationType `Enumeration` key from serializeMicroflowDataType,
which writes the same family for a microflow's own return type.
The semantic model keeps the data type as a kind plus a ref rather than one
string, because "Module.X" alone cannot say whether X is an entity or an
enumeration -- the ambiguity CLAUDE.md documents for the MDL visitor.
Verified beyond `mx check` reporting 0 errors, which for a new element type
proves only that mxbuild tolerated it. Dropping the target microflow makes
mxbuild name the elements back:
CE1613 "The selected microflow 'AcTest.RecordPrediction' no longer exists."
at Published microflow 'RecordPrediction'
CE1613 "The selected parameter 'AcTest.RecordPrediction.DriverId' no longer
exists." at Published microflow parameter DriverId from published
microflow RecordPrediction
So mxbuild resolved the exposed name, the microflow reference and both
parameter references -- the element is understood, not merely accepted. Clean
on both engines before the drop; the cross-engine parity gate now publishes an
action too.
Still owed, and not claimed here: DESCRIBE does not yet emit the block, so a
service with an action does not round-trip; no doctype example; docs pending.
mxcli-formula1 §47.1
Completes the OData action support. DESCRIBE now emits the `publish microflow` block as part of the service body, so its output is a full description of the service rather than one missing its actions. Read paths added on both engines -- sdk/mpr/parser_odata.go and mdl/backend/modelsdk/odata_read_detail.go -- including a DataTypes$* reader that mirrors the writers: Object/List name an Entity, Enumeration names an Enumeration, everything else is a bare primitive. The block guard also had to widen: it keyed off entity types and entity sets, so a service whose only published thing is an action emitted no body at all. Two things the controls corrected: - **A test that calls the emitter helper directly does not prove the emitter is wired.** Reverting the loop in outputPublishedODataServiceMDL left every printPublishedMicroflowMDL test green. The DESCRIBE tests now go through the real entry point, and both the loop and the widened block guard have a revert control that fails. - **The severity claim in the previous commit was too strong.** Replaying DESCRIBE output does NOT drop the action even with the emitter reverted, because `create or replace` preserves microflows when the statement supplies none (the same rule entities follow). The defect was an incomplete description -- you could not see the action, or use the output to recreate the service elsewhere -- not in-place loss. Verified end to end on both engines: write from MDL, DESCRIBE, replay the output, action still present, `mx check` 0 errors. The doctype example now publishes an action (including an action-only service), so the mxbuild gate covers it on both engines. Docs: odata-data-sharing.md gains an OData Actions section explaining why this matters -- Mendix validates $filter against published metadata before the read microflow runs, so a parameterised entity set cannot take its arguments as filters -- plus the two Mendix behaviours that bite around it (a returning stored procedure needs a SELECT-able wrapper; the JDBC driver must be declared and included even for PostgreSQL). Syntax help and the quick reference updated. mxcli-formula1 §47.1
A service that declares `TopSupported: No` could not be consumed. The
contract carried it correctly, but the generator stamped
`SkipSupported = true; TopSupported = true` on every external entity, so
the consuming app failed to build:
CE6630 'Seasons' is marked supports $top=False in the OData service,
but True in the app.
Two layers were involved. `applyCapabilityAnnotations` opened with
`if ann.Record == nil { continue }`, but TopSupported/SkipSupported are
standalone boolean terms carrying `Bool` on the annotation element, not
the record shape InsertRestrictions uses — so the parser discarded them
and EdmEntitySet had nowhere to put them. `applyExternalEntityFields`
then hardcoded both true, beside a Countable that was derived and under
a comment explaining the very CE6630 mechanism the hardcode triggered.
Absent stays true: OData's default is supported, so defaulting to false
would invert CE6630 for every unannotated service.
This is the prerequisite for MDL-ODATA03's advice being followable — the
rule tells authors to declare `TopSupported: No` on a read-microflow
resource, which until now broke their consumers.
mxcli-formula1 §42
MDL-ODATA02 (the KEY promise) and MDL-ODATA03 (paging) both fired on one condition: the read microflow has no System.HttpRequest parameter. But the two concerns share that parameter, so adding it to answer the KEY also silenced the paging rule while nothing about the paging changed — a false negative on exactly the half-fixed resource still shipping unpaged 200s. MDL-ODATA03 now asks whether the option is used. An OData query option is spelled `$top` / `$skip` on the wire, so a microflow implementing one must name it; the rule reflect-walks the reachable body (following calls into microflows the script defines) and reports only the options nothing names. Prose is excluded — an `@annotation` documenting the limitation is not an implementation of it. Silence remains the answer whenever the body stops being readable: a Java action, a JavaScript action, a nanoflow, or a microflow defined outside the script ends the analysis, the same stance the rule already took toward out-of-script read microflows. The doctype example's request-aware variant was written to the old rationale and started warning; it now locates the options it advertises. mx check passes on both engines, 0 errors. mxcli-formula1 §42
`run --local` refused to boot with "a previous 'mxcli run --local' … is
likely still serving on it" and told the user to go hunting with pgrep.
The guess was wrong as often as right, and one of the suggested
patterns — `pgrep -f 'mxcli run'` — matches the shell it is typed into.
The guard now resolves the listener through /proc (inode from
/proc/net/tcp{,6}, owner from /proc/<pid>/fd) and prints its pid and
command line. No lsof/ss: both are routinely missing from slim
containers.
It also separates two cases that had shared one message and need
opposite remedies. A leftover of a previous run — which can only happen
after a kill -9, a crash, or a reaped container, since a graceful stop
already reaps the whole process group — gets a ready-to-paste
`kill <pid>`. A foreign listener gets told it is foreign and pointed at
--app-port, with no kill offered.
Detection-only is unchanged: reaping someone else's process stays the
user's call.
mxcli-formula1 suggested issue 8
FilterRestrictions and SortRestrictions have two shapes and only one was
read. The parser pulled NonFilterableProperties out of the record and
ignored the record's own Bool property, so `Filterable:
!nonFilterable[p.Name]` was true for every property of a set that had
declared nothing filterable — 28 × CE6630 on one service:
'message' is marked Sortable=False in the OData service,
but True in the app.
Mendix picks the shape by arithmetic, not preference: it lists
NonFilterableProperties when SOME attributes are filterable, and emits a
bare `Bool="false" Property="Filterable"` when NONE are, because then
there is no list to write. Both appear in one document, on different
entity sets — an entity exposing only a KEY produces the whole-set form.
Both shapes now sit behind EdmEntitySet.AttrFilterable/AttrSortable, so
a caller cannot consult one and forget the other, which is how this
arose. The accessors are nil-safe, which also removes the entitySet !=
nil dance at the call site. Absent still means true: OData's default is
allowed.
This is the sibling of the Top/Skip fix in 27ea1da — same vocabulary,
same two-shapes cause, found because a consuming app failed to build.
mxcli-formula1 §48
A generated project can have documentation nowhere and nothing says so: `mxcli check` and the build both pass, because documentation is never load-bearing. QUAL002 was the reminder, but it reached only entities and microflows. Java actions were not reachable from Starlark at all, and their parameters were not reachable from anywhere — the catalog kept a parameter count and discarded each parameter's Description. That is the field Studio Pro shows to whoever wires up the call, where an undocumented parameter is a blank next to a name like `pInput` at exactly the moment a caller has to decide what to pass. - catalog: java_action_parameters table + view, registered in Tables() - linter: JavaActions() carries its parameters, so a rule naming a parameter can name its action without re-joining; Marketplace and System modules excluded as every sibling iterator does - starlark: java_actions() builtin, parameters nested on each action - QUAL002: Java actions, their parameters, and (off by default, on request) entity attributes — every target switchable via get_option CatalogSchemaVersion 8 -> 9. Without the bump an existing cache keeps its old schema, the query fails, the error is swallowed, and the rule reports zero parameters — silent under-reporting that looks exactly like a documented project.
Extends the previous commit from four targets to all of them. Nineteen document types are now swept by one table-driven projection rather than nineteen bespoke builtins, so covering a new Mendix document type is two rows: one in documentableSources naming the catalog table and its documentation column, one in _DOC_KINDS giving the option and suggestion. On by default, one option each: Module, Entity, Page, Snippet, Building block, Layout, Enumeration, JavaScript action, Image collection, Data transformer, Workflow, Business event service, REST client, Published REST service, Constant, JSON structure, Import mapping, Export mapping. Off by default: attributes and associations — a domain model has hundreds and the same check there is a wall of text rather than a signal. Java action parameters stay on: an action has a handful, and Studio Pro shows each description to whoever wires up the call. Also fixes a leak the sweep made impossible to ignore. modules.Source carries "Marketplace ..." for downloaded modules and is empty for System exactly as it is for the user's own, so the usual `WHERE COALESCE(m.Source,'') = ''` excludes Marketplace and lets all of System through. On a blank 9.24 project that was 52 findings, of which 47 were FileDocument, HttpRequest and friends. Filtering additionally on the sentinel module id leaves the 5 that are the user's. Around ten other LintContext iterators still carry the Source-only filter and leak System into their own rules; those are left alone here rather than silently changing every rule's output in a documentation commit. The documentation column is not uniform — Mendix says Documentation for Java actions, REST and mappings, Description for the rest — and a revert control confirms assuming one spelling silently drops five kinds. Verified end to end, not only in unit tests: exec'd MDL against a real .mpr creating documented and undocumented elements, rebuilt the catalog and ran the CLI, which reports the undocumented ones and stays silent on the documented ones. Unit fixtures insert catalog rows directly and so cannot see a builder that never populates a column.
Follow-up to fbb1609, which fixed the leak only in the iterators it introduced. The same Source-only filter appeared in eleven more places, so every rule that walks entities, pages, microflows, enumerations, constants, snippets, widgets or database connections — plus all three FindUnused kinds — reported platform elements the user cannot change. On a blank Mendix 9.24 project the whole run goes from 60 findings to 8. Removed: CONV001 asking to rename System booleans (User.Active -> IsActive), SEC001 demanding access rules on 38 System entities, DESIGN001 splitting QueuedTask, SEC006 on System.User, MPR003 splitting the System module itself. Verified by diffing full lint output before and after: 52 findings removed, every one of them System, and the ADDED set empty — the predicate only ever narrows, so it cannot invent a finding. TestIterators_ExcludePlatformModules drives all twelve iterators against a catalog holding System, Marketplace and user rows, and asserts each returns the user's element and neither platform one. The third assertion matters most: an iterator returning nothing would satisfy the first two. Also fixes drift in setupModuleFilterDB, whose hand-rolled modules table lacked the Id column the sentinel check reads. Because these iterators swallow query errors and return no rows, that surfaced as four "expected ModA entities to be yielded" failures rather than "no such column".
Same schema drift as setupModuleFilterDB, in two more hand-rolled fixtures. I ran ./mdl/linter/ before committing 4ccf8e5 but not ./mdl/linter/rules/, so four tests in that package were left red. The failure text is misleading in the same way: because the iterators swallow query errors, "no such column: m.Id" reaches the test as "expected 1 violation, got 0".
An iterator whose query failed returned no rows and said nothing:
`if err != nil { return }` inside an iter.Seq[T], which has no error
channel. For a linter that is the worst shape of failure — the entire
output is "here is what I found", so a dead query is indistinguishable
from a clean project, and CI goes green on a run that checked nothing.
This is how three fixtures' missing modules.Id column surfaced as
"expected 1 violation, got 0" rather than "no such column".
LintContext now collects QueryErrors. Iterators still degrade to "no
rows" so one broken query cannot take down the run, but the failure is
recorded, and `mxcli lint` prints each one and exits 1.
All 34 sites: Query+return, Query+continue, bare and inline rows.Scan,
the `return unused` in FindUnused that a bare-return sweep misses, and
the reader-backed ListScheduledEvents. Errors dedupe on iterator+cause,
since several rules iterate the same accessor.
Verified end to end by dropping and recreating a view in a real cached
catalog: the run names the iterator and the cause, exits 1, and the
remedy it prints — delete .mxcli/catalog.db — was run and does clear it.
The healthy path is unchanged: no output, exit 0, same findings.
run --local port diagnostics; OData Filter/Sort whole-set restrictions; QUAL002 all document types + System-module leak
…ations mxcli-formula1 §50. Re-running `create external entities from` added two associations every time, without bound: `season_2`, `season_3`, then `season_4`, `season_5`, and so on. One project had reached `season_15` before anyone noticed — `mx check` clean, every test passing, the only symptom duplicate links in Studio Pro's domain model. Association names are unique per module, so the second entity with a `season` nav property is stored as `season_2`. The dedup looks for an association matching the nav property it is about to import, and `season_2` can never match `season` by name. The index that WOULD have matched it is keyed on RemoteParentNavigationProperty — and the modelsdk reader never read the OData association source back, so that index was always empty on the default engine. The write path set the field and the legacy reader read it; only the default read dropped it, so it survived one save and vanished on the next load. assocFromGen now reads Rest$ODataRemoteAssociationSource back (nav properties, navigability, and the four capability flags). The dedup index moves into indexExistingAssociations so it can be tested directly. Not fixed by stripping a trailing _<n> from names: that would also match a user's genuine `season_2`, and the model already records the true origin. Existing damage is not cleaned up here — those associations are referenced by external-entity access rules, so deleting one raises CE1613.
fix(odata): stop create-external-entities duplicating suffixed associations (formula1 §50)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Twenty-eight commits since the last sync (
2f5b182f), all driven by findings frombuilding two real apps against mxcli (
ako/mxcli-formula1FINDINGS.md §35–§50 andits suggested-issues appendix). 95 files, +8101/-216.
Grouped by theme; each was filed with a reproduction and fixed against it.
OData — publishing
ededab1,3509f2a) — the lastthing that forced a hand-written Java action per operation.
DESCRIBEround-trips it.109a55c) — this is whatlet one app drop
BcryptCost = 8, which was 60–80% of a page turn.AllowedModuleRoleswas never written by the modelsdk writer (dc780ec).28ce821).054f780).OData — consumption
(
66c938b0) — Mendix emitsNonFilterablePropertieswhen some attributes arefilterable and a bare
Boolwhen none are, and both appear in one document.Generating external entities from a real service gave 28 × CE6630.
$top/$skip(27ea1da).70a169b5).Lint — coverage, and a large source of noise
5a7780fa,fbb1609b) — it previouslyreached entities and microflows only. Now 21 types, including Java actions and
their parameters (the description Studio Pro shows at the call site, which
the catalog had been discarding in favour of a count). One table-driven
projection, so a new Mendix document type is two rows rather than a new builtin.
4ccf8e5c) —modules.Sourceis
"Marketplace …"for downloaded modules and empty for System exactly as itis for your own, so the usual filter excluded Marketplace and let all of System
through. On a blank 9.24 project the whole run was 60 findings, of which 52 were
platform elements: SEC001 demanding access rules on 38 System entities, CONV001
renaming
User.Active, MPR003 asking you to split the System module itself.Now 8. Verified by diffing full lint output — every removed finding a
System.*element, and the added set empty.
c9245e87) — an iteratorwhose query failed returned no rows and said nothing, which for a linter is the
worst shape of failure: a dead query is indistinguishable from a clean project
and CI goes green on a run that checked nothing.
mxcli lintnow reports eachfailure and exits 1.
Dev loop and diagnostics
4c78200a) — resolved through/proc,distinguishing a leftover of a previous run (safe to kill, so it offers the
command) from a foreign listener (not safe, so it does not).
6aa7c33a,5b62444).b987327c).screenshots (
f1fa02b).Pages, check, describe, navigation
MENU ITEM … ICONagainst a Studio Pro reference (10ba2e1,9364d43).unloadable page (
ff97412).cd2ac98).DESCRIBEwas dropping (4551a4e).18795da).416b4ee,1ab84da)..ai-context/skills/in step with the binary that serves them (01ef224).go test ./mdl/... ./sdk/... ./cmd/...green;go vetandgofmtclean.