Skip to content

Sync ako/mxcli: incremental source-catalog rebuilds (re-describe only what changed) - #1681

Merged
ako merged 41 commits into
mendixlabs:mainfrom
ako:main
Oct 30, 2026
Merged

ako merged 41 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

Syncs ako/mxcli:main into upstream: 1 commit from fork PR ako#1802.

Refs #1081 and #1329.

Incremental source-catalog rebuilds

Problem: once a project had a source index (refresh catalog full source), every later rebuild described every document again. That included the implicit rebuilds that search and show references start after every save. On a 4,753-unit app that took 38 s each time.

This follows from #1081's never-narrow guard: the cheapest rebuild that can still be saved is the most expensive one.

Fix: a source build now records what it described from:

  • source_units: every unit's content hash and container;
  • source_elements: which document came from which unit;
  • a fingerprint of the mxcli build that wrote the rows.

The next build carries a document's row over only when all of these hold:

  1. Its own unit is unchanged. An entity's unit is its domain model, and the container is compared too, because a move into another folder changes the described folder.
  2. No document its references name has changed. For example, a page prints its data-source flow's return entity.
  3. Nothing changed outside the described documents. A folder, module, settings or Java action change describes everything, as before.
  4. The same mxcli build wrote the rows.

FORCE still describes everything. Anything the plan can't vouch for is described again, so reuse can make a build faster but can't change its result.

Other details:

  • Reader.UnitContentHashes reads v2 hashes from the Unit table, and computes v1 hashes from the blob, because older v1 writes left them stale.
  • No schema bump is needed: a cache without the new tables simply reuses nothing.

Evidence

  • TestSourceCatalog_IncrementalBuildEqualsFullBuild compares the incremental build with a forced full build, row for row, after each of these:
    • no change;
    • a flow edit;
    • a callee's return type changed;
    • a data-source flow returning another entity;
    • a flow added, renamed or dropped;
    • an entity altered;
    • a flow moved to a new folder, and another moved into it.
  • Controls:
    • With dependency tracking stubbed out, the test fails on the page whose data-source flow changed.
    • Without the container in the comparison, it fails on the folder move.
    • It fails if no build reused rows.
  • On a 4,753-unit MPR v2 app, after alter entity … add attribute, a plain refresh catalog takes 11.2 s instead of 38.2 s, reusing 3,525 of 3,718 rows. The result is byte-identical to a forced full rebuild.
  • Docs: the Caching section of docs-site/src/tools/refresh-catalog.md explains the incremental rebuild and what FORCE does

claude and others added 30 commits October 30, 2026 07:00
…not have

