Six fixes: a write path that blocked every in-place page edit, a runtime that would not start, and four checks that fired too late - #1039
Merged
Merged
Conversation
Every in-place edit of a page was refused, while a full rewrite worked:
refusing to write unit …: 1 element id(s) are used more than once
c540fbf0-… held by [Texts$Translation ×8]
`GRANT VIEW ON PAGE` failed, `ALTER PAGE … INSERT` failed, and `CREATE OR
REPLACE PAGE` did not — it rewrites the unit rather than patching it. So
the page looked correct and only UPDATES were blocked, which is why it
took enabling a second language to surface at all.
CarryTranslations pairs a rebuilt text to its stored translations by
SOURCE STRING when the two documents' text paths differ, and mergeText
appended the stored element verbatim — deliberately, because keeping the
stored $ID is what lets no-op elision fire. When several rebuilt texts
share one source string (eight copies of the literal '{1}' on a page is
entirely ordinary) they all resolve to the same stored set, and every one
got the same element, id included.
The first use keeps the stored id; each further copy gets a deterministic
derived one. Derived rather than random so the same inputs give the same
bytes, and the visit order is sorted rather than map order — otherwise
which text keeps the stored id varies per run and the document churns.
Re-identifying a copy is safe here in a way that deduplicating ids in
general is not, and that distinction is the argument for doing it at all.
An $ID is a pointer target, and rewriting one means finding every
reference to it (ADR-0008) — which is exactly why the write-time guard
refuses rather than repairs. Nothing references a Texts$Translation: it is
a leaf child of a Texts$Text with four keys and no identity anything
resolves by, so there are no references to miss.
The guard in duplicates.go recorded that the cause of the reported case
could not be established — two explanations proposed, both withdrawn. This
is it. That comment is corrected, and the guard stays: it is cheap, it is
the only thing between a write and an unopenable project, and nothing says
Texts$Translation was the only way to get here.
Verified end to end on a real 11.13 project with de_DE enabled and three
widgets sharing a caption. Pre-fix: one id used 3x, and the next ALTER
PAGE is refused with the reporting project's message verbatim. Fixed: 27
distinct ids for 27 elements, the ALTER PAGE succeeds, and mx check is 0
errors.
Three controls, because the fix trades against the property the verbatim
append existed for. The German translation still arrives, so this is not a
"fix" that stopped carrying anything. A second identical run still reports
`Unchanged page` with the same sha and mtime, so elision still fires. And
stubbing reuseSafeID reproduces the duplicate.
Reported as CapTrackV2 FINDINGS §30 and §17 (one root cause).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…ntity
Sorting a list on an attribute the entity INHERITS was not expressible in
either direction:
retrieve $Users from App.AppUser sort by Name asc;
-> mxcli check and exec both pass, then mxbuild:
[CE1613] "The selected attribute 'App.AppUser.Name' no longer exists."
retrieve $Users from App.AppUser sort by System.User.Name asc;
-> mxcli refuses: "sort by attribute 'System.User.Name' does not
belong to entity 'App.AppUser'"
So the spelling that wrote was wrong and the spelling that was right was
refused. Mendix resolves a sort reference against the entity that DECLARES
the attribute; mxcli qualified a bare name with the entity being retrieved,
and treated any other entity in a qualified name as foreign — inferring
association traversal steps, finding none, and refusing. An ancestor is not
a traversal: the attribute is on the object already.
Both halves now consult the generalization chain. The interesting part is
that neither needed new machinery: flowBuilder has carried
resolveAttributeInEntityHierarchy and entityIsSubtypeOf all along, and this
path simply did not call them. The tell was in the report — reading the
same attribute worked, because the page builder has walked the chain for
years (declaringEntityFor). When one path resolves a name and its sibling
does not, the resolver usually already exists.
Verified end-to-end on a real 11.13 project with `extends System.User`.
Pre-fix: stores App.AppUser.Name, mx check reports CE1613. Fixed: stores
System.User.Name, mx check 0 errors, and the qualified spelling that was
refused now executes.
Two controls, both of which a careless fix would break: an attribute the
entity declares ITSELF is still qualified with that entity, and an
attribute on an unrelated entity is still refused rather than passed
through to become a CE1613 at the far end of a build.
Reported as CapTrackV2 FINDINGS §13.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…needs
An association written with DELETE_BEHAVIOR PREVENT produced an app whose
RUNTIME WOULD NOT START. Not a build error — `mx check` reports 0 errors
either way:
ERROR - M2EE: An error occurred while initializing the Runtime: None.get
java.util.NoSuchElementException: None.get
at …SchemeFactory$.…$setDeleteBehavior(SchemeFactory.scala:515)
mxcli wrote ChildDeleteBehavior "DeleteMeIfNoReferences" with a NULL
ChildErrorMessage, and MDL had no syntax for the message at all.
The field is CONDITIONAL in Studio Pro's own dialog — it appears only once
that third radio button is selected — which is why it went unnoticed, and
why a screenshot of the association properties nearly falsified the
(correct) diagnosis. A census of 47,789 units across 122 projects found 423
delete behaviours and NOT ONE using this one, so no reference existed
anywhere until one was authored for this fix.
That reference (ako/TestApp, Mappings.Order_Customer) pins the shape:
"ChildDeleteBehavior": "DeleteMeIfNoReferences",
"ChildErrorMessage": { "$Type": "Texts$Text",
"Items": [ 3, { "$Type": "Texts$Translation",
"LanguageCode": "en_US",
"Text": "…" } ] },
"ParentErrorMessage": null
Two things it settles that reasoning would not: the item collection's
typed-array marker is 3, and the other side stays null because that side is
still "keep". So the element is written for that behaviour only, and only
on the child side.
The syntax is SQL's, because Mendix's three behaviours ARE SQL's
referential actions and MDL's FROM/TO already matches a foreign key's
direction — measured on the same reference: ParentPointer -> Order (the FK
owner, the FROM), ChildPointer -> Customer (referenced, the TO):
FROM Shop.Order TO Shop.Customer
ON DELETE RESTRICT
ERROR_MESSAGE 'A customer with orders cannot be deleted';
That reads the way it does in CREATE TABLE, with no knowledge of which
side Mendix calls the child. The old spelling gave none: MDL's names are
Mendix's with the word "Me" dropped, and Me was the only word saying whose
deletion was being described. DELETE_BEHAVIOR still parses and still means
the same thing, and can take ERROR_MESSAGE too, so no existing script
breaks; DESCRIBE emits the ON DELETE form because it says which side is
governed.
ERROR_MESSAGE rather than ERROR: the latter is already a token, and the
compound mirrors Studio Pro's own label. SQL's RESTRICT has no custom
message, so this clause is a Mendix extension rather than borrowed.
Also removes three ast.DeleteBehavior values no grammar rule could produce
and which named nothing Mendix has — the trap behind upstream mendixlabs#901, where
String() was used as a storage encoding.
Verified end-to-end on 11.13. A project authored by the pre-fix binary
reproduces `None.get` at boot verbatim; the fixed one writes a delete
behaviour byte-identical to Studio Pro's (the legacy engine matches
including key order; modelsdk differs only in gen's pre-existing
Parent-before-Child property order, which every association mxcli has ever
written already has). mx check 0 errors, and DESCRIBE round-trips the
message through the parser.
The empty-message shape is INFERRED, not measured — the reference captures
a filled-in message, and nobody has saved one with the box cleared. Flagged
at deleteErrorText.
Reported as CapTrackV2 FINDINGS §1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
… the newest
The bootstrap-app skill pinned `11.13.0` as the Mendix version default, which
is wrong in both directions: it ages, and it ignores what the session already
has. A Claude Code session image can bake in an MxBuild — this one carries
11.13.0 — and asking for a different version turns a no-op into a
multi-hundred-MB download of both the MxBuild and the runtime tarball.
The rule is now: use whatever is already in ~/.mxcli/mxbuild/, and otherwise
the newest version on the CDN. There is no environment variable to read (mxcli
defines none, and `mxcli new --version` has no default of its own), so the
cache directory is the signal:
ls ~/.mxcli/mxbuild/ 2>/dev/null | sort -V | tail -1
A version the user names still wins over both.
The CDN fallback names 11.14.0 — verified, along with the fact that it is
current: mxbuild-11.14.0 and mendix-11.14.0 both answer 200, 11.15.0 answers
404. It is written as perishable and the check is parameterised on $V rather
than repeating a literal that will rot the same way 11.13.0 did.
Only the skill source changes; cmd/mxcli/skills/ is gitignored and regenerated
by `make sync-skills`, which was run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
…l CE0068
A `return` inside a `loop` passed `mxcli check` and `exec`, and mxbuild then
rejected it:
[CE0068] "End events cannot be placed inside a loop."
MDL062 exists to catch exactly that and has since mendixlabs#893. It did not fire,
because it stood down for the WHOLE microflow whenever the header carried a
`returns T as $Var` clause. Two claims justified that:
1. buildFlowGraph synthesizes the End event from the variable, so none lands
inside the loop.
2. Measured: the shape builds CE0109 ("Undefined variable") rather than
CE0068, so firing here would mislabel a different defect.
The first was never true — `describe microflow` shows the in-loop `return`
written either way. The second was an artefact of the reproduction. mxbuild
reports ONE error per microflow, and the microflow that was measured never
assigned its AS variable, so CE0109 won the race and hid the CE0068 underneath.
Re-measured on mxbuild 11.13.0, adding a `declare` for that variable and
changing nothing else:
as-clause, $Done unassigned -> CE0109 "Undefined variable 'Done'."
as-clause, $Done assigned -> CE0068 "End events cannot be placed
inside a loop."
Same body, same loop, same return. So the exemption is deleted rather than
narrowed, and its test is inverted with the masking recorded in the comment —
a test asserting "clean" against a source that was never clean is how this
shipped.
The control is the half a careless fix would break: an as-clause microflow
whose loop does NOT return must stay silent, since the clause's real effect
(the terminal End event taking its value from the variable) is unchanged. Both
halves are in mdl-examples/bug-tests/captrack-19-return-in-loop-as-clause{,.fail}.mdl.
Reported as CapTrackV2 FINDINGS §19.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…ET23) A data view's own `OnClick:` parses, passes `mxcli check`, is written by `exec` without a word, and the rendered element has no handler and no role="button". `OnClick:` is an alias for `Action:` — the visitor stores both under Properties["Action"] (mendixlabs#603) — and mxcli writes that property for three widget kinds only: container/customcontainer, the buttons, and a navigationlist item. Measured on Mendix 11.13 by writing ONE page with four widgets and reading it back with `describe page`: container OnClick kept Pages$DivContainer.OnClickAction listview OnClick GONE Mendix HAS ListView.ClickAction; no mxcli writer dataview OnClick GONE Mendix models no click action on a data view dynamictext OnClick GONE same Nothing fails the build — the document is valid, the widget simply does not do what was asked — which is why this needs saying out loud and why the severity is warning, matching MDL-WIDGET20/21. The property allow-lists behind MDL-WIDGET01/07 could never have caught it: they are widget-type AGNOSTIC, the same blind spot mendixlabs#928 documented for `editable:`. Two messages, because the remedy differs. Where Mendix models no click action at all, a container inside the widget is the correct modelling — it renders with tabindex/role="button". Where Mendix models one mxcli cannot write (listview, staticimage, dynamicimage, checked against generated/metamodel), the container is a workaround and the message says so. The rule NAMES the types it reports rather than reporting everything outside an allow-list of the three writers. The allow-list version looked tighter and was wrong: running it over mdl-examples/ flagged three shipped examples, because `mxcli check` without -p has no widget registry, so lookupWidgetDef returns nil for a pluggable widget too and the caller's "static widgets only" branch does not hold — `datagrid` is DataGrid 2, whose onClick the widget engine does write. A missed warning costs nothing; a false one tells an author their working page is broken. That case is now a control test. Reported as CapTrackV2 FINDINGS §21. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…L073)
`alter settings model AfterStartupMicroflow = 'Mod.MF_Seed'` accepted a
microflow with no return type, `mxcli check` passed, and the build failed:
[CE0142] "After startup microflow should return a boolean"
#274 made ALTER SETTINGS resolve the qualified names it writes, so a
MISSPELLED microflow is caught. This one is not misspelled: the name resolves
perfectly, and the constraint is on the thing the setting names rather than on
the reference. Nothing looked at the return type. The shape that trips it in
practice is a seed/demo-data microflow wired to after-startup — it does its
work, returns nothing, and the build refuses it, well away from the statement
that caused it. `alter microflow … returns …` does not parse either, so the
remedy is DROP + CREATE (the setting survives that, being stored by name).
Two halves, one function, so they cannot drift:
- ValidateAfterStartupReturnType runs with NO project, over a microflow the
script itself creates. That is the usual shape — create the seed microflow,
then wire it — and the answer is in the script.
- validateSettingsReferences covers a microflow that was already stored,
reading its return type from the backend.
flowSignature gained ReturnKind: its existing Returns field holds the entity
name for object/list returns and so cannot tell a Boolean from a void.
Deliberately narrow, with a control test for each limit. BeforeShutdown and
HealthCheck are NOT type-checked — their rules have not been measured here, and
asserting one on a guess is the same defect facing the other way. A microflow
whose return type cannot be established says nothing rather than refusing a
script that builds. And a Boolean after-startup microflow stays clean, which a
check that simply rejected the setting would also have to pass.
Reported as CapTrackV2 FINDINGS §6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
`ACTIONBUTTON … (Action: SIGN_OUT)` was refused by the default engine:
client action *pages.SignOutClientAction not yet supported by the
modelsdk engine — rerun with MXCLI_ENGINE=legacy
The refusal was honest. The advice was not. The legacy writer had no case for
the action either, and its default branch is QUIET — it returns Forms$NoAction
for anything unmatched — so the recommended escape hatch produced a button that
rendered, said "Sign out", and did nothing, with `mxcli check`, `exec` and
`mx check` all clean. Measured on Mendix 11.13, `describe page` came back
`actionbutton btnOut (Caption: 'Sign out')` with no action at all, and the
stored BSON held Forms$NoAction.
Both engines now write the same document, and DESCRIBE renders `sign_out` so it
round-trips:
{ "$Type": "Forms$SignOutClientAction", "DisabledDuringExecution": true }
Two keys and no more, pinned against a Studio Pro-authored sign-out button in
ako/TestApp. That reference is provably Studio Pro's rather than mxcli's,
because until this change NEITHER engine could emit the type — which is also
why the shape could not have been guessed from the writers.
Verified with the fix reverted, one engine at a time: modelsdk fails with the
refusal verbatim, legacy fails by writing Forms$NoAction. mx check is 0 errors
on both engines' output, before and after — this was never a build error, which
is exactly what made it dangerous.
The control pins the fallback separately. A test asserting "SIGN_OUT is no
longer NoAction" would also pass if someone had merely softened the default, so
OPEN_LINK — still unimplemented — is asserted to STILL hit Forms$NoAction on
legacy and still be refused on modelsdk.
OPEN_LINK is left unimplemented deliberately: gen calls it OpenLinkClientAction
and its Address is an element rather than a string, so it is a separate job.
`mxcli syntax page.action` listed it as available with no caveat; it now says
it is written by neither engine and points at a nanoflow instead.
Reported as CapTrackV2 FINDINGS §10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
`ACTIONBUTTON … (Action: OPEN_LINK 'https://…')` reached storage on neither
engine. modelsdk refused it; legacy fell through to its QUIET default and wrote
Forms$NoAction, so the button rendered, said "Docs", and did nothing — with
`mxcli check`, `exec` and `mx check` all clean. Same defect as the SIGN_OUT
case in the previous commit, and the syntax help listed the action as available
either way.
Two traps here that a Studio Pro reference settled and reasoning would not.
The STORAGE NAME is Forms$OpenLinkClientAction. The semantic type is
LinkClientAction and the executor stamped "Forms$LinkClientAction", which is
not what Mendix stores — a wrong $Type that never reached disk only because
nothing could write the action at all. And the address is not a string
property: it is a nested Forms$StaticOrDynamicString.
Pinned against 31 Studio Pro-authored link buttons (ako/TestApp,
FeedbackModule) — exactly five keys, LinkType "Web" in all 31:
{ "$Type": "Forms$OpenLinkClientAction",
"Address": { "$Type": "Forms$StaticOrDynamicString",
"AttributeRef": null, "IsDynamic": false,
"Value": "https://www.mendix.com/" },
"DisabledDuringExecution": true,
"LinkType": "Web" }
6 of those 31 are DYNAMIC — the address is read from an attribute at runtime.
MDL cannot author that, so DESCRIBE flags such a button rather than printing
its address as a literal, which would round-trip into a different link.
gen declares a fourth property on Forms$StaticOrDynamicString, `Attribute`,
that not one of the 31 documents carries. It is deliberately left unset:
writing a key Mendix does not store is what produces a document mxbuild accepts
and Studio Pro cannot open.
The previous commit's "still unimplemented" controls named LinkClientAction,
which stops being a valid control the moment this lands — they now name
ShowHomePageClientAction, which has no gen type, no metamodel counterpart and
no MDL statement that builds one, so it is structurally unwritable rather than
merely not yet written.
Both engines emit the same document; mx check 0 errors on each.
Reported as CapTrackV2 FINDINGS §10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
A navigation menu can carry a log-out item. mxcli could neither author one nor
read one back:
authoring MDL's `menu item` took PAGE or MICROFLOW only, so there was no
spelling for it at all.
reading ako/TestApp's sign-out menu item came back as a plain
`menu item 'Item 5';`, so DESCRIBE -> exec turned a working log-out
entry into a dead one — silently, with mx check clean.
Fixing the sign-out BUTTON proved nothing about this, because a menu item's
action reaches storage through four switches that share no code with the button
path — two writers (a standalone menu document and the menu inside a navigation
profile) and two readers. All four had to be wired:
menuActionToGen NoAction default modelsdk, menu document
navMenuAction NoAction default raw BSON, navigation profile
resolveMenuAction raw type name modelsdk read
parseNavMenuItem raw type name legacy read
The readers are the subtler half. Both had a fallback that stored the unmapped
$Type, which LOOKS like it preserves information — ActionType became
"Forms$SignOutClientAction" — while breaking the round trip, because DESCRIBE
and both writers key on "SignOutAction". A round trip closes only when the
reader produces the exact string the writer consumes.
Studio Pro stores the same element a button carries — Forms$SignOutClientAction,
DisabledDuringExecution true, nothing else — which is why SIGN_OUT sits beside
PAGE and MICROFLOW rather than getting a syntax of its own. It names no target,
so the visitor reads it separately from the PAGE/MICROFLOW switch; folding it in
would consume a qualifiedName and mis-assign a trailing ICON. That case is in
the example.
Measured on ako/TestApp (Mendix 11.14), and controlled by neutralising both
readers and re-reading it:
before show navigation menu Responsive -> Item 5
after show navigation menu Responsive -> Item 5 -> sign out
describe -> exec now puts an identical Forms$SignOutClientAction back on disk,
and mx check reports 0 errors on the result.
Reported as CapTrackV2 FINDINGS §10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
The store describes project.md as loaded every session, and justifies giving it
the tightest cap in the store on exactly that basis. Nothing made it true. The
only route to the brain in a generated project was a row in CLAUDE.md's skills
table, and the skill's own description is symptom-triggered ("use before
designing something that looks like it was decided before") — so a session that
never hits the symptom never learns the project's decisions, and the cap was
resting on a load that did not happen.
The generated CLAUDE.md now names docs/brain/project.md as the first thing to
read, with the module shards on demand and `brain plan` for picking work up.
That is the mechanism; the skill remains the detail.
Asserted by a test rather than left to review, including that the section sits
in the first third of the file — "read this first" is otherwise a claim the
document's own ordering contradicts. Control: removing the block fails the test
with the two paths named.
Noticed by comparing the design against Anthropic's AI-native SDLC playbook,
which is explicit that CLAUDE.md is what gets read at the start of a session.
The gap was ours: we had the policy without the mechanism.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An @expect that is syntactically valid but only rejected by MxBuild took down an entire `mxcli test --local` run: no test results at all, valid tests in the same file never executed, and the cause arrived as ~200 lines of mxbuild JSON with the real error among dozens of unrelated Atlas warnings. BuildResult parsed only status and message and left the rest of the response unread, though mxbuild returns every problem with a severity, an error code and a location. Measured on 11.13, a failing build returns 18 problems of which one is the error, so printing the body meant 11,580 bytes in which nothing marked the line that mattered. Filtering to errors renders it as: [CE0117] Error(s) in expression. -- at MxTest / Microflow 'Test_test_3' / Decision '$result = 3' The location's document names the generated test microflow, so it maps back to the test exactly: that test is reported ERROR with the consistency message and every other test as SKIP -- never PASS, because nothing ran. An error in the project rather than the suite is reported as such instead of being blamed on a test. The finding's other suggested remedy -- refusing an unbound variable at injection time rather than letting the build find it -- was implemented and then removed. It passed check-mdl's 465 scripts and the whole unit suite and was still wrong: `mxcli test` execs its microflows, and the microflow validator's scope model tracks variables where they are ASSIGNED. Reusing it to check READS refuses valid work. Two independent holes, both surfaced only by `make test-integration`: - $latestHttpResponse is a Mendix system variable that no MDL statement declares; it is populated after SEND REST REQUEST - a loop iterator is registered only when the list's type is known Both refuse a microflow `mx check` accepts at 0 errors. A variable model built for checking writes only has to know the names being bound; the read side has to know every name that can legally be in scope, including ones the platform supplies. That set cannot be enumerated confidently here, and each miss refuses a working microflow -- so the build stays the authority and this change makes its verdict legible instead. Nothing is lost by dropping it: CE0109 reaches the build and is attributed to its test by the same path as CE0117. Controls: an error in a generated test microflow produces per-test rows, an error in the user's own model produces none, and no test may be reported PASS after a failed build. The response shape is captured from a real failing build rather than inferred -- no fixture here had recorded one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Two gaps, both surfaced by comparing the design against Anthropic's AI-native SDLC playbook. OPEN QUESTIONS. The store recorded what had been decided and what was going to be built, but not what was still undecided. A known unknown is brain-shaped by the store's own test — not derivable from the model, lost when the conversation ends, and expensive to rediscover — so it had nowhere to go and was simply forgotten. A question is a decision that has not been made yet, and it needs its own treatment for one reason: its anchors must NOT be checked. It routinely names something that does not exist, because the question is often precisely whether it should. Measured, with the identical anchor: exit 1 as a decision, exit 0 as a question. That is the same property that separates requirements from decisions — what a failed anchor MEANS — so this is a third point on an axis the store already had, not a new mechanism. Questions live beside the decisions they will join rather than in a file of their own, because the moment you need to see one is while reading what that module already decided. The OPEN marker therefore travels in the entry rather than being implied by the file, as a requirement's kind is. That is still one copy of the fact: what the store forbids is two. `brain resolve` converts a question into a decision IN PLACE, keeping its id and its position — an answered question is the same piece of knowledge as the question — with the question retained as the answer's context. From that moment its anchors are checked like any other decision, which is the transition the kind exists for and is asserted rather than assumed. A question filed against a slice is counted apart from its requirements: an unanswered question is not outstanding scope, and counting it as such would overstate what is left to do. A CAPTURE TRIGGER. Capture had no doctrine for WHEN, which left the two halves lopsided: the plan fills at bootstrap, while decisions fill only if someone remembers. The skill now names the trigger the playbook uses for CLAUDE.md — a correction you have had to make twice, because the second one predicts a third for someone else — plus a choice between real alternatives where the losing one would look reasonable later. `mxcli lint` now reports unanswered questions alongside unpromoted entries. Same reasoning as before: a question nobody answers is the one kind of entry that gets more expensive the longer it sits, and a report only `brain check` prints is a report nothing demands. Controls in both directions throughout, including that `--open` does not quietly disable the check for everything, that an entry written before the marker existed still reads as a settled decision, and that a resolved question stays where it was rather than moving to the end of the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…es them
The proposal described one kind of record, a decision, while the store has
three. Adds §4.5 with amendments A10 and A11.
The separating property is not three separate arguments but one: what a FAILED
anchor means. A decision's anchor points backward at what exists, so one that
stops resolving means the decision is stale. A requirement's points forward, so
one that does not resolve means not built yet. A question's points at what is
under discussion, so it means nothing at all — the question is often whether the
thing should exist. Identical syntax; only the direction differs, and that is
the whole lifecycle.
Records that this was measured before it was designed: as an ordinary entry, a
single not-yet-built requirement takes `brain check` to exit 1, and so does a
question. Neither could be more decisions without making the check useless.
Also records where this departs from the AI-native SDLC playbook and where it
was behind it. Departs: the playbook keeps a plan.md and recommends a hook
enforcing that the diff still matches it — a self-reported artifact being
policed, which is only necessary when there is no queryable model. Behind: the
playbook's trigger for CLAUDE.md ("when Claude makes a mistake twice, the
correction goes into CLAUDE.md") is the one thing this design was missing
outright, and is now A11.
And notes the defect the comparison exposed, because the lesson generalises: the
proposal asserted project.md was loaded every session and justified the store's
tightest cap on that basis, with nothing behind it — a generated project
mentioned the brain once, in a skills-table row, behind a symptom-triggered
description. A policy about what is loaded is worth nothing without the thing
that loads it, and the same question should be asked of any future claim of that
shape here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(test): attribute a failed build to the test that caused it
Sign-out and open-link actions: stop dropping them to Forms$NoAction
docs(bootstrap): prefer the environment's cached Mendix version, else the newest
The nightly is red on 10.24 and 11.6.8 while 11.12, 11.13 and 11.14 are green. Four of the 29 mendixlabs#1018 documentation fixtures cover doctypes that do not exist below 11.9 — ai model, knowledge base, consumed mcp service, agent — and had no version gate, so on an older project they failed at CREATE: --- FAIL: TestDocumentation_SurvivesRewrite/ai_model create: create model requires Mendix 11.9.0+ (project is 11.6.8) That failure says nothing about documentation carry. The doctype is simply absent, which is what a gate is for. Reproduced locally rather than reasoned about: mxbuild 11.6.0 is already cached, and MX_BINARY pins it, giving the identical message and line in four seconds. Both controls run: 11.6.0 4 SKIP, 25 run and pass, both control tests pass 11.13.0 no skips — all four actually run and pass The second one is the one that matters. A gate that always skipped would turn the whole matrix green while testing nothing, and nothing else would notice. The minimum is carried as a version on the case rather than a boolean, so the reason is legible where the case is written and it mirrors the registry entries in sdk/versions/mendix-11.yaml (agent_model, agent_knowledge_base, agent_consumed_mcp_service, agent — all min_version 11.9.0). Every other doctype passes on 10.24 and 11.6.8 untouched, which is a useful incidental result: the documentation carry itself holds across all three supported majors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves the conflict GitHub reports on the findings shard. The shard is merge=union in .gitattributes, so both appended lines are kept — but GitHub's server-side merge does not run merge drivers, which is why it shows a conflict the local merge does not have. Merging here does the union locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A generated domain model opened in Studio Pro as a single horizontal line of entities with the boxes touching — unreadable at any zoom. Reported against a 40-entity model (ako/CapTrackV2, Mendix 11.13). The default position for a CREATE ENTITY with no @position was: location = model.Point{X: 100 + len(dm.Entities)*150, Y: 100} Same y for every entity ever created. 40 entities is a 6,950px row, and 150px is narrower than an entity box, so they overlapped as well. Two changes, because there are two different questions. **The default** (mdl/dmlayout.GridSlot) is now a wrapping grid. It cannot be better than that: the first entity of a script is placed before the last one exists, so no create-time rule can see the association graph. Slot n stays a function of n alone, so adding an entity never moves one already written. Those same 40 entities now span ~1,400px. **mxcli layout** does the real thing, from the whole model once it exists. Entities are layered on the association graph — an entity referencing nothing else in the module is a lookup and goes left, everything else one column past the furthest thing it references — so association lines mostly run one way instead of crossing the diagram. Entities with no association at all go in a band underneath rather than among the lookups, which is where the non-persistent helpers belong. Run against CapTrack's own 01-domain.mdl, the layering falls out of the model: six lookups in one column, then Team/GoalBucket, then PlanScope's band, out to EmployeeMonth/Movement. mx check 0 errors, and the positions round-trip into MDL as @position. It is opt-in and it overwrites hand-arranged positions in the modules it touches, so --dry-run lists the moves first. Marketplace modules and System are skipped, and NAMING one is an error rather than a silent skip — a silent skip reports success having done nothing. Three properties the tests pin, each measured rather than assumed: - idempotent: a second run detects nothing to move and never calls the writer (16 of 16 moved, then "already laid out"). - local: adding one entity with one association moved 3 of 17, not the model. A layout that reshuffled everything would make each domain-model commit an unreadable diff. - deterministic: an unsorted walk gives a different diagram every run, which rewrites the unit every time — the churn ADR-0008 exists to prevent. The control fails on run 0. Two constraints that shaped the geometry: an entity stores only Location and NO Size (Studio Pro derives the box when it draws), so spacing is estimated from name length and attribute count; and a Mendix position is the box's CENTRE, so placement adds half a box rather than none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
fix(executor): version-gate the four doctypes that need Mendix 11.9
The layout change landed with CLAUDE.md and the Cobra help updated and nothing
else. The three that were missed are the user-facing ones.
`mxcli syntax domain-model.entity.alter` did not document SET POSITION AT ALL —
a gap that predates this work — so the topic now lists it, says the coordinate
is the box's CENTRE rather than its top-left, and points at `mxcli layout` for
arranging a whole module.
The generate-domain-model skill was actively wrong:
IMPORTANT: All entities MUST have @position annotation
Without it, entities appear at (0,0) or random locations.
Neither half held. An entity without a position took the next slot in a
deterministic row — now a grid — and never (0,0). The "MUST" was advice nothing
enforced, and the model that prompted this work ignored it, which is part of how
it came out as a 6,000px line. The section now says positions are optional, that
a grid is a default rather than a layout, and that a generated domain model is
better served by writing none and running `mxcli layout` afterwards.
New docs-site page tools/domain-model-layout.md, linked from SUMMARY.md: what
the layering does, the measured column breakdown, the flags, the fact that it
replaces hand-placed positions, and the idempotence/locality properties that
make it safe to leave in a build script.
make check-skill-mdl passes (205 blocks).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Lay domain-model entities out instead of stacking them in one row
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six independent fixes from the fork. Each one was reported by a real symptom, reproduced first, and has a control proving the fix is what changed the outcome.
Three of them share a shape worth calling out:
mx checkreported 0 errors and the thing was still broken. A build that passes is not evidence for these.The one that blocked writing
canon: one translation element was carried onto several texts. Every in-place edit of a page was refused —refusing to write unit …: 1 element id(s) are used more than once, oneTexts$Translationid held by eight elements.GRANT VIEW ON PAGEandALTER PAGE … INSERTfailed whileCREATE OR REPLACE PAGEsucceeded, because that rewrites the unit instead of patching it. So the page always looked right and only UPDATEs were blocked, which is why it survived so long.The one that broke the runtime
PREVENTneeds. An association written withDELETE_BEHAVIOR PREVENTproduced an app whose runtime would not start —NoSuchElementException: None.getinSchemeFactory.setDeleteBehavior.mx checkreports 0 errors either way, so nothing before boot catches it.Four checks that fired too late, or not at all
CE1613 "attribute no longer exists", and the qualified spelling was rejected earlier. Sorting now resolves through the attribute's declaring entity.returns T as $Varclause, which hid a realCE0068 "End events cannot be placed inside a loop."The exemption is removed; the rule has existed for this exact case since mxcli check accepts seven constructs that Studio Pro rejects (CE0038/CE0068/CE0079/CE0711/CE0249/CE0111 + unvalidated icon glyphs) #893.OnClick:was silently dropped (MDL-WIDGET23). It parses, passescheck, andexecwrites the page without a word — but mxcli only writes that property for containers, buttons and navigation-list items, so the rendered element has no handler and norole="button". Measured on 11.13 by writing one page with four widgets and reading back which kept it.AfterStartupMicroflowaccepted a microflow with no return type (MDL073), failing the build withCE0142 "After startup microflow should return a boolean". refactor: remove ExecContext.executor back-pointer #274 already catches a misspelled name here; this one resolves perfectly, and the constraint is on the thing the setting names rather than on the setting.Not included: the project-brain work already came across in #1022