Sync ako/mxcli: agent loop efficiency, page/widget writes, workflow fixes - #1163
Conversation
…ing it as an entity
`create snippet Test.SNIPPET_Label (params: { $Label: string })` — the spelling
`mxcli syntax snippet.create` printed in its own Syntax line — passed
`mxcli check` and failed at exec with
failed to build snippet: failed to resolve entity string: entity not found: string
naming a type nobody spelled (mendixlabs#1028).
`snippetParameter`/`snippetParameterList` in MDLPage.g4 were a byte-identical
duplicate of `pageParameter`/`pageParameterList` with their own visitor, which
never called buildDataType — so a primitive type never reached the AST, and
buildSnippetV3 (with no primitive branch either) took the source text for an
entity name. The same duplication had produced the quoted-name bug fixed just
before this one, and patching the copy that time left this half live and made it
silent. The duplicate rule and its visitor are gone: a snippet's Params clause
IS pageParameterList.
The obvious repair is the wrong one. Storage would take a primitive —
Forms$SnippetParameter's ParameterType is the polymorphic DataTypes$DataType,
exactly as Forms$PageParameter's is — but mxbuild will not. Measured on 11.13.0
in one run, against a project holding nothing else:
snippet params { $Label: string } -> CE0046 "Invalid data type 'String'."
snippet params, all six primitives -> one CE0046 each
snippet params { $Order: Mod.Order } -> 0 errors
PAGE params, the same six primitives -> 0 errors
So the restriction is on snippet parameters, not on primitives, and a primitive
one is refused rather than written — by one rule (types.SnippetParameterTypeRule)
that `mxcli check` reports as MDL087 and buildSnippetV3 refuses with, so a script
cannot pass one and fail the other. Both name CE0046 and the caption mxbuild
quotes, so the message matches a build log verbatim.
Also fixed, found on the way:
- pageParamBSONType returned "DataTypes$LongType", a $Type that exists in
neither generated/metamodel nor modelsdk/gen (constant_write.go has carried
the note, "storage has no LongType"). pageParamTypeToGen's default arm quietly
rescued it into a String, so a `Long` PAGE parameter had been stored as String.
It is now IntegerType, which is Studio Pro's single "Integer/Long" — verified
at 0 errors on 11.13.0 and round-tripping through `describe` as Integer.
- describe of a snippet parameter went through extractEntityQualifiedName, which
answers "Unknown" for anything that is not an entity — MDL that re-executes as
a reference to an entity called Unknown.
- pageParamTypeToGen and snippetParameterToGen were two copies of one conversion;
the snippet's could only ever produce an ObjectType. Now one builder.
- `mxcli syntax snippet.create`, docs-site and the overview-pages skill said a
snippet parameter may be a primitive. The reporter reached the bug by
following that line.
Control (recorded in the tests): with the pre-fix visitor restored, the new
executor test fails with the reported message verbatim, and with
snippetParameterToGen's unconditional ObjectType restored the write test reports
`{"$Type": "DataTypes$ObjectType", "Entity": ""}`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McnNijpTzHtdEf6mtmz53r
`ON CREATED MICROFLOW` written in the wrong position failed to parse with `mismatched input 'ON' expecting ';'`, which names neither the clause nor the rule — and cascaded into up to six more errors, including a bogus `extraneous input 'END'`. The reporter worked the required order out empirically and wrote it into their notes. `createWorkflowStatement` and `workflowUserTaskStmt` were a fixed sequence of optional groups, so every clause was optional but its position was not. They are now a set: any order, each at most once, which is how the rest of MDL reads (ADR-0003). Relaxing the grammar alone would have mis-assigned every clause, because the visitor read qualified names by counting them (`names[1]` or `names[2]` for the overview page depending on whether PARAMETER was present) and strings by index. Each clause is now read off its own clause context. Two things the clause set had to keep from the sequence: - At most once. `page M.A page M.B` would otherwise parse with the second silently winning — a worse failure than the parse error it replaces. The rule is enforced in the visitor rather than the grammar, so the message can name the clause: `duplicate PAGE clause on user task Review (already given on line 12)`. The list-valued clauses — `outcomes`, `boundary event`, and the header's event handlers — still accumulate. - The multi-user vocabulary. The MULTI alternative keeps its own clause rule, so a single user task still refuses `participants`, `decide by` and `await all users`. `targeting microflow` and `targeting xpath` now count as one clause. A user task stores one UserSource, and the sequence grammar accepted both and let whichever was written last silently win. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qPaSqkSeaM4Ziuex4nxSG
Two jobs captured their test command into a variable and echoed it on the next line: out=$(go test -v -count=1 -run '...' ./cmd/mxcli/docker/) echo "$out" The runner's shell is `bash --noprofile --norc -e -o pipefail`, so a non-zero `go test` aborts the step AT THE ASSIGNMENT and the echo never runs. The capture prints on success, where nobody reads it, and prints nothing on failure, where it is the only thing anyone wants. Measured on #594: `windows-process-regression` went red with a log holding one line of substance -- ##[error]Process completed with exit code 1 -- no test name, no failure message. The only evidence of WHICH test had tripped was the runner's own cleanup line, `Terminate orphan process: pid (2760) (PING)`, which the green run on main does not have: the `ping` grandchild that TestKillProcessGroup_ReapsGrandchildAndUnblocksWait spawns, still alive because the test had hit one of its deadlines. Replaced with `2>&1 | tee go-test-output.txt`, `status=${PIPESTATUS[0]}`, then grep the file. Output streams as it is produced, the vacuous-`-run` guard the capture existed for still counts its `--- PASS:` lines, and a real failure exits with go test's own status. Verified without CI, by extracting each step's `run:` block straight out of the YAML (yaml.safe_load) and running it under the runner's own shell flags with a stub `go` on PATH. Six cases, all correct: win job fail -> full output + "go test exited 1" exit 1 win job pass -> full output + "executed: 9" exit 0 win job vacuous -> "executed: 0" + the renamed-tests msg exit 1 seam job fail -> full output + "go test exited 1" exit 1 seam job pass -> full output + "executed: 4" exit 0 seam job vacuous -> "executed: 0" + the !linux-stub msg exit 1 Control: the OLD body, same stub, prints zero lines and exits 1 -- the CI log reproduced exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXYNJwiutu5AjxLmG7Fgsu
`create workflow … overview page X` reported `Created workflow` and exit 0
and stored nothing. The written unit carried no page reference and not even
the page's qualified name as a string; `mx check` passed, because a workflow
with no overview page is valid, and `describe workflow` omitted the clause,
so nothing revealed the loss.
Two fields for one concept, never joined. The executor set the semantic
`Workflow.OverviewPage`; the backend only ever wrote `Workflow.AdminPage`,
which nothing set. The read half was wrong in the mirror direction — the
reader took `g.OverviewPageQualifiedName()` — so even `alter workflow … set
overview page`, which writes the right key and always has, read back empty.
Which name is the stored one is settled by the Model SDK's own
StructureVersionInfo (mendixmodelsdk 4.115.0, src/gen/workflows.js):
overviewPage: { deleted: "9.11.0" }
adminPage: { introduced: "9.11.0" }
and generated/metamodel, the arbiter where it and modelsdk/gen disagree,
declares AdminPage and no OverviewPage at all. modelsdk/gen declares both,
which is how a reader and a writer in one package ended up on opposite
sides of a 9.11 rename.
The two semantic fields collapse into one. OverviewPage is the word the MDL
clause, the describer and the catalog use; the storage name stays in the
storage adapter, per ADR-0005.
Only AdminPage is written. The version branch CLAUDE.md's overlay rule asks
for on a CREATE would be dead code here, and that is a measurement rather
than an assumption: workflowToGen writes WorkflowV2 unconditionally, and
that property was introduced in 11.1.0, so no reachable project wants the
pre-9.11 key. It is still read, as a fallback — a read fallback invents
nothing.
Measured on mxbuild 11.6.6, same script and project, only the write
suppressed:
control fixed
workflow unit 2,192 bytes 2,284 bytes
page in the document no yes
describe workflow no clause overview page WF586B.Overview
mx check, valid overview page 0 errors 0 errors
mx check, page without a param 0 errors CE7410
The last row is the evidence. mxbuild can only validate a page it can see,
so CE7410 firing only with the fix proves the property reaches the platform;
the row above it is why the unit test asserts on the raw document instead of
on a build.
That rule is now documented too: an overview page takes System.Workflow,
while a user task's page takes System.WorkflowUserTask.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qPaSqkSeaM4Ziuex4nxSG
Both skills offered a microflow data source as the way past the System-module
ceiling. It is not one, and the way it fails is silent: a workflow inbox built
that way drew the right number of cards and every card was blank, with mxcli
check, lint, report and docker check all at 0 errors.
The rule, measured in a browser on 11.14.0 rather than reasoned about: a
microflow data source moves the ROWS, not the MEMBERS. A microflow does not
apply entity access, so its retrieve returns every row — but the runtime
re-applies entity access when it serializes those objects to the client, XPath
constraint included, so a row the role may not read arrives with every member
empty.
The probe is one page with TWO microflow-sourced lists over the SAME System.User
retrieve, opened by an Administrator and by a plain User:
list Administrator plain User
A the System.User objects admin, viewer (blank), viewer
B a module-owned copy, Name read admin, viewer admin, viewer
INSIDE the microflow
All four cells hold two rows, which is what proves the microflow did carry the
rows past entity access and isolates the loss to serialization. Only A loses the
values, and it loses them per OBJECT: System.User's rule grants read where
[id = '[%CurrentUser%]'], which is why a user picker shows you yourself and
nobody else while a workflow inbox — no such escape hatch for an ordinary role —
comes out entirely blank.
The control is the ROLE, not a before/after build: same binary, same model, two
logins.
- manage-security gains the rule, the measurement and a second remedy (read the
member inside the microflow, return an object your module owns), replacing the
advice that caused the rebuild.
- system-module states the rule where it repeats the ceiling, rather than linking
to it.
- mdl-examples/bug-tests/security-587-system-member-access.mdl is the probe, so
the measurement can be repeated rather than taken on trust. Verified from a
blank project: mxcli check --references clean, exec clean, mx check 0 errors.
One thing found on the way, not fixed here: `grant <Role> on System.User` PASSES
`mxcli check --references` and is refused only by `exec`, so a script carrying
one checks clean and then stops part-way. Noted in the skill and in the finding.
Closes #587
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2aYb3zDscezm874CDTH6R
fix: workflow clause order, and the dropped overview page (#586)
ci: stream go test output instead of capturing it
fix(snippets): refuse a primitive snippet parameter instead of resolving it as an entity
Correct the System-module ceiling advice with a measurement (#587)
modelsdk/mpr/version declared its own ProjectVersion struct with the same seven
fields as mdl/types.ProjectVersion rather than aliasing it, so the two were
unrelated Go types that print under the same name: a value could not cross the
mdl/ <-> modelsdk/ boundary without a conversion. The sdk/mpr copy deleted in
the legacy-engine retirement aliased the canonical type; this one did not, and
CLAUDE.md's shared-types rule asks for the alias.
A same-shape duplicate is invisible to every signal except an assignment across
the boundary. It compiles, the tests pass, and the error it eventually produces
names the same type on both sides of "want" — the shape that cost a session once
already on widget BSON, where a delegation handed back a v2 bson.D to a caller
asserting the v1 one and the failure read "widget type is bson.D, want bson.D".
So the guard is a COMPILE-TIME assertion rather than a test body:
var _ *types.ProjectVersion = (*version.ProjectVersion)(nil)
which builds only under an alias. Written first, it failed to compile with three
errors showing the two types non-interchangeable in both directions — that
failure is the reproduction.
Two measurements made the cleanup safe rather than brave. The four methods this
package redeclared (IsAtLeast, IsAtLeastFull, String, IsMPRv2) were diffed
against types' BEFORE assuming they were redundant: identical behaviour,
IsAtLeast differing only in early-return style. And the two that could not
survive as methods on an aliased type, IsSupported and SupportsFeature, were
counted first — zero callers anywhere, in this package or any other — so they go
with Feature, MinVersion, featureVersions and SupportedVersionRange. That map
described itself as "the fallback when the YAML registry is unavailable"; the
live registry is sdk/versions/mendix-{9,10,11}.yaml read through checkFeature,
so this was a second hand-maintained copy with nothing reading it.
CLAUDE.md cited this as an open cautionary case; it now states the rule and
points at the compile-time guard.
Gates: build, vet (incl. -tags integration), go test ./... (exit 0), check-mdl
(611), check-findings (1,164). Four cmd/mxcli skill-reading tests failed once in
a full run interleaved with make check-mdl, which runs sync-skills
(rsync --delete into cmd/mxcli/skills/); they pass in isolation, on clean main,
and in an uninterleaved full run, so they are not this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…thing
`update widgets` counted widgets it FOUND, not assignments that SUCCEEDED, so
a run where everything was refused still claimed success --- immediately
after warning about each refusal. Measured on a blank 11.12.2 project with
two Data grid 2 widgets:
Found 2 widget(s) in 2 container(s) matching the criteria
Warning: Failed to set 'Compact' on dgA: pluggable property … not found
Warning: Failed to set 'Striped' on dgA: pluggable property … not found
…same for dgB…
Updated 2 widget(s)
Note: Run 'refresh catalog full force' to update the catalog with changes.
Four failures out of four, an instruction to pick up changes that did not
exist, and exit 0.
`updated++` sat outside the assignment loop and was unconditional, so the
counter meant "this widget was found". The same counter gated
`mutator.Save()`, so a container whose every assignment failed was saved
anyway.
The outcome is now the three things that actually happen --- changed,
matched but unwritable, listed in the catalog but absent from the document
--- because rounding the third into either of the others is how a stale
catalog reads as success. Save is gated on something having changed, the
catalog note only prints when it is true, and a run that matched widgets and
wrote none of them exits non-zero with a message naming the likely cause.
Updated 0 widget(s)
2 widget(s) matched but had no property that could be set
Error: no widget was updated: 4 assignment(s) could not be applied. A
design property (Atlas styling) is not a pluggable widget property and
cannot be set this way — see `mxcli syntax page.styling`
exit 1
DRY RUN had the same defect one step earlier, and is the worse half because
the syntax help says to run it first: it printed `Would set …` without
attempting anything. It now applies the assignments to a discardable copy
(`pagemutator.Probe`, the seam `mxcli check` already uses for ALTER PAGE SET)
and reports `Cannot set` / `Would update 0`. Best-effort: a mutator without a
probe keeps the optimistic preview, which is no worse than before.
Severity was bounded, and the commit says so rather than letting this read as
corruption: the rebuilt document was semantically identical, so ADR-0008
elision skipped the write --- no mprcontents/ unit changed mtime, `mx check`
stayed at 0 errors. A reporting defect, not a data one.
Controls, all passing: a real pluggable property still counts, saves, and
round-trips (`PageSize: 25` via `describe page`, `mx check` 0 errors); a
partial run reports both halves; a widget missing from the document is
neither changed nor a property failure.
Not fixed here: `Compact`/`Striped` are Atlas DESIGN properties in
Appearance.DesignProperties, which SetWidgetProperty does not reach. That
capability gap is #515.
Fixes #520
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
`CREATE OR REPLACE LAYOUT` was DeleteLayout + CreateLayout with a freshly built layout, and a create goes through InsertUnit under a newly minted id. So an identical re-run replaced the layout's unit under a new GUID every time. Measured on a blank 11.14.0 project, three runs of one idempotent statement: run 1 Replaced layout … D mprcontents/8a/a3/8aa37ee1-….mxunit ?? .../fd/6e/ run 2 Replaced layout … D mprcontents/fd/6e/fd6e9c96-….mxunit ?? .../6d/2a/ run 3 Replaced layout … 3 changed files The whole .mxunit is renamed each run — a delete plus an untracked add, not churned bytes in a stable file — so `git status` never comes back clean and an MDL-generated project is not reviewable in version control. The storage-layer net for delete+insert recreates cannot reach this. It keys on the unit ID and this path re-mints it, so there is nothing to reconcile the re-insert against; the decision has to be made at the statement. execCreateLayout now keeps the stored layout's unit and rewrites it through a new UpdateLayout, which goes to UpdateRawUnit and so reaches canon.Reconcile: an identical rewrite is elided outright and a real one keeps the stored element $IDs. Duplicates of the same name are still deleted; a layout that does not exist yet still goes through the insert. The verb is reported through ReportMutation, so an elided rewrite says Unchanged rather than claiming a replacement that did not happen. Measured after: three identical re-runs report `Unchanged layout …` with 0 changed files, and a real edit reports `Replaced layout …` as a modification to the SAME .mxunit file. 0 errors on mxbuild 11.14.0 with a page bound to it. A second defect falls out, and only the control found it: a layout MOVEd into a folder was filed back into the module root on every rewrite. There is no FOLDER clause on CREATE LAYOUT, so the rebuild always names the module root and the insert applied it to the unit's row — the defect mendixlabs#932 fixed for REST clients. Measured, `show layouts` Folder column: Layouts -> (empty) on the old build, Layouts -> Layouts on this one. An in-place write does not touch the row. Neither symptom is visible to a build. With a real page bound to the churned layout, mxbuild reports 0 errors on both variants, because pages resolve layouts by qualified name and not by unit GUID — which also corrects #556's reading of this as the same class as #553, where the project stopped loading. Refs: #600, #556 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QWZjWZQhk3cNQCcy1Z2xzH
The bug test is idempotent: running it twice must leave the project byte-identical. Control on a binary built without the fix — `Replaced layout …` and 3 changed files on every re-run, against `Unchanged layout …` and a clean tree with it. CLAUDE.md gains the rule the third instance in a week earned. The #556 carry keys on the unit ID, so it does not reach a handler that re-mints one, and three did: REST client (#556), view entity OQL document (#583), layout (#600). The cheap tell is to `ls` the .mxunit filenames across two identical runs — a changed filename is delete+insert and the handler is wrong, a same filename with different bytes is the codec or a missing carry and canon is where to look. Refs: #600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QWZjWZQhk3cNQCcy1Z2xzH
`alter page … set '<design property>' = <value> on <widget>` dead-ended:
`set` reaches a handful of first-class properties and the stored widget's
pluggable property bag, while a design property lives in
Appearance.DesignProperties. The only spelling that worked was ALTER
STYLING --- a second statement for the same operation.
The same operation, measured rather than assumed. ALTER STYLING's grammar
is
ALTER STYLING ON (PAGE|SNIPPET) qualifiedName WIDGET IDENTIFIER …
with BOTH operands mandatory (omitting either is a parse error), and there
is no bulk form. So it is the one-widget-on-one-page operation ALTER PAGE
SET already performs, which "One Way to Do Each Thing" rules out having
twice.
alter page MyFirstModule.ThingList {
set 'Row size' = 'Small' on lvThings;
set 'Hover style' = on on lvThings;
};
Measured on a blank 11.12.2 project: describe styling reads back
`['Row size': 'Small', 'Hover style': on]`, mx check 0 errors --- and
writing the same thing via ALTER STYLING first makes this run rewrite ZERO
units, so elision found the two documents semantically equal. (`Altered
page` is ALTER PAGE's fixed verb, not the elision verb, so it proves
nothing on its own.)
The resolver this needed already existed with no callers.
bsonTypeToDesignPropsKey maps $Type → theme key and had never been
referenced, hence never validated; #509 deliberately avoided
standing up a third consumer of the concept before something needed it, and
this is that something. The mutator returns raw storage facts
(WidgetStorageType: $Type plus, for a pluggable widget, Type.WidgetId) and
the executor owns the mapping, so the theme concern stays in one place.
Routing happens only on a POSITIVE declaration for this widget's type.
Anything else falls through to the pluggable setter and keeps that error ---
routing on "the theme says nothing, so it must be a design property" is how
a typo becomes a silently-written design property.
Two shapes are refused, inheriting existing refusals rather than forking the
vocabulary: a flat value on a multi-select property (CE6084,
#511) and a compound one. Both name the inline
`DesignProperties: [...]` spelling, because a SET assignment carries one
scalar --- exactly as a StylingAssignment does.
TestBsonTypeAndKeywordDesignPropsKeysAgree pins both maps. It does NOT
assert they agree, because they do not, and both directions have measured
reasons: $Type-only DataGrid/Gallery are the native widgets whose MDL
keywords now resolve to pluggable ids, so the stored path resolves more than
the inline one; keyword-only Header/Footer are wrong, since MDL builds both
as Forms$DivContainer and Atlas declares no such groups --- so inline
design-property validation for a header silently skips the widget. Left
uncorrected: it changes what existing pages validate against.
Knock-on: the mendixlabs#1135 error message named ALTER STYLING as the route, which is
stale now that `set` has it. It now says the key is neither a property of
the widget nor a design property the theme declares for it, and points at
`show design properties`. The test asserting the old wording is inverted.
Part of #515; the plural ALTER PAGES form is still to come.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
…ectory The four reference pages that document the .mpr format described a `UnitContents` table holding v1's BSON blobs, and a v1/v2 detection recipe that probes for it. No .mpr has ever had that table. A v1 file has exactly two tables — `Unit` and `_MetaData` — and document contents are the `Unit.Contents` blob; `grep -rn UnitContents --include=*.go` is 0 hits. The detection recipe was the worse half: implemented as written the probe can never succeed, so it returns v2 for every project including genuine v1 ones — a wrong answer rather than an error. What the code actually does is check for the `mprcontents/` directory, falling back to whether `Unit` has a `Contents` column for a .mpr copied away from its folder. Two further fabrications on the same pages, not in the report: `Unit` was given `UnitType` and `Name` columns (it has seven, and neither is among them — type and name come out of the BSON `$Type`/`Name`), and `mprcontents/` was drawn flat when it is sharded `<XX>/<YY>/<swapped-uuid>.mxunit`. Rewritten from the SQLite catalogs of two real fixtures: the v1 project in modelsdk/mpr/testdata (Mendix 9.24.30) and the v2 project in testdata/expr-checker (11.6.6). modelsdk/mpr/docs_schema_test.go holds the pages there. Prose cannot be type-checked but the identifiers in it can: the rule checks only names beginning with a real table name against the union of the fixtures' tables and columns, so `UnitContents` and `UnitType` are caught while the catalog tables these same pages mention (`REFS` and friends) need no allowlist. The page set is discovered by content, so a page added later is covered. One consequence is stated in the test: a page that wants to say a column does NOT exist must say it in prose. Code font there is indistinguishable from the defect — the old pages' "no `UnitContents`" read as a v1/v2 difference rather than as a fiction, and an exemption for denials would have masked it. Control: with the doc edits stashed and the test kept, the failures reproduce the reporter's line list verbatim — version-compatibility.md:31, mpr-format.md:21,23, mpr-v1-v2.md:12,35,69,73,74,84,94, 10-bson-mapping.md:30 — plus the two they had not found. No bug-test MDL: the defect is in prose, and no MDL statement reproduces it. Refs mendixlabs#1072 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013kg5TN4Brse6DJ9WHbm3Ud
The "Unit Types" tables on the two MPR reference pages mapped a document's BSON `$Type` to a document kind, and 15 rows named a spelling no unit carries. Four gave the TypeScript SDK's qualified name instead of the storage name — `Pages$Page`, `Pages$Layout`, `Pages$Snippet`, `Pages$BuildingBlock`, where every real unit says `Forms$*` — and 10-bson-mapping.md lowercased eleven more (`microflows$microflow`, `pages$page`, `security$ProjectSecurity` and friends), which matters because `$Type` is case-sensitive. Selecting on either spelling matches zero units: a wrong answer rather than an error, the same failure mode as the UnitContents detection recipe in the previous commit. `CustomWidgets$customwidget` was removed rather than corrected. It is a widget element inside a page's widget tree (`CustomWidgets$CustomWidget`), never a unit. Both tables are now the measured set: every distinct `$Type` across a blank Mendix 11.6.6 app (369 units) and a 9.24.30 app (20 units), 28 in all. Types a blank project has no instance of are listed separately and sourced to mxcli's own readers and writers rather than presented as measured. TestDocumentedUnitTypesUseStorageNames holds them there. It keys on the local name after the `$`, case-insensitively: a fixture cannot prove a type absent — a blank project simply has no business-event service — so requiring every documented type to be present would fail rows that are correct. When a fixture does have a type with the same local name, the documented row must equal it exactly. That catches all four `Pages$` rows and all eleven lowercase ones with no false positives on legitimately absent types. The limit is stated in the test: a row whose local name appears nowhere in the fixtures is not checked, which is how `CustomWidgets$customwidget` slipped past and had to be removed by hand. Control: with the two tables reverted and the test kept, it fails on exactly those 15 rows and no others. Refs mendixlabs#1072 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013kg5TN4Brse6DJ9WHbm3Ud
…ties
The singular form (previous commit) styles one widget on one page. Applying
a styling decision across an app — "every data grid gets Compact and Striped"
— meant one statement per widget per page, which is how a design system drifts.
ALTER PAGES [IN <module>]
SET '<design property>' = '<value>' | ON | OFF [, ...]
WHERE WIDGETTYPE = <keyword|'widget id'>
[DRY RUN]
The selector resolves an MDL widget keyword (datagrid, combobox, ...) to the
pluggable widget id, so the statement is written in the same vocabulary the
rest of MDL uses; a raw widget id is still accepted for a widget MDL has no
keyword for. Matching reuses findMatchingWidgets from UPDATE WIDGETS, so
pages and snippets are both covered.
Reporting reuses the updateOutcome type from the UPDATE WIDGETS fix in
79f49c8, for the same reason: a bulk statement that matched widgets but
could not set the property on any of them exits non-zero and says so, rather
than printing a success line that ADR-0008 elision then quietly makes untrue.
DRY RUN goes through pagemutator.Probe(), so the preview is the real setter
on a discarded copy rather than a guess.
Verified end to end on an 11.12.2 project: dry run names both grids, apply
reports 2 styled, `describe styling` shows both properties, `mx check` is
0 errors, and a misspelled property key reports 0 styled / 2 matched with
nothing settable and exits 1.
Refs #515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
… exist yet windows-process-regression failed intermittently on TestKillProcessGroup_ReapsGrandchildAndUnblocksWait — "cmd.Wait() did not return after killProcessGroup", ~20.5s at the select deadline, with the runner afterwards reporting `Terminate orphan process: pid (5264) (PING)`. killProcessGroup itself reported no error. The readiness marker named the wrong process. The `spawn` helper started `cmd /c ping -n 60 127.0.0.1` and wrote "grandchild-started" immediately after gc.Start() returned. Start() only guarantees cmd.exe was CREATED; ping.exe — the process that ends up holding the inherited stdout pipe — does not exist yet. The test raced ahead to killProcessGroup, `taskkill /F /T` enumerated a tree ping.exe had not joined, returned 0, and ping survived holding the write end, so cmd.Wait() never saw EOF. The failing job's timing is the evidence: the 15s marker wait did not fire. Of the 20.56s total, 0.56s elapsed before the kill — the marker had arrived promptly and the kill went ahead regardless. The grandchild now announces ITSELF, with its own pid, over the inherited pipe: a third helper mode re-execing the test binary instead of `cmd /c ping`. Reading the marker therefore proves the pipe holder is running, and the test asserts processAlive on that pid before killing — a control the test did not have, so a green run now means a tree kill was really exercised rather than a race that killed a one-process tree. This is the unix half's handshake, which the Windows half had silently diverged from: procgroup_unix_test.go runs `sh -c 'sleep 60 & echo $!; wait'` and checks the echoed pid with kill(gpid, 0) before the group kill. The fix is porting the sibling's, not inventing one. Two things deliberately NOT done. A deadline bump cannot fix this — the grandchild is never killed, so no amount of waiting helps — and it would have buried the race. And the tree is now two deep rather than three (test → helper → grandchild, not test → helper → cmd.exe → ping.exe): that is the field shape being regressed, where the mxbuild.exe wrapper spawns one Deno worker, and it still fails on a single-PID kill, which is the property the test exists for. The marker parser moved to an untagged file so its one invariant is covered on every platform and not only in the Windows CI job: reject a line not yet terminated by \n, because a truncated pid parses as a perfectly plausible different pid. Control run — with the completeness guard removed, TestGrandchildPID fails on exactly the two partial-line cases and nothing else. Not verifiable on this machine: the race is Windows-only, so the fix is argued from the job log and the code, and CI is the measurement. One green run does not settle a flake. Blame was settled by a same-commit control rather than by reading a diff. Commit 0710968 ran Build, Test & Lint twice, attempt 1 each time: push (35715042749) green, pull_request (35715076433) red. Identical code, opposite outcomes. TestGrandchildPID does not match the CI job's -run pattern, so its "at least 5 tests ran" guard is unaffected. Refs #594, #601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Alias ProjectVersion instead of duplicating it
…ng script The doctype gate (`make test-integration`) runs every script in doctype-tests/ through exec + mx check against a blank project, and the 12-styling.mdl added in 78612b5 failed it two ways. `make test`, `make lint` and `make check-mdl` all pass without touching that gate, which is how it reached CI. 1. It was not self-contained. It styled `Styling.StyledPage`, a page nothing in the script creates: `Execution error: page not found`. Every other doctype script creates the module and pages it uses. 2. The design properties were native-only. `Spacing bottom` and `Full width` are declared for native widgets; a WEB DivContainer declares `Item gap`, `Card style`, `Disable row wrap`, `Background color` and friends (themesource/atlas_core/web/design-properties.json). The routing added in 992dc05 refused them correctly — the code was right and the example was wrong, which is the outcome to prefer but only if the example is fixed rather than the guard loosened. The file is removed rather than repaired: 12-styling-examples.mdl already exists, already builds a StyleTest module with pages, and already owns the ALTER STYLING sections these two levels belong beside. A second styling doctype script was duplication the PR checklist asks to look for first. The new sections use properties the web theme actually declares, and add two data-grid pages so the bulk form has something to select. `Compact` and `Striped` come from datawidgets' own design-properties.json, so the plural example exercises a pluggable widget id rather than a native $Type. Gate passes: TestMxCheck_DoctypeScripts/12-styling-examples.mdl/modelsdk, and mx check reports 0 errors on the resulting project. Refs #515 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LYaTtzjmMCUjo8XAVN1sQx
…estigation-179ttu
fix(layout): CREATE OR REPLACE LAYOUT rewrites the stored unit instead of replacing it
A side-by-side test (Opus building an app on Vercel vs with mxcli) showed 4.25x the model calls and 5.4x the conversation re-read. This proposal works out why and what to do about it. The spine is the arithmetic: every call re-reads the transcript, so total cost is calls x conversation size, and when a removed call takes its tool results with it the total falls with the square of the call count. That sets the priority order — fewer calls first, less text per call second, output tokens (500k of 228M) not at all. Six levers, ordered by that arithmetic. Two findings are already actionable against today's binary: exec runs the semantic check itself and refuses on error, yet projectGates teaches check-then-exec as two gates in every generated CLAUDE.md; and the 35s restart the session paid per change is opt-in slowness next to run --local --watch and test --attach. Sequenced with diag loop-report and a benchmark first, so the remaining levers are falsifiable rather than plausible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
… defaults The first draft called the 35s restart "opt-in slowness" next to run --local --watch. That is wrong on 11.14, and the measurement was already in the tree: on 11.14 the first build in an mxbuild --serve process succeeds and every subsequent build in that process fails, because the first does not leave the deployment in a state its own incremental build can continue from (webclient_legacy_paths.go, measured over mxbuild's HTTP API with mxcli removed, with the one-shot, Rspack and 11.13 controls). The project that first reported it routed around it with a restart per change — the exact pattern in the cost report. Three consequences, now in the proposal. It does not weaken the main lever: wall time and call count are separate axes, the defect taxes time, and the 228M bill is calls — so `mxcli apply` collapsing 5-8 calls into 1 matters whether the build under it is warm or cold, which makes it the only remaining lever on that axis rather than a lesser one. Two new actionable items. `test --attach` rebuilds through the attached app's own serve process, making its rebuild a second serve build — read off runner_attach.go, not measured, and one run on 11.14 settles it. And bootstrap-app defaults to the newest CDN version, so a fresh project lands on the broken one without anyone choosing it; the default should prefer 11.13.0 until mxbuild is fixed. Also records the standing risk: --serve is in mxbuild --help but not in the reference guide, so the warm loop rests on an undocumented interface with no compatibility promise. Argues for reporting this upstream rather than only routing around it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
…tiered Two objections to the first draft, both of which hold. The agent can already chain the loop with `&&`, so the call-count win needs nothing built. Checked that this is actually safe: exec exits non-zero on any failed statement and docker check propagates mx check's status through cmd.Run() rather than printing errors and exiting 0 — a chain over a command that reported failure only in stdout would pass silently, and that is what would make chaining unsafe. It is not present here. What `&&` does not give is the token win: it concatenates every stage's stdout. So output discipline is the more fundamental lever, not the junior partner — with terse delta-shaped output, `&&` gets nearly all of apply's value at zero new surface, and the improvement lands on every other invocation too. apply is now contingent on diag loop-report showing the published chain still gets composed wrong. Second: the first draft put a Playwright run in the default chain. That is the same mistake the cost report describes — an always-on chain trains maximal verification, and hard-wiring the browser would turn "I tested every admin flow, most checks included screenshots" from a choice into a property of the tool. The chain is now tiered, stopping at the first sufficient gate, with the routing rule that verify-in-runtime.md already owns: most changes stop at exec or the build, and the browser row is rare and the only one paying image input. Sequencing rewritten: the loop-shaping item is now a documentation change, and the only substantial build is the output work that makes it pay in tokens rather than only in calls. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
…symmetry The draft said "Vercel's agent writes a .tsx file and the work is done", which is false and was filing the gap under platform tax instead of work. TypeScript needs type-checking, building and rendering too. The difference is four properties of the verification, not whether it happens: cost per verification (tsc ~1-3s and HMR at ZERO tool calls, vs mxbuild ~25s and a tool call), how often it is needed (first-attempt success, where TS is saturated in training data and MDL appears nowhere), error locality (file:line:col vs a CE naming a document at the end of a build), and whether the last tier needs a running app. Three of the four are engineering, and two are already in flight. check-vs-mxbuild parity is a standing programme with ~17 rules shipped, each moving a construct from a 25s build to a 2s check — this proposal's contribution is to say why that is a token lever and not only a correctness one. Diagnostic quality is the other, and it matters here because a vague error costs a diagnosis, which is the 40-call tail. The real target named: build once per batch, not once per change, which is what the Vercel agent does with tsc. The blocker is trust rather than speed — an agent builds after every change because check passing does not yet mean the build passes, and #568 (docker check reporting 0 errors over a build-failing model) attacks that trust from the other side. "Mendix builds are a per-change tax forever" has accordingly moved out of the does-not-fix list. What stays there is the render tier, which is true of React too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
…bill runSemanticValidation in lsp_diagnostics.go runs the same validators as cmd_check.go — the LSP and `mxcli check` are one checker behind two front ends. So the LSP finds exactly what check finds, and the check-to-build gap that forces the 25s mxbuild per change is untouched. It makes the already-cheap tier cheaper and does nothing to the tier that dominates. Also a wash on call count as normally used: the call it would save is the redundant check that exec already folds in, and reading diagnostics is itself a tool call. The one condition under which it becomes real: diagnostics riding along with the Write/Edit result at zero extra call, which is the property the asymmetry table identifies as the Vercel agent's biggest structural advantage. Wired that way it is the only available route to a zero-call verification tier for static errors; wired any other way it is a front end onto a command we have. Records the failure mode too — auto-attached per-edit diagnostics are paid on every intermediate save of a script that is incomplete until its last line, so they want to fire on batch completion rather than per edit. The real argument it supports is spending on parity instead: because the validators are shared, every rule from the check-gap programme lands in the editor and the agent's checker at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
The proposal claimed exec folds in everything check does, making the two-gate form a wasted call in every project. That was Track A's headline and it is false. There are two validation passes and exec runs only the first. executor.ValidateProgram (package-level, semantic rules) runs in both. exec.ValidateProgram (method, project connected — reference resolution) and exec.CheckProjectConflicts (plain CREATE over an existing document) run in check only, behind --references, which -p implies. So check -p catches dangling references and create-conflicts that exec does not, and since exec is not transactional that preflight is load-bearing. The claim was read off cmd_exec.go's doc comment, which is accurate about the pass it describes and silent about the two it does not. Reading the call graph takes one more step and was skipped; that is recorded in the proposal because it is the instructive part. What survives is better targeted. The gate list's defect is its framing, not the check gate: it calls the seven gates "the definition of done ... a change is finished when they have all been run" while holding docker check, test and run --local, which read literally mandates ~55s of gates on every change — the maximal-verification pathology the cost report describes, and a contradiction of the escalation rule one line above. The fix is batching, not deletion. Also names the one real code change in Track A: exec already connects to the project, so it could run the reference pass in its preflight, which would make the check gate genuinely redundant for the apply path and stop exec half-applying a script with a dangling reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
… refused Closes #607. `mxcli check -p` runs two validation passes and `exec` ran only the first. executor.ValidateProgram (semantic) ran in both; exec.ValidateProgram (reference resolution against the connected project) ran in check alone, behind --references, which -p implies. exec already connects before its preflight, so it had everything the second pass needs. What that cost, measured on the expr-checker fixture with the pre-fix binary (--no-check reproduces it) — and NOT the failure the neighbouring refusal describes: create entity "NotAModule"."Thing" exit 0, "Created module: NotAModule" — a misspelled module is silently created microflow retrieving a missing entity exit 0, both documents written exec did not half-apply; it completed, and wrote a model only a ~25s mxbuild would reject (CE1613). So this is a check-to-build parity fix — it moves a build-tier error to the 2s tier — and it closes a violation of the "no silent side effects on typos" rule in CLAUDE.md's own checklist. After the fix both scripts exit 1 with nothing written. Safe to refuse on: the pass skips references to objects the script itself creates, so an error means the name resolves to nothing in the project AND is not created here. Gated on -p, since a script using its own CONNECT has no backend at preflight time — the same condition check uses. CheckProjectConflicts is deliberately NOT run: a plain CREATE over an existing document is worth reporting when validating a script but ordinary for a re-run, and refusing it would break scripts that work today. The test states the GAP rather than that the validator works: its first assertion is the control, showing the pass exec ran reports nothing for a script the reference pass refuses, and it fails loudly if that ever stops being true rather than being quietly relaxed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
…edit Closes #608. The gate section had a completeness rule ("**They are the definition of done, not a menu** — a change is finished when they have all been run") one line below an escalation rule ("each is only worth paying for once the one above is clean"). They contradict each other and the bolded one wins, applied to a list holding docker check (~25s), test (~30s cold) and run --local. "A change" was never defined, so in practice it became each edit: ~55s of gates and five tool calls per edit. That is measured, not hypothetical. A session-cost comparison of the same class of app built with mxcli vs on Vercel found 523 model calls against 123, with the per-change loop at 5-8 tool calls; every call re-reads the transcript, so call count enters the bill quadratically once the removed calls take their output with them. The report's own line — "I tested every admin flow ... most of those checks included screenshots" — is this instruction being followed, not an agent being careless. The fix is the UNIT, not the list. Every gate stays: the three-copy tests exist because `test` fell off this list once and became reachable only when a user asked for it by name. What changes is that the gates are done-criteria for a coherent unit of work — iterate with exec, then run them once over the result. Same shape as a TypeScript agent leaning on tsc per edit and batching the build. Held in all three places by a new test, for the same reason the gate list itself is: stated in two of three, it applies when someone remembers. Also drops --references from the check gate line, which -p implies and which the flag's own help says is kept only for compatibility, and says what check still buys now that exec resolves references too (#607): it does not apply. Generated file is 5857 of its 6000-byte budget. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
…y went The instrument the agent-loop work needs, and the first item in PROPOSAL_agent_loop_efficiency.md's sequencing, because every lever after it is a hypothesis until a number moves. Every mxcli invocation already writes a session_start naming its argv (mdl/diaglog, wired at newLoggedExecutor so it covers all commands, not a curated subset). Nobody had read it. This reports per-command call counts, wall time and median, plus `check` immediately followed by `exec` of the same script. Three things it does on purpose: Verbs resolve through rootCmd.Find rather than a hand-kept table, so a renamed or added command is picked up with no change here — a stale table would report a real command as "(unknown)", which defeats a report whose job is ranking commands by frequency. An invocation with no session_end is counted and labelled "unclosed", not "failed". mxcli exits through os.Exit on most failures and deferred Close() does not run then, so the two correlate — measured against a real log, where the 3 unclosed of 10 were exactly the 3 non-zero exits — but a killed or still-running process looks identical, so the report says "did not close" rather than asserting an exit code. Unclosed runs contribute no wall time: guessing an end from the next start would inflate the figure the report exists to make trustworthy. It prints what it cannot answer. It counts mxcli PROCESSES, not model calls; output bytes are recorded nowhere, which is the other half of the bill; and `run --local` writes no session records, so reloads vs restarts are absent. Stating those beats quietly implying the numbers are the whole picture. Also: the report filters its own invocations. diag does not log today, so this cannot be caught by running the binary — it keeps the report correct if diag ever starts. And verbStats.Unclosed is deliberately not named Failed, since loopReport.Failed means a run that closed reporting errors: one name for two populations is how a report starts lying. Tests are mutation-checked, not just green: removing the self-filter and dropping the same-script condition from the pair detector each fail their guard with the reported symptom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
Lever 2 of PROPOSAL_agent_loop_efficiency.md. "Unchanged …" is the most repeated thing mxcli prints, and a settled re-run is nothing else. MEASURED on a 40-statement script applied to a project already holding it: 41 lines / 1,604 B, every one saying nothing happened. After: 2 lines / 177 B. That is cheap in a terminal and charged repeatedly in an agent session, where a tool result is written into the conversation once and RE-READ by every later model call — so a no-op line costs its length times the number of calls that follow it. Two rules keep the collapse honest, and each came from getting it wrong. Only `Unchanged` collapses. It is the one verb that by construction reports an absence (storage was offered a write and skipped it, ADR-0008), so no line a reader would act on is replaced by a number: a mixed run still names every real write individually and counts only the rest — measured, 2 created + 1 modified + 39 elided prints the three and one summary. The obvious alternative, collapsing on volume, would hide real writes in exactly the runs where they matter most. The trigger is how many elisions arrive, not which entry point ran. A lone one is printed verbatim, since "1 document already in sync" is worse than the line it replaces. The first implementation gated on "is this a program run?", which looked equivalent and was not: `-c` reaches ExecuteProgram too, because executeMDL prepends a CONNECT statement, so a one-liner collapsed to a count of one. Caught by running it, not by the tests, and now has a test naming the measurement. Nested EXECUTE SCRIPT does not emit a second summary mid-run: begin() reports whether the call owns the tally, and only the outermost flushes. Guards are mutation-checked — dropping the held line makes the lone-line test fail with an empty report, and the control test asserts both landed writes are still named while two elided ones are not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
First slice of #611. CLAUDE.md is re-read into every context in this repo, and at 108,761 B (~27k tokens) it was 18x the 6,000-byte budget init_claudemd_budget_test.go enforces on users' projects — the same reasoning, applied outward only. Implementation Status was 33% of the file and bimodal: 36 bullets < 250 B pure capability listing ("Domain model", "Pages with 50+ widget types"). `mxcli syntax`, `help` and `lint --list-rules` answer this authoritatively and cannot go stale, so these are deleted rather than relocated. 20 bullets ~33 KB real measurement — CE numbers, the control that settled a question, the trap that cost an afternoon. Moved to the skill for that doctype, where it is loaded when the area is touched instead of on every session. One more was dropped as a duplicate: the idempotent-writes bullet summarised a Key Concepts section in the same file and the docs-site page it cited. Verified rather than assumed — every concept it named (StableId, TransplantIDs, MXCLI_ALWAYS_WRITE, the canonical form) is still present in CLAUDE.md. Nothing else was lost: three distinctive probes per moved bullet were checked against the destination after the move, and the largest deleted bullet is 139 B. Six moved sections cited a skill and, now living inside it, cited themselves — three through the flat `<name>.md` path that no longer exists. Those clauses are stripped. 108,761 -> 73,712 B (32%). Key Concepts (26%) is the next slice, and the budget test lands once the file is under target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
The generated CLAUDE.md is budgeted at 6,000 bytes because it is re-read into every context a project starts. 3065f68 added the once-per-change paragraph and took it to 5,857, leaving 143 bytes — tight enough that the next contributor to touch the file would have had to buy space from something else. Four lines to three, and the "~55s a time" figure goes: the gate list two lines below already shows ~25s for docker check and ~30s cold for test, so the reader adds up numbers that are on screen rather than being told a total that can drift from them. 5,857 -> 5,805, headroom 195. The marker phrase the three-copy test holds ("not per edit") is unchanged, and the skill and docs-site copies keep their own longer wording — neither is re-read per session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
Second slice of #611. Key Concepts was 26% of the file: 27,733 B, now 8,101. CLAUDE.md is 108,761 -> 53,950 across both slices (50%). The rule applied per subsection: a concept stays only if it is non-inferable, causes silent unrecoverable damage when violated, AND applies regardless of what is being touched. What stays is storage names, GUID-as-database-identity, the three conditional-write rules, the reserved-member names, association pointer inversion, expression escaping. Everything else is needed only when touching one subsystem. Relocated, because the content lives nowhere else: the gen wrong-key ledger and overlay-write rules to MODELSDK_ENGINE_ARCHITECTURE, TypeEnumeration ambiguity to MDL_PARSER_ARCHITECTURE, pluggable widget templates to diagnose-ce0463, AfterStartupMicroflow to project-settings, the fluent API to README. Compressed rather than moved where the canonical home already had it, verified by probing distinctive claims rather than counting greps: the tunnel section against ADR-0009 (4/6 — the two it lacked, the never-obfuscate rule and the seam file names, are kept in the compressed text) and the theme section against theme-styling/SKILL.md (6/7, the seventh a Go constant name that grep finds). That probe is why "Writes Are Conditional" was relocated instead: ADR-0008 predates the transplant work and has none of TransplantIDs, dropCollisions or carryIdentityFromRemovedUnit, and the ADR is immutable, so the mechanism moved to the docs-site internals page it already cited. Its three rules stay in CLAUDE.md — each has been violated once, and #125 shipped green without them. Public API Pattern is dropped outright; README documents it. Every moved section was probed in its destination after the move (21/21 distinctive strings present). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
…ve (#611) The checklist was 11,868 B re-read into every session, and most of it only bites when a change touches a particular subsystem: backend abstraction, full-stack wiring, version gating, test coverage, security, docs, code quality. Those moved into /mxcli-dev:review, which is the command that applies them and which previously only pointed back at CLAUDE.md. Two subsections stay, because they govern how the work is DONE rather than how it is reviewed, and an agent that never runs the review command still has to follow them: the bug-fix evidence bar (test written first, verified at the layer the symptom lives in, fix proven to be the cause by reverting it) and one-thing-per-commit. That distinction is the whole judgement here. Moving the evidence bar too would have made it apply only when someone remembered to ask for a review, which is the same failure the three-copy gates tests exist to prevent. Renaming the section broke two references to it, in CLAUDE.md's own welcome and in review.md step 2; both updated, and a grep confirms none are left. 11,868 -> 2,839 in CLAUDE.md; 53,950 -> 45,074 overall. Six distinctive strings from the moved subsections verified present in review.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
Four sections were restating what a command, a directory listing or a frontmatter description answers authoritatively — the same pattern already removed from Implementation Status. The 27-row CLI feature table goes: `mxcli help <command>` and `mxcli syntax` own it. Two facts in it existed nowhere else and were relocated first — the wrapping-grid/6,000px default for an entity with no @position, to domain-model-layout.md, and `fix widgets` clearing CE0463 (measured 203 -> 0 on a vanilla 11.12.1 app), to diagnose-ce0463.md. The skill list goes, on this file's own rule that a skill's frontmatter description IS the index — the table drifted to 12 of 68 before mendixlabs#906 caught it. The always-on MDL idioms it contained stay. The directory tree goes; `ls` answers it. What replaces it is the orientation the layout does not give you: that modelsdk/ is the engine and sdk/mpr is deleted, and how mdl/'s grammar -> visitor -> ast -> executor chain fits together. The file index becomes routing only, for the entries whose annotation is not derivable from the filename: the CE0463 elimination order, the verify-in-runtime tier, the both-type-and-object template rule, and that System-module string lengths are measured rather than taken from the Model SDK. Three CLI rules were kept because no --help states them: generated parser files are not committed, skills are edited in .claude/skills/mendix/ and not the rsync --delete'd embed dir, and mxcli-dev/ commands are not synced. Also merges "MDL Syntax Quick Reference" into "What mxcli Can Do", which had come to say the same thing, keeping the link. 108,761 -> 25,514 B across the four slices (76%, ~27k -> ~6k tokens). No dangling relative links. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
The last item on #611. init_claudemd_budget_test.go argued the case for a user's project — a file re-read into every context is a per-session tax rather than a one-off — and then applied it outward only, while the repo's own CLAUDE.md grew to 18x that budget in the same package. Set at 28,000, just above the current 25,514. Higher than the generated file's 6,000 because this one legitimately carries more: the invariants whose violation is silent and unrecoverable, and the evidence bar for a change. Deliberately not generous — enough to edit within, not enough to regrow into, so adding something means taking something out. The test is mutation-checked rather than merely green: lowered to 20,000 it fails naming the real size, so it is measuring the file and not passing vacuously. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
Agent loop efficiency: the proposal, three fixes, and the CLAUDE.md that motivated them
fix: correct the MPR format reference docs, and hold them to real fixtures
…pixels Closes #614. Most screenshotting during agent work answers a textual question — did the page render, is there an error banner, did the grid get rows. A PNG costs ~1,500 tokens to read against ~100 for a verdict, and the cost is not a one-off: an image read into a conversation is re-read by every later model call, so one PNG early in a long session is charged hundreds of times. It is also the weaker instrument. Measured against two real rendered pages: page /p/customers title="Customers" h="Customer overview" rows=2 text=21 console-errors=0 page /p/customers title="Customers" NO VISIBLE TEXT console-errors=1 ERR Cannot read properties of undefined (reading 'items') The second is the blank-page symptom WITH ITS CAUSE NAMED. A console error is the commonest reason a page renders blank and it does not appear in a picture at all — that class of bug took ~40 calls to trace in the session behind PROPOSAL_agent_loop_efficiency.md, every one of them looking at pixels that could not show it. --page-check prints the verdict and no PNG; --screenshot now prints it alongside the PNG, so the cheap signal arrives whether or not it was asked for and the image need not be opened. No new dependency: the Playwright CLI has no text-dump subcommand, so the probe runs under the node Playwright already needs, resolving the package via `npm root -g`. It never throws on a bad page — a page that fails to render is the thing being measured, not an error in measuring it. Brevity is load-bearing, so everything unbounded is clamped and a test holds the verdict under 600 B on a page with 5,000 rows and 40 console errors: a verdict that grows with the page defeats its own purpose. Also adds the routing rule to verify-in-runtime.md, which had nothing about cost (0 mentions) — the proposal stated it and the skill never got it, so nothing steered an agent from pixels to text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
… a bug
The proposal listed the five Mendix limitations the cost report named and
proposed turning each into a check diagnostic or skill. That was written
from the report's framing without verifying any of them. Checking all five
afterwards:
inputs in lists read-only an MXCLI BUG, fixed 2026-09-06 — List View
has its own Editable (default No) that wins
over the textbox's; buildListViewV3 never
read it and the writer wrote false
popup styling covered in theme-styling/SKILL.md
scripted login input covered in test-app/SKILL.md, with the
playwright-cli eval workaround
wrong Java version handled in docker/javaversion.go (11.14 is
the first wanting 25 rather than 21)
stale sidebar the only one still open, and it is runtime
refresh behaviour with no static signal
So the lever as written would have shipped one diagnostic that is now
actively wrong — inputs in lists ARE editable, and saying otherwise sends a
reader back to a pop-up workaround they no longer need — and three that
duplicate skills that already exist.
What the correction reveals is a ROUTING problem rather than a knowledge
one: the knowledge was there, in the skill whose description is supposed to
surface it, and the session hit the wall anyway. Same failure as the skill
table drifting to 12 of 68 (mendixlabs#906) — an index that does not route is
indistinguishable from missing content.
The principle survives; the lesson measuring adds is the prior step. Check
whether it is already known, and whether it is even true, before encoding
it. A platform limitation that was really a bug outlives the bug.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
The nightly failed on Mendix 10.24 in TestMxCheck_DoctypeScripts: alter settings workflows add group '<name>' requires Mendix 11.2.0+ (project is 10.24.24.119349) Examples 4.3-4.7 in 14-project-settings-examples.mdl were added ungated, though Settings$WorkflowGroup is 11.2 metamodel and the executor rightly refuses it on an older project. Wrap them in a `-- @Version: 11.2+` section, with the directive above the doc comment so it isn't orphaned. Reproduced against mxbuild 10.24.24.119349: the test fails unfixed with the nightly's exact error and passes with the gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcJgoFWn5vFoQ2Zw8dp3DE
Answer the screenshot's question in text, and correct a proposal lever that measurement disproved
fix(examples): gate the workflow-group examples to Mendix 11.2+
… log Closes #617. Found by testing loop-report against a real project (mxcli-ledger FINDINGS phase 41, finding 153); reproducing it here showed the hole was wider than reported. `loop-report` prints "mxcli invocations: N" as though it counted every run. It counted only commands that built a logged executor — 14 files against 53 registered commands — and only when the run survived long enough to reach that code. Measured, watching invocations/unclosed move: -c against a missing .mpr exit 1 +1 / +1 check against a missing file exit 1 +0 / +0 check on a REAL file, no -p exit 0 +0 / +0 The third row is the part the ledger did not see: newLoggedExecutor("check") sits inside the `if checkRefs` block, so a check without -p never logged at all. That makes the ledger's own headline — 250 of 301 invocations are `check` — an undercount, which strengthens its conclusion rather than weakening it. What it hid is the point: burning calls on wrong paths and missing files is a characteristic agent failure and is exactly what this report is opened to see. `failed` stayed 0 while real non-zero exits happened. Now initialised once per process from PersistentPreRun, before argument validation, so an invocation is recorded whatever the command and however early it fails. Init is a singleton, so the commands that build a logged executor later get the same logger instead of opening a second session. The first attempt fixed counting and broke the signal: with no matching close, a SUCCESSFUL check was recorded as unclosed, turning every clean run of a non-logging command into a false failure. PersistentPostRun closes it, and cobra skips that on os.Exit — which is how nearly every failure path ends, so "no session_end" remains the tell. Verified in both directions: successes counted and closed, failures counted and flagged. Making Init a singleton broke isolation in three existing tests, which had relied on it always creating fresh state; they now reset explicitly. Also corrects the report's own caveat, which named `run --local` as the exception and so implied everything else was covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
Closes #618. Reported in mxcli-ledger FINDINGS phase 41 (finding 154) and reproduced verbatim. $ cat bad.mdl this is not valid mdl at all; / $ mxcli check bad.mdl Check passed! exit 0 $ mxcli exec bad.mdl -p Ledger.mpr exit 0, nothing applied Root cause, confirmed at the visitor: Build() returns zero statements AND zero errors for text the grammar cannot begin to parse, so it is indistinguishable from an empty file — and an empty script is legitimately a pass. It is not a lenient parser: a malformed statement that STARTS with a keyword is still caught. So a truncated file, a lost heredoc, or a path that resolved to the wrong kind of file passed both gates and moved nothing, silently, at exit 0. For a workflow whose premise is that mdlsource/ replays, that is the worst available outcome — the replay reports success and the model does not change — and an agent cannot see it, because both commands it would use to check say everything is fine. Both gates now refuse zero statements from non-empty input and name the first line that did not parse, since "the parser never got into your file" is only actionable if it says where. exec refuses rather than warns: "apply this file" that applies nothing is never what was meant. Three controls keep the guard narrow, and are tested: an empty file, a whitespace-only file and a comments-only file all still pass, because each is a legitimately empty script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
…it suggests `diag loop-report --json` emitted `failed`, which stayed 0 across a real 442-invocation log while runs were genuinely exiting non-zero. Beside `"unclosed": 10` in the same object it reads as "nothing failed" — the opposite of the truth. The counter was correct; the name described a different population. It fires only for an invocation that CLOSED while its summary reported errors, and that count is diaglog's statement-level counter — reachable only via `exec --continue-on-error`. A run that actually fails exits through os.Exit, skipping the deferred Close() and PersistentPostRun, so it writes no session_end and lands in `unclosed`. The two populations are disjoint by construction. failed -> runs_with_statement_errors (JSON), Failed -> StatementErrors (Go) and the text report now prints it when non-zero, stated as separate from the unclosed count. Zero is suppressed: a 0 beside a non-zero unclosed count is the misreading itself. renderLoopReport takes io.Writer so that output is testable. Making `failed` mean failed is the larger change — an exit-code path through ~250 os.Exit sites, since Go has no atexit. `unclosed` carries that signal today and the report explains what it is. Proven by revert both ways: restoring the JSON tag fails the key test with `"failed":2` in the payload; relaxing the guard to >= 0 prints `Finished with failed statements: 0` directly under `Did not close: 2`, which is the reported symptom exactly. Closes #620 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
`make check-mdl` went red on 15-fragment-examples.test.mdl with #618's message: "the parser could not begin reading it. First line that did not parse: create module FragTest;" — a line the parser was never handed. The same 416 bytes under a plain .mdl name parse fine, at 18 statements. Two defects, stacked. **Mine.** A .test.mdl is not top-level MDL: each block is a microflow body, so `check` parses testrunner's RENDERING of it (mendixlabs#1103). The #618 guard was given `string(content)`, the file as read. A file with no @test block renders to nothing, so the guard compared zero statements against the non-empty ORIGINAL and quoted a source line from text the parser never saw. It now runs on `source` — whatever was actually parsed — and an empty rendering from non-empty content gets its own message naming what is really missing. **Pre-existing, and what the guard found.** That fixture declares no @test at all; it is a syntax demo misnamed .test.mdl, its sibling 15b-fragment-slots-examples.mdl being plain. So it rendered to nothing, check printed "Check passed!" on zero statements, and check-mdl had been reporting PASS over 416 lines nothing ever read — concealing a real MDL-PAGE20 violation (page parameter $Customer, url with no {Customer} segment). #618 is "a silent no-op is the worst outcome", and the repo's own suite held an instance. Renamed to .mdl, url fixed, now genuinely checked. roundtrip-issues.md listed it under "require Docker runtime", which was never true — it needed no runtime and was checked by nothing. The list is a record of a past manual run, so the entry stands with the correction beside it rather than being rewritten. Proven by revert end-to-end: restoring `string(content)` reproduces the CI message verbatim on the same bytes under the .test.mdl name. The new test asserts the predicate check branches on (the rendering being empty) with a control showing the same statements inside an @test block do render. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgcYSQrLLbUcnAMaCHpyqQ
Three defects that testing loop-report against a real project turned up
AI Code ReviewWe are reviewing a PR titled "Sync ako/mxcli: agent loop efficiency, page/widget writes, workflow fixes" The PR has 38 commits and includes:
We are to review against the checklist in CLAUDE.md. Let's break down the review by the checklist sections: Overlap & duplicationWe need to check for duplicate implementations. The PR introduces several new features and fixes. We should look for:
Looking at the diff, we see:
We must check if these duplicate existing functionality. From the PR description:
We don't see obvious duplication of existing commands or features. MDL syntax designThe PR introduces new MDL syntax? Let's see: From the PR:
These are modifications to existing
We must check if the new syntax follows the guidelines. Looking at the diff for MDL changes:
We must check the new syntax for:
Looking at the new type AlterPagesStylingStmt struct {
Module string // "" = every module the project owns
Assignments []StylingAssignment // reuses ALTER STYLING's assignment shape
WidgetType string // MDL keyword (`datagrid`) or a full widget id
DryRun bool // report the matches and write nothing
}And The property format for the assignments would be inherited from We must check if the new statement follows the pattern:
This seems to follow the pattern. However, note that the condition uses We must also check that the statement reads as English: "ALTER PAGES SET 'Row size' = 'Small' WHERE WIDGETTYPE = datagrid" - which is understandable. We don't see any keyword overloading or symbolic syntax. The PR also mentions workflow changes: allowing clauses in any order. This is a change to the The workflow changes are in the visitor and parser. The grammar for workflow was changed to allow clauses in any order (by making each clause optional and not position-dependent). This is acceptable and does not violate the syntax design guidelines because it doesn't add new keywords or change the verb. Full-stack consistency for MDL featuresWe must check that any new MDL feature is wired through the full pipeline. New MDL features in this PR:
For the bulk styling:
Looking at the PR description, it doesn't mention DESCRIBE for this feature. However, note that the existing We see changes in We must verify that the DESCRIBE for a page that has been styled by the bulk statement would output the bulk statement. If it outputs individual But note: the bulk statement is a new way to express the same set of changes. The DESCRIBE could output either form and still be correct. However, the checklist says: "if the feature creates artifacts, DESCRIBE should output re-executable MDL". The feature (the bulk statement) does not create a new artifact; it's just a different way to express a change to existing artifacts (pages). So as long as the DESCRIBE outputs some re-executable MDL that produces the same change, it is acceptable. Given the complexity and the fact that the PR is large, we might need to flag this as a potential issue if we don't see DESCRIBE updated. However, note that the PR does not mention any issue with DESCRIBE, and the existing DESCRIBE for pages might already output the individual SET statements. Since the bulk statement is just an optimization, it might be acceptable for DESCRIBE to output the individual statements. But let's look at the diff for Alternatively, we can note that the PR does not add a new artifact type, so the DESCRIBE roundtrip requirement might be satisfied by the existing DESCRIBE for pages. For the workflow clause order change:
We see a new file For workflow, we see:
We must look for workflow executor changes. The diff shows:
But we don't see an obvious workflow executor file. However, note that the workflow executor might be in Alternatively, the workflow execution might be handled by the same mechanism as other statements via the executor dispatcher. We see in Given the complexity, and since the PR is about fixing the clause order (making it free), we assume the executor was already handling the workflow statement via the backend and the change only affected the visitor (building the AST) and the executor already used the AST fields (so no change needed in executor). This is acceptable. We do not see any LSP or DESCRIBE changes for workflow, but again, workflow is not an artifact that DESCRIBE outputs? Actually, we have We see changes in However, note that the PR adds test files for workflow in Without seeing the DESCRIBE for workflow, we cannot be sure. But the PR description says: "CREATE WORKFLOW clauses are accepted in any order" and the test files are in bug-tests, so it's likely that the DESCRIBE was not changed and still outputs in a fixed order. However, the requirement for DESCRIBE roundtrip is that if you DESCRIBE a workflow and then EXECUTE the output, you get the same workflow. If the DESCRIBE outputs in a fixed order and the workflow was created in a different order, then executing the DESCRIBE output would produce a workflow with the same meaning (because the clauses are order-free) but the text would be different. This is acceptable because the meaning is the same. Therefore, the DESCRIBE roundtrip is satisfied in terms of semantics, even if the text is not identical. Test coverageWe see:
We must check that there are no Security & robustnessWe don't see any obvious security issues in the selected diff. The new We see that the Windows grandchild marker was fixed (in Scope & atomicityThe PR has 38 commits and covers multiple areas: agent loop efficiency, pages/widgets, workflows, and corrections. The PR description says: "The bulk is one investigation — why an agent-driven mxcli session costs several times what the same app costs on Vercel — plus the fixes it turned up. The rest is pages/widgets, workflows and a set of corrections to documentation that was measurably wrong." This suggests that the PR is not scoped to a single feature or concern. However, the agent loop efficiency work and the fixes it turned up are related (the fixes were found by investigating the loop efficiency). The pages/widgets and workflows fixes are separate Automated review via OpenRouter (Nemotron Super 120B) — workflow source |
38 commits since the last sync. The bulk is one investigation — why an agent-driven
mxcli session costs several times what the same app costs on Vercel — plus the
fixes it turned up. The rest is pages/widgets, workflows and a set of corrections
to documentation that was measurably wrong.
Agent loop efficiency
A side-by-side build of the same class of app put mxcli at 4.25x the model calls
and 5.4x the conversation re-read (523 vs 123 calls; 228M vs 42M cache-read
tokens). Every model call re-reads the whole transcript, so the total is calls x
conversation size — which means removing a call removes its tool output from every
later call too, and the cost falls with the square of the call count. Output size
is not the lever: 500k of 228M.
docs/11-proposals/PROPOSAL_agent_loop_efficiency.md, with fivecorrections kept in-tree rather than dropped, each recording how the wrong claim
was reached. Four were mine: "check before exec is redundant" (it is not — three
different validation passes), "the 35s restart is opt-in slowness" (it is an
mxbuild 11.14 defect, filed as On Mendix 11.14 every rebuild in an
mxbuild --serveprocess fails, so the warm loop is a restart per change ako/mxcli#606), "a .tsx file is done when written"(a false asymmetry that filed our gap under platform tax), and lever 5 (four of
its five items were already done, the fifth was a bug fixed two weeks earlier).
execresolves references in its preflight (exec's preflight skips reference validation, so it half-applies a script with a dangling reference thatcheck -pcatches ako/mxcli#607). Measured before:create entity "NotAModule"."Thing"exited 0 and reported "Created module:NotAModule"; a microflow retrieving a missing entity exited 0 with both
documents written. Project-conflict detection stays
check-only, since a re-runhitting an existing document is ordinary.
diag loop-report— the session logs already record every argv, and nobodyhad looked. Run before and after a change it turns "the loop feels expensive"
into evidence. First real use contradicted the proposal's own assumption:
docker checknever appeared, and the dominant command was the cheapest one at~83% of invocations.
run --page-check(run --local --screenshotanswers a textual question with an image, and the image is re-charged on every later model call ako/mxcli#614) — a text verdict where a screenshot wasbeing used as one: title, heading, row count, visible-text length and console
errors, in two lines instead of an image.
1,604 B down to 2 lines / 177 B. The trigger is how many elisions arrive, not
which entry point ran, so a single elision still prints verbatim.
linked docs rather than deleted, with a test holding it at 28,000
(CLAUDE.md is 108 KB — 18× the 6 KB budget it enforces on users' projects ako/mxcli#611). The generated per-project CLAUDE.md keeps its 6,000-byte budget.
Three defects the loop-report work found once it ran against a real project:
diag loop-reportcounts only the minority of commands that happen to log, and presents the total as if it were all invocations ako/mxcli#617).newLoggedExecutorsat insidecheck'sif checkRefsblock, so acheckwithout
-pnever logged;docker check,run,testandmarketplacewrotenothing either. The same window went from 301 to 442 invocations, with
checkshowing 10 unclosed where it had shown 0. The first fix broke the signalin the other direction — a clean run of a non-logging command became a false
failure — so closing moved to
PersistentPostRun, which cobra skips onos.Exit, keeping "no session_end" as the tell.checkand is silently applied byexecako/mxcli#618).visitor.Buildreturns zero statements and zero errors for text the grammar cannot begin to
parse, so it is indistinguishable from an empty file:
checksaid "Checkpassed!" and
execapplied nothing, both at exit 0. A truncated file or a lostheredoc therefore replayed as success while the model did not change. Both gates
now refuse it and name the first line that did not parse; empty, whitespace-only
and comments-only files stay passes, with a test each.
failedfield never counted a failure (diag loop-report: thefailedfield never counts a failure ako/mxcli#620). It read 0 acrossa 442-invocation log while runs were exiting non-zero, because it counts runs
that closed while reporting statement-level errors — reachable only via
exec --continue-on-error— and a real failure exits throughos.Exitand landsin
unclosed. Renamed toruns_with_statement_errorsand printed only whennon-zero; a 0 beside a non-zero unclosed count was the misreading itself.
That last guard then went red in CI, and it was right to:
15-fragment-examples.test.mdldeclares no
@testblock, so it rendered to nothing andmake check-mdlhad beenreporting PASS over 416 lines nothing ever read — concealing a real MDL-PAGE20
violation. Two fixes:
checknow diagnoses a test file on the text the parser wasactually given (it had been quoting a source line the parser never saw), and the
fixture is renamed to
.mdland genuinely checked.Pages, widgets and layout
ALTER PAGES … WHERE WIDGETTYPEbulk-sets design properties across pages.ALTER PAGE … SETwrites Atlas design properties.identities survive. Regression case included, with the delete+insert tell.
UPDATE WIDGETSno longer reports success after writing nothing.entity.
Workflows
CREATE WORKFLOWclauses are accepted in any order (CREATE WORKFLOW clause order is load-bearing, and the parse error does not say so ako/mxcli#586).Corrections backed by measurement
$Typestorage names in the MPR unit-type tables were wrong.UnitContentstable — detection is by directory.(manage-security understates the System-module ceiling: a microflow datasource gets rows, attributes still render blank ako/mxcli#587); the lengths come from a built
model.mdp, not the Model SDK,which does not contain them.
ProjectVersionis aliased, not duplicated.CI
go testoutput is streamed rather than captured (Bind a pluggable widget's textTemplate to an attribute (#575) ako/mxcli#594), so afailure is visible while the job runs.
cmd.Start()guarantees only thatcmd.exewas created, notping.exe, sotaskkill /F /Tenumerated a tree the grandchild had not joined andcmd.Wait()hung. It read as Windows flakiness and was attributed to whicheverPR happened to be red. The marker is now emitted by the process under test, over
the channel under test, matching the unix half's handshake