The write refuses an action type newer than the project
(canon.UndeclaredTypeError, #1774), but nothing at check time did, so
`mxcli check -p` passed a script that exec then refused part-way, and
--continue-on-error ran on without that microflow (#1787).
check -p and exec's pre-flight now report MDL-VERSION01 for each, naming
the statement and the version, wherever it sits in the body (bodyContains:
branches, loops, error handlers). Each floor is the type's introduction
in mendixmodelsdk 4.117.0:

  call workflow, set task outcome, open user task, open workflow  9.0.2
  workflow operation abort / restart                              9.2
  workflow operation pause / continue                             9.3
  get workflow data                                               9.10
  workflow operation retry / unpause                              9.12
  notify workflow                                                 10.0
  get workflows, get workflow activity records                    10.4

Lock and unlock (9.21) get no gate of their own: every one check and
exec accept names its workflow, and that selection is gated at 10.0.

The floors stay in sdk/versions, the one place every gate reads and
`show features` lists. Each gate now names the storage types it stands
for, and TestTypeGates_FloorIsTheTypeIntroduction pins its row to
canon.TypeIntroduced of them (the #1774 workflow type gates too), so the
check-time floor and the write-time floor cannot drift.

Control: with the table's gates left out of microflowBodyGates, all 70
below-floor cases fail with no violation; a row moved off its type's
introduction fails the pin test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
statementVersionGates had no case for `alter microflow|nanoflow`, so no
version gate reached what an insert or a replace splices in: check -p
passed an alter inserting `get workflows` on 9.24.30, and the write then
refused it (#1787). The body gates are now shared
(microflowBodyGates, nanoflowBodyGates) and applied to every operation's
fragment: the workflow actions, send email, a lock or unlock naming its
workflow, and synchronize unsynchronized in a nanoflow. A drop writes
nothing new and is not gated.

execAlterFlow applies them too, before it reads the stored flow, so exec
without the pre-flight (the REPL, --no-check) refuses the statement
instead of reaching the write.

Control: without the AlterFlowStmt case, the 140 alter cases of
TestCheckVersionGates_WorkflowMicroflowActions and the three of
TestCheckVersionGates_AlterFlowGatesWhatItInserts report nothing; without
the handler call, exec fails with "microflow not found" instead of the
version refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…0.24

The bug-test writes a microflow per gated workflow action (the 9.0.2 to
9.12 ones together, then notify workflow, get workflows and get workflow
activity records, one nested in a decision) and an alter microflow that
inserts get workflows into the first.

On 9.24.30, before: check -p passed the script without its 10.0
workflow, and exec refused MF_Notify at write time after five statements;
with --continue-on-error --no-check it wrote the other five, and mx check
reported 0 errors, so the 9.0.2-9.12 actions load there. After: check -p
and exec's pre-flight report MDL-VERSION01 for MF_Notify (10.0),
MF_GetWorkflows, MF_ActivityRecords and the alter (10.4), and App.mpr is
byte-identical. On 10.24.24, before and after: check -p is clean, exec
writes everything, and mx check reports 0 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
The check-time gate and the write-time type floor answer one question
twice, and the class has recurred four times (#1434, #1771, #1774,
#1787). The page now names the variant, where the sweep starts (the
types the converter writes) and the guard (a pin to canon.TypeIntroduced).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
`mxcli debug break` without `--if` left the `condition` property out of
add_breakpoint. Mendix 11.13 and 11.15 (both measured) refuse that request
with {"message":"Missing property: condition","status":2} and register
nothing, so the microflow never paused. The property is now always sent,
as "" for an unconditional breakpoint, which both runtimes accept with
status 0.

The test stub now answers like the runtime: status 0 on success, and the
runtime's "Missing property: <name>" rejection for each property measured
as required. The test that asserted an empty condition is omitted encoded
the defect; it now asserts the empty string is sent.

#1782, mendixlabs#1676

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
`mxcli debug continue` sent `continue` with empty params. The runtime
requires the flow's debug_id and refuses the request with
{"message":"Missing property: debug_id","status":2} (Mendix 11.13 and
11.15, measured), so the flow stayed paused while mxcli printed
"Continued." -- the reporter's "continue after a step reported Continued.,
but the flow stayed paused until continue --all".

Continue now takes the debug_id and `debug continue` resolves it the way
`step` and `inspect` do: the only paused flow, or the one named with the
new --flow. continue_all, which takes no flow, moves to ContinueAll.

#1782, mendixlabs#1676

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
The runtime answers every /debugger/ request with HTTP 200 and says in the
reply envelope whether it took it: {"result":...,"status":0} on success,
{"result":{},"message":...,"status":2} on a refusal (every reply measured
on Mendix 11.13 and 11.15). post read only the HTTP status, so each refusal
came back as success with an empty result: `debug break` printed
"Breakpoint set" over "Missing property: condition", `continue` printed
"Continued." over "Missing property: debug_id", and `inspect` printed {}
for a String variable over "The variable ... is not a Mendix object."

post now returns a *DebuggerError carrying the action, status and the
runtime's message whenever the status is non-zero, for every action. A
reply without a status is still success, so a runtime that omits it keeps
working. A refused session token (the runtime restarted, or another
debugger started a session) adds the `mxcli debug enable` remedy.

#1782, mendixlabs#1676

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
The client test covers the request continue sends; this one covers the
command: with one flow paused, `debug continue` resolves its debug_id
from get_paused_microflows (the 11.15 reply, verbatim) and sends it.
Against the command as it was, the stub records `continue {}` and the
output is "Continued." -- the reported symptom.

#1782, mendixlabs#1676

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
`debug inspect <var>` asks get_object, which takes only an object
variable. For a String (and an enumeration) the runtime refuses it --
"The variable 'Greeting' is not a Mendix object.", status 2, measured on
11.15 -- and the refusal's empty result printed as {}: the reporter's
"inspect <var> printed {} for a String variable".

get_paused_microflows already carries each in-scope variable's type and,
for a primitive, its value ({"type":"string","value":"hello"}). inspect
now prints that entry for any variable whose type is not object or list,
and sends only objects to get_object. A variable the paused state does
not list (a nanoflow's lives in poll_events) still goes to get_object.

#1782, mendixlabs#1676

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Now that a refusal from the runtime is an error, every `mxcli debug`
subcommand printed it followed by the command's whole usage text --
thirty-odd lines of flags -- and then the error again, so the runtime's
reason ("Failed to parse condition: ...") was the line you had to scroll
for. Once a subcommand's RunE runs its arguments were accepted, so an
error from there on is not a usage mistake: RunE now sets SilenceUsage.
A missing argument or unknown flag fails before RunE and still prints
the usage.

#1782

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
One finding for the debugger fixes: the /debugger/ endpoint reports the
outcome in the reply envelope under HTTP 200, a status-code-only client
hid three malformed requests behind it, and the stub has to answer the
runtime's way for the tests to mean anything. Also records the two
poll_events traps measured but not fixed: the ~25 s long poll, and an
event lost to an abandoned poll, which rules out a short client timeout
as the fix for the wait.

#1782, mendixlabs#1676

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
An incremental source rebuild compared an entity through its module's
domain model unit, so one new attribute re-described every entity of the
module and every document that used any of them. On a 4,753-unit app,
adding an attribute to an unreferenced entity in a 154-entity module
re-described 346 documents.

An entity's describe reads only its own subtree, so that subtree (with
its domain model's ID) is now its basis, and the rest of the domain
model is a parent whose change still re-describes the module. The same
edit now re-describes 1 document; a heavily referenced entity
re-describes itself and its 69 users.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82ttaGzay5CwiC2V7cf
A catalog refresh read every .mxunit of an MPR v2 project after every
save: 4,753 file opens on a large app, each one scanned on Windows,
where users measured the scanner as the bulk of their wait.

The reader now keeps a zstd-compressed copy of the units in
.mxcli/cache/unit-contents.db and serves a unit from it while the
file's size and modification time (stat'ed before the read) and the
Unit row's ContentsHash are what they were when it was stored. Misses
are recorded and written at the end of a full pass and on Close; units
the project no longer has are dropped. The store's directory carries
its own .gitignore, because .mxcli/ is not ignored in every project.

On the test fixture the next process's refresh reads 0 of 369 unit
files (1 after a file changes) and builds a row-identical catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82ttaGzay5CwiC2V7cf
Gate workflow microflow actions at check time; debug break, continue and inspect work against the runtime
fix(catalog): base an entity's source reuse on its own subtree (#1803)
…g work

debug enable said breakpoints and stepping "arrive in a later slice";
debug status and debug break said a paused flow waits for "'continue'
(a later slice)". Both have worked since #1782. enable now
names debug break, and status/break name debug continue. The package
comments and the Timeout comment described slice 1; the Timeout comment
now records why it must stay above poll_events' ~25 s long poll
(#1812).

The proposal's protocol table listed add_breakpoint's condition as
optional and continue with {}; 11.13 and 11.15 refuse both. It now
records the measured shapes and the status envelope.

Closes #1813.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
fix(mpr): serve unchanged v2 units from one store file (#1804, part 1)
`mxcli playwright check http://frontend.local:8180/` (an /etc/hosts name
for 127.0.0.1, the app's configured root URL) failed a page that rendered
on two console errors:

  WebSocket connection to 'ws://frontend.local:8180/mxdevtools/' failed:
  Error during WebSocket handshake: Unexpected response code: 502

while the same check on 127.0.0.1 passed (#1800).

Neither the runtime nor run --local returns the 502: the runtime answers
the dev channel's handshake 101 on the configured name. It is the
environment's HTTPS proxy. Chromium on Linux takes its proxy from the
environment and sends ws:// to the HTTPS proxy (RFC 6455 4.1.3), so with
only https_proxy set the page loaded directly while the WebSocket went to
the proxy, which cannot reach this machine's loopback. no_proxy listed
localhost and 127.0.0.1 but not the app's own name.

The browser mxcli starts against an app (the page probe, the sign-in and
sign-out scripts, the screenshot) now gets every host that resolves only
to this machine (loopback, or an address of one of its interfaces) added
to no_proxy and NO_PROXY. The proxy settings are untouched, so a remote
URL still uses the proxy. No console error is ignored: the channel
connects, and a genuine console error still fails the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Explain in the playwright check reference and in the docker-workflow
skill's mxdevtools section why a browser sends ws://<app-host>/mxdevtools/
to the HTTPS proxy when no_proxy lists only the loopback names, what the
browser mxcli starts now does about it, and that a browser of one's own
needs the name in no_proxy (#1800).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…er list

A published entity set whose statement stated no mode was written without
ReadMode, InsertMode, UpdateMode and DeleteMode, and an OData action whose
microflow takes no parameters without Parameters. Studio Pro writes all five
on every element, so on Mendix 11.14.0 `mxcli fix property-sets` reported
them as MIXED (#1805):

  MIXED  ODataPublish$EntitySet.ReadMode: on 42 of 60 element(s); without it: …
  MIXED  ODataPublish$PublishedMicroflow.Parameters: on 2 of 5 element(s); only on: …

and Mendix's merge engine refuses to compare a revision of such an element
with one Studio Pro saved.

An unstated mode is now written as the value Mendix gives the absent
property, and a parameterless action as the empty marker-3 list. Measured
with `mx convert -p -s` on 10.24.24, 11.13.0 and 11.15.0 (ReadSource,
ChangeNotSupported, [3]); the 11.14.0 Studio Pro service in
testdata/testapp-views carries the same. What the service does is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
… out

On a Mendix 11.14.0 project mxcli wrote, `mxcli fix property-sets` reported
five names on one domain model and left them (#1805):

  MIXED  DomainModels$DateTimeAttributeType.LocalizeDate: on 15 of 16 element(s); without it: … [DomainModels$DomainModel]
  MIXED  DomainModels$NoGeneralization.HasOwnerAttr: on 81 of 84 element(s); without it: … [DomainModels$DomainModel]
  (and HasChangedByAttr, HasChangedDateAttr, HasCreatedDateAttr)

No writer of today leaves these out: a script through create, create or
modify, add and modify attribute, association, alter, rename and move gives
no such line on 10.24.24, 11.13.0 or 11.15.0. The elements were stored by an
older mxcli, which wrote only the true system-attribute flags and no
LocalizeDate, and a domain-model write passes the entities it does not name
through as their stored bytes. The table had no row for the keys, since no
measurement of today's writers saw one missing, so neither the next write
nor the repair could fill them. Published OData services stored before the
previous commit are in the same position for the entity-set modes and an
action's Parameters.

Ten rows, measured by stripping each key from Studio Pro- and mxcli-authored
elements of a fresh app and letting `mx convert -p -s` write it back on
10.24.24, 11.13.0 and 11.15.0: the flags false, LocalizeDate true, ReadMode
ReadSource, the change modes ChangeNotSupported, Parameters [3] — the same
on all three, so since 10.24.24. Each is what Mendix reads the absent key as,
so the model says what it said before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
One script through create, create or modify, add and modify attribute,
association, an alter that rewrites a sibling, rename and move, and a
published OData service whose entity sets state their modes and do not and
whose actions take parameters and do not, created and then modified.

Measured on fresh 11.13.0 and 11.15.0 apps: before the fix `fix
property-sets --dry-run` reports ReadMode, InsertMode, UpdateMode, DeleteMode
and Parameters as MIXED (also on 10.24.24); after it, "Every unit already has
the property names Studio Pro writes", `mx convert -p -s` on a copy adds no
key to any unit mxcli wrote, and `mx check` reports 0 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…ind one MIXED report

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…the full build

For a Reference from an entity to itself (M.Order_Parent), the full build
typed `retrieve $P from $Order/M.Order_Parent` as a list of M.Order unless
an object use followed: the start variable is an instance of the TO entity
too, so addRetrieveAction read it as a reverse traversal. Mendix follows a
self-reference from its FROM end - the stored AssociationRetrieveSource has
no direction - so `$n = count $P;` was written and failed the build with
CE0013 "Input variable 'P' must be of type 'List'." (#1793,
measured on 11.13.0; a change of $P builds).

The direction is now one rule, associationRetrieveEnd: a start at both ends
of a self-reference is the FROM end, a start at both ends of an association
between two different entities is ambiguous, otherwise the end it is.
addRetrieveAction and associationRetrieveType (the splice path, #1777) both
ask it through associationRetrieveEndFrom, so the two can no longer type a
retrieve differently; a test pins that they agree on every unambiguous
shape. A self-reference is never a reverse traversal, so it never takes the
owner-Both database fallback either. The ambiguous shape keeps the full
build's old reading (the TO end), which is not measured.

Control: with addRetrieveAction's direction reverted, the self-reference is
typed "List of Sample.Node" after a count, a loop, or no use, from the
entity and from a specialization, and the owner-Both self-reference becomes
a DatabaseRetrieveSource; the agreement test reports the build and the
splice disagreeing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…ve binds

check --references judged a retrieve over an association in one direction
only: a list used as one object (MDL-RETRIEVE03, mendixlabs#1516).
The one object a Reference gives from its FROM end, used as a list, passed
check and exec and failed the build - and associationRetrieveShape returned
"not known" for a self-reference outright, so `retrieve $P from
$Order/M.Order_Parent; $n = count $P;` was silent (#1793).

associationRetrieveShape now decides the start end by associationRetrieveEnd,
the rule the full build takes, so a self-reference is followed from its FROM
end and a specialization of both ends of an association between two
entities is left alone (it used to take the nearest link of the chain).
The chain is read through the script's own entity declarations first, so a
specialization the same script creates counts. The pass reports, as
MDL-RETRIEVE01, that object taken by any aggregate (CE0013), any list
operation but range (CE0097), a loop (CE0100) or a list parameter (CE0117) -
each measured on 11.13.0 over a self-reference's object. range
(Microflows$ListRange) and add/remove/clear/set (Change list) are not
reported: mx check does not type their variable. A one-to-one from its TO
end is not judged, because the full build rewrites a list use of it into a
database retrieve. A forward Reference is the same rule and the same hole:
`count` of it was CE0013 too.

validateFlowBody's forwardReferenceTarget takes the same rule, so `set` on a
self-reference's object is refused like `set` on any object (CE7247). The
Change list Replace mxcli wrote for it passed mx check only because mx check
does not type a Change list's variable (`add` to the object is clean too);
CE0100's "'P' is of type M.Order" settles what Mendix types it as.

Two bug-tests held shapes the rule now refuses:
- 1323-set-object-variable.mdl item 2 set a self-reference Reference
  retrieve "typed as a LIST by Mendix"; it now shows the same over a
  self reference set, which is a list (11.13.0: 0 errors).
- 1100-loop-variable-typing.mdl looped over a Reference retrieved from its
  FROM entity: exec'd as written it is CE0100 on 11.13.0. It now starts from
  the TO entity, which is the list the loop meant (0 errors).

Control: with the source reverted to main, the 11 refused shapes and the
script-declared specialization report nothing, the self reference set's
member path is not reported, `set` on the self-reference is accepted, and
the ambiguous-start control reports a
CE7247 by a direction nobody measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
The bug-test holds the shapes that build: a self-reference Reference
retrieve used as the one object it is (change, a member path), from the
entity and from a specialization of it, and a self reference set used as
the list it is (loop, count). The refused shape is in the header comment.

On a fresh 11.13.0 app, one microflow per shape:

Before (main): check -p --references passed every list use of
`retrieve $P from $Order/M.Order_Parent` and exec created them all; mx check
then reported CE0013 for count, sum, average, minimum, maximum, all, any and
reduce, CE0097 for head, tail, find, filter, sort, union, intersect,
subtract, contains and equals, CE0100 for a loop, CE0117 for a list
parameter, the same from an M.SpecialOrder, and CE0013 for count of a
forward Reference. range, add, remove, clear and `set` over the object, the
change of it, and the reference set's count and loop: 0 errors.

After: check -p and exec's pre-flight refuse the eight list uses in the
first measurement script as MDL-RETRIEVE01 (and `set` as CE7247), naming the
CE code, with nothing written. The valid shapes (object uses from the
entity and a specialization, the reference set's count, loop and set, a
one-to-one counted from its TO end), this bug-test, and the rewritten 1323
and 1100 bug-tests pass check -p, exec, and mx check with 0 errors.
describe -> exec of four of them (an unused and a changed self-reference
retrieve, this bug-test's parent flow, 1323's ReplaceList) reports all four
already in sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
claude and others added 11 commits October 30, 2026 11:49
One finding: four sites answered "which end does this association retrieve
start from" (the full build, the splice, check's shape, validateFlowBody),
and the full build's answer was wrong for a self-reference. The insight is
the measurement: every aggregate is CE0013 and every list operation CE0097
over the object, but range and the Change list activities are not typed by
mx check at all - which is how #1323's bug-test came to record a
self-reference retrieve as a list - and CE0100's message naming the
variable's type is the reading to trust.

#1793, #1777, #1323

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
`workflows basic` said 9.0.0 while Workflows$Workflow is a 9.0.2 type
(mendixmodelsdk 4.117.0), and only `alter workflow` applied it. On 9.0.0
or 9.0.1 `create workflow` and `create or modify workflow` passed
`check -p`, and the write then refused them part-way through exec
(canon.UndeclaredTypeError, #1774).

The row is now 9.0.2, pinned to the type by
TestTypeGates_FloorIsTheTypeIntroduction, and create, create or modify
and alter workflow all apply it. `alter workflow`'s hint named 9.12, a
version the gate never stood for; it names none now, as the message
states the registry's floor.

Fixes #1809.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…ir type floor

send rest request, range, call external action, transform … with and
find/filter by expression had no check-time gate, so `check -p` passed
them and the write refused them part-way through exec
(canon.UndeclaredTypeError, #1774).

flowActionGates names each at its storage types' introduction in
mendixmodelsdk 4.117.0, with the sub-shapes that add a later type: a
send rest request's arguments (10.2), result variable (10.4) and body
(10.5), and a call external action's arguments (10.2). It applies to
create and alter of microflows and nanoflows alike. A find whose input
the statement declares as a String is a String find(), not a list
operation, and is not gated.

The pin test now also takes the properties a gate's shape writes a
value to, so a row is pinned to the later of a type and its holding
property.

Part of #1811.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…base queries

Listing every $Type the converters construct against the Model SDK's
type data found more flow activities the write refuses below their
floor with no check-time gate: a loop over a list and a while loop
(9.0.4, Microflows$IterableList / WhileLoopCondition), show page with
arguments (9.7, Forms$PageParameterMapping), a call in a task queue
(9.10, Queues$QueueSettings), execute database query (9.24) and its
connection arguments (11.1). They join flowActionGates, the loops last
so that an action inside one is the refusal named.

The database query rows of sdk/versions said 10.6 and 11.0, floors
nothing measured; they now hold the type floors, and mx 9.24.30 loads
the action. The #1787 test's at-floor check now allows the loop's own
refusal for a loop placement below 9.0.4.

Part of #1811.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…floor

Past the workflow document's own 9.0.2 (#1809), what a workflow states
has later floors the write enforced and no gate named. Measured on 9.x
projects whose recorded version was set below each floor, a workflow
with a parameter is refused below 9.18 (Workflows$Parameter.Name) and a
user task below 9.19 (Workflows$UserTaskOutcome.Value); a jump needs
9.0.5, a call workflow activity 9.6 (its parameter mappings 9.18), an
overview page 9.11, an annotation 9.15 and an interrupting
notification event sub-process 11.10.

workflowShapeGates names each, pinned to the latest of the types and
properties the shape writes, and create and alter workflow apply them.
statementTypeGate carries a typeGate for a whole statement. The user
task row said 9.0.0; it is 9.19.0.

Part of #1811.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
… floor

The sweep of converter types found document shapes written in a type or
property newer than the project with no check-time gate: snippet
parameters, alter page add parameters, page variables, the workflow
button actions, menu item icons, progressive web app settings, a native
profile's home page, a not-found page, JAR dependencies and their
exclusions, scheduled event schedules, database connections and their
queries, business event services, and a consumed REST service's basic
authentication, operations, query parameters and OpenAPI document.

documentTypeGates names each, pinned to the latest of the types and
properties the shape writes; statementVersionGates adds them after the
per-statement cases. A database connection is 10.0, and one with a
query 10.12: measured on stamped projects, every query's QueryType is
refused below that.

create view entity was gated at 10.18, but the source document mxcli
writes for the query is a 10.21 type, so on 10.18 to 10.20 check -p
passed it and the write refused it. viewEntityGate is pinned at 10.21.
The sdk/versions rows that disagreed with the write (database connector
10.6, business events 10.0, view entities 10.18) now hold its floor.

The OpenAPI import tests ran on a 10.6 mock project, where the write
refuses the stored OpenAPI document (measured on a stamped 10.6 and
10.20); they now run at its 10.21 floor.

Part of #1811.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…wer type

TestTypeGates_CoverEveryWrittenType lists every storage $Type the write
paths construct (a "Module$Type" literal, or a modelsdk/gen constructor
resolved to the type it stamps) and, for each introduced after 9.0.0,
requires a type gate that names it, a broader gate whose floor is at or
above it, or an entry in typeCoverageExceptions saying why no project
below its floor reaches it (lowered, unreachable, or a RESIDUAL the
statement cannot tell). A converter that starts writing a newer type
fails here instead of passing check -p and being refused by the write.
Entries that are no longer written fail too.

allTypeGates is now the one list both this test and the floor pin read.

Part of #1811.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
1811-actions-below-their-version.mdl holds the 9.x and 10.x shapes and
1811-mendix-11-shapes-below-their-version.mdl the 11.x ones. On 9.24.30,
before, check -p passed the script (without the two statements main
already gated) and exec refused the database connection at write time,
and with --continue-on-error each 10.x statement; after, check -p and
the pre-flight report MDL-VERSION01 for each and App.mpr is unchanged,
while the 9.x-floor statements write and mx check reports 0 errors. On
10.24.24 the 10.x script writes everything with 0 errors, and the 11.x
script is refused by check -p where before the write refused it.

The finding records how the floors were measured without an old
mxbuild: a newer app with its recorded version stamped down.

Part of #1811.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…cate-resolver-drift

The version-floor variant came back twice more (#1809, #1811). Record
that the sweep is now a test over the written types, that a shape's
floor is the latest of its types and the properties it fills, how to
measure it with a stamped project, and that mx check is no witness for
a newer property.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Check-time gates for every write floor; self-reference retrieves; OData property sets; playwright behind a proxy; debug text
@ako
ako merged commit 1472cd5 into mendixlabs:main Oct 30, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants