Sync ako/mxcli: rewrites stop deleting what the statement did not restate, authorable message definitions, and a project knowledge store - #1022
Merged
Conversation
…opping it
A mapping has FOUR source kinds, not three. Beside a JSON structure, an XML
schema and a message definition, it can be sourced from an imported web
service — a WSDL binding: ImportedWebService plus ServiceName, OperationName
and RootElementName (and ParameterName / IsHeader on an export mapping).
model.ImportMapping carried three and a comment saying "Schema source (at most
one is set)", so neither engine read the fourth and every CREATE OR
REPLACE/MODIFY destroyed it. Measured on mxbuild 11.13.0: describe emitted the
mapping with no source clause at all, and describe -> exec — which is how a
document is copied — left ServiceName and OperationName blank and removed
ImportedWebService and RootElementName outright:
[error] [CE6896] "A mapping must have exactly one schema source."
[error] [CE0270] "No root element could be found in the schema."
So a working SOAP integration became an unbuildable one, and the diff blamed
the statement the user ran rather than the source they never mentioned. Same
class as the queued-call guard (ADR-0005), and worse than #259's dangling
reference, which wrote a bad name through rather than deleting a good one.
The binding is now read on both engines and the rewrite REFUSED, naming the
service and operation that would have been lost. Refusing rather than
preserving is deliberate: carrying the binding through a rebuild would claim
the rest of the document survives too, and mxcli cannot check that — a SOAP
mapping's elements resolve against the WSDL's schema entries, which live inline
on WebServices$ImportedWebService and which mxcli does not read. describe marks
the source as NOT REPRESENTABLE rather than emitting nothing, because the
silent output parses and re-executing it is the deletion.
Verified against planted import and export mappings on both engines: refused,
binding intact afterwards, ordinary JSON-sourced mappings still rewrite.
Closes #365
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…placed ones A Microflows$MicroflowParameter is a stored node with real geometry (RelativeMiddlePoint + Size 30;30) that Studio Pro lets you drag, but no MDL annotation reached it. Two consequences, the second the more serious: a generated flow's parameter block landed wherever the writer put it, and an existing hand-aligned one was MOVED by any rewrite — including a describe -> exec of mxcli's own output. Measured on a real nanoflow, a parameter at -77;0 came back at 200;53; on a 1971-unit project, 20 of 28 parameters sat off the derived grid and so were moved. The cause was a four-point chain: the semantic type had no position field, so neither reader carried one, so both writers could only recompute 200+idx*100;53 inline, and the grammar had no slot to state one. The placement rule is @start's, unchanged (mendixlabs#884, mendixlabs#951): a parameter sitting exactly where the layout would have put it is mxcli's own arithmetic handed back, carries no intent, and is re-derived; one anywhere else was placed by a person, survives a rewrite, and is emitted by DESCRIBE. Carrying stored coordinates over unconditionally is the trap that rule exists to avoid — inserting a parameter would strand the existing ones on the old grid while the new one landed on top. The arbitration lives in the readers, so a non-nil Position means intent everywhere downstream; it is a pointer because 0;0 is a coordinate a person can choose. Syntax needed one grammar line (annotation* on microflowParameter), with no ANTLR ambiguity, so the block-level @parameters(...) alternative is unnecessary. @position is the only annotation a parameter takes; anything else — a typo of it above all — is refused as MDL059 rather than parsing and doing nothing, in check, exec and the LSP. Covers microflows, nanoflows and rules (shared parameter grammar) on both engines. The four near-duplicate describers now share one helper, so the annotation cannot appear from one command and not another. Verified on mxbuild 11.13: authored positions stored verbatim, the unannotated control still deriving to 200;53/300;53, describe -> exec reporting Unchanged, 0 errors. Control: with the read-side carry stubbed, -77;0 -> 200;53 returns and DESCRIBE emits nothing. Refs: ako/mxcli#993 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
A mapping binds to one of four schema sources and MDL can create exactly one. Measured across all 327 import/export mappings in the nine demo apps: JSON structure 250 (76.5%), message definition 74 (22.6%), XML schema 3 (0.9%), imported web service 0. So a project built entirely through MDL can express three-quarters of the mappings a real app has. Message definitions are the one remaining source that is both worth doing and doable: unlike an XML schema or a WSDL, the document holds nothing external — it is a selection over the domain model. The spec is measurement-first, over all 36 collections / 56 definitions / 4,686 elements. Almost every property is a constant or is derived; the author chooses only the collection name, each definition's name, its root entity, the members, and an optional rename. Two findings shape the syntax: MaxOccurs is NOT a function of the association's type — all 927 resolvable ones are Reference, yet 526 are MaxOccurs=1 and 401 are -1. It tracks the DIRECTION of traversal, with zero counter-examples: holder is the FROM entity → 1 (496), holder is the TO entity → -1 (401). Getting that backwards exposes a list as a single object with no build error behind it, so the statement names the target entity (`Assoc/Module.Entity`, the shape mappings already use) and direction is explicit rather than inferred. Studio Pro pluralises a repeating element's ExposedName (Reference→References, Factory→Factories, Parts→Parts) while keeping ExposedItemName at the singular — 461/461. ExposedItemName is therefore free; the plural is not, and the proposal recommends NOT implementing English inflection, defaulting to OriginalName with `as` to override. Same conclusion as #272's item naming, and the cost of it is stated rather than buried. Closes nothing; opens the design for review. Refs #272 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolving the findings shard by hand, because `merge=union` got it wrong. main did not append to `findings/mdl-executor.jsonl` — it REWROTE it, deleting 250 records that the bug-pattern consolidation had folded into docs-wiki/bug-patterns/. Union never deletes, so it resurrected all 250: the merged tree had 1,135 records where main has 884. Resolved as main's shard verbatim plus this branch's one new record (885). Verified: the 250 exist nowhere else on main's shards, so they were deleted rather than re-sharded, and none of them survives here. Worth noting for the next person: union is right for concurrent appends to a file of independent records — which is what .gitattributes documents — and wrong the moment one side deletes or rewrites. A silently inflated file is the failure mode, and `make check-findings` counts records without noticing they came back from the dead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(mappings): refuse to rewrite a SOAP-sourced mapping instead of dropping it
feat(microflow): @position on a flow parameter, and stop moving hand-placed ones
docs(proposal): spec authorable message definitions
…requires it The first draft had only collection-level CREATE/DROP/DESCRIBE/SHOW, on an unstated assumption that a definition is a flat field list. Measuring the corpus shows it is not: definitions nest to depth 7, with most elements at depths 5-7 (2,208 at depth 7 alone), a mean of 4.7 members per object element and a tail out to 22. So a whole-document CREATE OR MODIFY is a poor tool for "expose one more attribute" — restating a seven-level document to add a leaf is exactly the diff-unfriendliness ADR-0003 argues against, and is why ALTER ENTITY ADD ATTRIBUTE exists rather than only CREATE OR MODIFY ENTITY. Adds two statement families: alter message definition collection M.MD_Order add|drop|rename definition ... alter message definition M.MD_Order.Order add|drop|set member ... Three choices worth reviewing. The definition is addressed as Module.Collection.Definition, the three-part reference WITH MESSAGE DEFINITION already takes, so the two cannot drift apart. It is SET member ... AS, not RENAME ... TO, because ALTER ENTITY's RENAME changes the model and rewrites every reference while this changes only the element's ExposedName — borrowing the verb would promise something far larger. And a nested member is reached with `in <exposed-name path>` rather than a /-joined name, because / already means "association to entity" inside a member. Also raises a wildcard member (`( * )`) as an open question rather than designing it: the checkbox-tree UI makes it a natural thing to want, but nothing in the corpus says anyone does, and proposing it would be designing for a shape with no document behind it. Verification plan gains the control that separates a targeted edit from a rebuild: add one member and assert every other element is byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ot compared `mxcli diff` against the UNMODIFIED output of `mxcli describe` reported that the script would delete activities the executor round-trips exactly: every java-action call, `download file`, `show message` and every @position/@start/@Curve line rendered as a deletion with no counterpart. Retrieve constraints came back as a Go struct pointer (`where &{0x236139b542d0 index=0}`), and an entity dump read as modified purely on `create PERSISTENT entity` vs `create persistent entity`. One cause, not four: diff rendered its two sides with two different renderers. The project side went through the real describer (renderMicroflowMDL, already shared with diff-local); the script side went through a second AST-to-MDL renderer whose statement switch covered 18 of 43 activity types with NO default case, so an unhandled activity emitted zero lines silently. Its expression switch ended in `%v` on an interface holding a struct pointer, which is the pointer leak. The fix is structural. The build phase of the CREATE handlers is split out (buildMicroflowFromStmt / buildNanoflowFromStmt) so diff can assemble the flow the script describes without writing it, then render it through the same describer the stored side uses. The second renderer is deleted rather than extended — adding the 25 missing cases would have fixed the symptom and left the drift that produced it. Two things the split has to get right, both load-bearing: the build phase MUTATES (findOrCreateModule and resolveFolder create documents, consumeDroppedMicroflow consumes session state), so a dry run takes an AllowCreate=false path with a read-only folder lookup; and the exec-only refusals are skipped there, because a refusal that aborts the build would leave the user with no diff at all rather than a diff plus the warning exec gives anyway. Separately, diffStatement skipped every statement it could not compare without a word — 6 of 158 statement types are supported — so a script that would genuinely add a constant summarised as "0 new, 0 modified, 0 unchanged". That is worse than a wrong count, because nothing is on screen to disbelieve. Unsupported statements are now listed under "Not compared", and a comparison that fails is reported instead of dropped. Verified on mxbuild 11.13: an unmodified describe dump of a microflow, a nanoflow and an entity all diff as unchanged, while a one-word edit still reports exactly one line and removing the java action still reports a deletion. exec is unaffected (0 errors, still idempotent). Control: re-instating the drop of unrendered activities brings the false deletion straight back. mendixlabs#913's split-indentation test drove the deleted renderer and is retargeted at formatMicroflowActivities, where that rule now lives. Refs: ako/mxcli#997 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Response to mendixlabs#1017, which asks for a proposal and for its four stated assumptions to be checked against the codebase rather than trusted. All four needed amending and one is wrong outright. - Documentation via MDL: available for domain-model objects via doc comments and ALTER … SET DOCUMENTATION, but uneven — `create … comment` was a dead option on seven doctypes and was removed rather than wired. Phase 3 now opens with a per-doctype audit. - The catalog: the objects view unions 43 document types with a QualifiedName, and resolution measured at 0.038 ms on a real 382-object project, so speed should not shape the design. Two caveats do: the view indexes only describable types, so a missing anchor is not proof of staleness, and member-level anchors need attributes_data. - Starlark: rules are discovered from files, so a generated rule is just a generated file and no engine work is needed — but `mxcli init` writes into the same directory, so generated rules need a reserved prefix. - Release mechanics: THERE IS NO GORELEASER. Releases run `make release`; skills ship by go:embed from cmd/mxcli/skills, mirrored with rsync --delete from .claude/skills/mendix. The proposal also brings evidence from mxcli's own store of this shape — the bug findings and their wiki digest — because it has already failed in four ways reachable from the brief as written: it grew past being readable, its digest went three months without a trigger, append-only plus merge=union produced 256 silent duplicates that are on main right now, and claims about the mechanism (including "it runs in CI") went stale. Those failures argue for the brief's caps and human-in-the-loop promote, not against the feature. Three open questions are recorded rather than answered, including that THEORY.md — which the issue says to read and update — does not exist anywhere in the repository. No implementation. The issue asks for review before code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two corrections from review, and a measurement that changes a
conclusion.
**Audience.** The proposal conflated two stores. The bug findings and
their wiki digest are for developing mxcli itself — a Go repo, many
parallel agent sessions, hundreds of entries. This brain is for USERS of
mxcli, in their own Mendix project: one developer and their agent, tens
of lines, reviewed by someone who may never see mxcli's source. Stated
up front now, and §3 is reframed as an analogy with the differences
named rather than as a precedent. Three of its failures transfer
(unbounded growth, an untriggered curation step, stale self-reported
claims); the duplicate-merge one does not, and the earlier draft
over-weighted it.
**Documentation.** `create … comment` was removed BECAUSE javadoc-style
comments exist and reach the .mpr — not because the capability was
missing. Verified: a /** … */ header on a microflow and on an entity
lands in the stored .mxunit and comes back through DESCRIBE. Wired at 28
sites across mdl/visitor. The earlier draft drew the wrong inference
from the removal.
But testing the next question found worse: a rewrite DESTROYS it.
microflow doc entity doc
after create PRESENT PRESENT
after replace mf ABSENT PRESENT <- control holds
after modify entity ABSENT ABSENT
Each rewrite destroys its own object's documentation and leaves the
other alone, and mx check is clean throughout because a document with no
documentation is valid. So a statement that says nothing about
documentation silently deletes whatever was promoted there. Tier 1 —
the strongest idea in the brief — is blocked until mxcli's writers
preserve documentation they do not restate, which is a writer fix and
belongs in its own issue.
The first version of that test chained `&& echo SURVIVED` to `head -1`,
which exits 0 on empty input, and reported success regardless of what
grep found. Recorded in the proposal, because it is the same shape as
the failures §3 catalogues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Filed as mendixlabs#1018. Also records the second control found while writing the repro: ALTER ENTITY … ADD ATTRIBUTE preserves the doc comment, which localises the defect to the rewrite paths and gives users a workaround today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(proposal): project brain, with the brief's assumptions verified
fix(diff): render both sides through one describer, and say what is not compared
docs(proposal): add targeted ALTER for message definitions, and the nesting measurement that requires it
This gate built ZERO image widgets, so it ran green through both image defects fixed in 597db1b -- the CE0463 on every image widget mxcli wrote, and the image reference that could not be authored at all. The file said "no pluggable replacement exists" and skipped images entirely, which was wrong: `image` routes to com.mendix.widget.web.image.Image. Only the BUILT-IN staticimage/dynamicimage are deprecated (CE0582) and those stay out. bug-tests/widget-image-collection-entry.mdl documents the fix, but bug-tests/ is not what CI runs -- TestMxCheck_DoctypeScripts reads only doctype-tests/. Three variants, chosen so the coverage is not a smoke test: - pixels/36, where width is a VISIBLE property - default units, where width and height are HIDDEN -- the case that regressed, since a hidden property must be written with its declared default rather than skipped onto whatever the template captured - imageUrl mode, whose source is a text template rather than the Image field Non-vacuity measured against the fix that is actually in the tree, not a reconstruction of it: with the declared-default write in hiddenUnnamedProperties reverted to a plain skip, this script reports CE0463 on all three widgets; restored, 0 errors. Omitting the `Image:` line likewise turns mx check red with "No image selected.". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEZmExJUvn2nWTWE9mrd4i
The user manual was the one artifact the authorable-layouts arc never
updated, and it was wrong rather than merely thin.
create-layout.md documented `CREATE LAYOUT module.Name { widget_tree }`
-- no header at all. Run verbatim that is an error, not a shortcut:
"layout needs a layouttype". It omitted `class`, which is load-bearing
(Atlas scopes its layout rules to .layout-atlas, so a layout without one
builds clean, passes mx check and renders with no topbar bar and no
sidebar rail), omitted OR REPLACE, the scrollcontainer/region/placeholder/
navigationtree vocabulary and the Marketplace refusal, and told the reader
"for advanced layout customization, use Mendix Studio Pro" -- for
something mxcli has done since #304. Neither of its two "Examples" was a
CREATE LAYOUT; both were CREATE PAGE statements naming an Atlas layout.
Rewritten from measurement: every statement in both pages was executed
against a real 11.13 project and every quoted error message was produced
by running the case that raises it, including the marketplace refusal, the
unknown-header-property error, the no-placeholder refusal and the
placeholder guard with its MAP remedy. The resulting project is 0 errors.
alter-layout.md is new: ALTER LAYOUT had no page on the site at all, nor
did the bulk `ALTER PAGES ... SET LAYOUT` migration form. It covers
addressing a region by slot, why ALTER beats CREATE OR REPLACE for a
layout you did not write, and the guard's refusal semantics. alter-page.md
already documented the single-page SET Layout, so that section gains only
the guard note and a pointer.
Also: `layout` is a top-level syntax path but was missing from the
hand-maintained topic list in help.go, so `mxcli syntax layout` worked
only if you already knew the word.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEZmExJUvn2nWTWE9mrd4i
0dd1e01 corrected all three navigation writers to store the fallback page as Navigation$NotFoundHomePage, but added a regression test for only one of them (mdl/backend/modelsdk). Reverting either of the other two -- sdk/mpr/writer_navigation.go, which is the DEFAULT engine's writer and the one that produced the unopenable project in the report, or modelsdk/mpr/nav_patch.go -- left the entire suite green, so the defect could be reintroduced on the default path undetected. Measured on Mendix 11.13 (Linux) against a blank app, with controls: untouched app 0 errors, exit 0 pre-fix write StorageLoadException, exit 1, no error count post-fix write 0 errors, exit 0 fix reverted symptom returns verbatim Mendix names the cause itself: "Object of type 'Mendix.Modeler.WebUI.Navigation.HomePage' cannot be converted to type 'Mendix.Modeler.WebUI.Navigation.NotFoundHomePage'". Each new test is verified to FAIL against a reverted writer, naming the stored $Type, rather than only passing against fixed code. Two guards accompany the pin. HomePage must keep Navigation$HomePage: the slots are adjacent and differ only in $Type, so a rename applied one sed too wide converts the home page as well. And an unset fallback page must stay an explicit null rather than an element with a blank Page, which would be a dangling reference where absent is the modelled default. Refs mendixlabs#1000 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…s and menus
An icon naming an image collection instead of an icon collection passed
`mxcli check --references`, executed cleanly, and surfaced only at build:
[error] [CE1613] "The selected custom icon
'Mod.Icons_SVG.cmdFilter24' no longer exists." at Action button 'abBad'
The reported case -- a button in a page body -- was already caught, and
the resolver shipped in 0.18.0 while the report is against 0.19.0. This is
not a resolver bug but a coverage gap: iconRefsInStatement walked only
CreatePageStmtV3.Widgets, AlterPageStmt operations and
AlterNavigationStmt, so four icon-bearing shapes were never inspected at
all, each holding its widgets in a field the walk did not visit:
placeholder X { … } CreatePageStmtV3.Placeholders, held apart from
the bare body in .Widgets
CREATE SNIPPET CreateSnippetStmtV3, absent from the type switch
CREATE LAYOUT CreateLayoutStmt, absent from the type switch
CREATE MENU CreateMenuStmt, whose NavMenuItemDef items carry
icons exactly as a profile menu's do
Layouts are the costliest of the four: a layout's topbar is shared, so one
wrong icon there is an error on every page using the layout.
Measured on Mendix 11.13. Before, each of the four was silent at exit 0
and produced its own CE1613 from mx check; after, all four are caught at
check time. The positive control is what makes the refusal trustworthy and
ships with the fixture rather than beside it: the same four shapes with a
VALID icon still pass --references, exec, and build to 0 errors. An
over-eager walk would have traded a silent miss for a false refusal.
The unit tests are at the walker, which is where the omission lives, and
each was confirmed to fail before the change.
Refs mendixlabs#1008
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
`FOR` binds a Mendix user role, which is project-level and written as a
bare name. Nothing resolved it: cmd_navigation.go turned the AST value
into a string and all three navigation writers dropped it into the BSON
`UserRole` key unexamined.
What that produced depended on the shape of the value. Measured on a
blank 11.13 app:
for Administrator → mx check exit 0, 0 errors
for Supervisor (bare, unknown) → CE1613, an ordinary build error
for MyFirstModule.Administrator → the project will not LOAD:
StorageLoadException: ... 'MyFirstModule.Administrator' is not a
valid UserRoleIdentifier
The third is a tier worse than a build error — it is raised before any
checking runs, so there is no error code and no location — and it was
the form mxcli's own documentation recommended, in ten places including
one runnable example. A module role is module-scoped and shares the
name: a blank app has a user role Administrator and module roles called
Administrator in three modules, so the wrong one reads as correct.
`check --references` and `exec` now refuse it through the same function,
naming the bare form to write. A bare unknown role and a case mismatch
(Mendix matches exactly, so `for administrator` really is CE1613) get
their own messages, because they need different fixes.
Two controls, both in the tests:
- a refused exec leaves the .mpr byte-identical, so nothing partial
lands before the refusal;
- a script that CREATEs a user role and then uses it still passes.
collectDefinitions does not track user roles, so a project-only
lookup would have refused the ordinary way to write one.
Docs corrected across the reference, the language pages, the quick
references, the synced manage-navigation skill and `mxcli syntax` — the
last two being what an LLM reads before writing navigation MDL.
Fixes mendixlabs#1001
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…e $Type Eight places recorded that mxbuild accepts either Navigation$HomePage or Navigation$NotFoundHomePage in a profile's NotFoundHomepage slot, and that this is why nothing caught the writers emitting the wrong one. Both halves are false. Measured on 11.13, against a build of the three writers emitting the old spelling (verified on disk, not assumed): `mx check` and `mxbuild --target=deploy` BOTH exit 1 with System.ArgumentException: Object of type 'Mendix.Modeler.WebUI.Navigation.HomePage' cannot be converted to type 'Mendix.Modeler.WebUI.Navigation.NotFoundHomePage' The project cannot be loaded, so every check downstream is lost with it. The shipped writer is the control: same script, same app, 0 errors. What actually let it through is that nothing had ever built a project with a fallback page set. The automated mx-check coverage runs doctype-tests/ only, and no script there sets one -- the first that does was added by the fix itself. This matters beyond the wording. The claim told the next reader that a whole class of $Type error is invisible to the build, which is the opposite of true and would send them looking in the wrong place. It also gave the wrong reason for the reader accepting both spellings: a pre-fix project does not build at all, so reading the old $Type is the repair path -- mxcli parses the BSON directly and is not bound by Mendix's load validation -- not evidence that those documents are fine. The findings record is corrected in place with a bumped date, per the convention in findings/README.md, and carries the generalisable form: "the build tolerates it" needs the same control as the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
mendixlabs#1018. `create or replace` / `create or modify` set `Documentation: s.Documentation` unconditionally, so a statement with no `/** … */` comment wrote the zero value OVER the stored one. Not a drop — an empty overwrite. The run reported success and mx check stayed clean, because a document with no documentation is valid. Absent and empty are different facts, and the parser is the only layer that knows which: findDocCommentText returned "" for both. It now has a sibling, findDocComment, returning the text AND whether a comment was written, and the statement carries a DocumentationSet bit. An absent comment preserves what is stored; an explicitly empty /** */ clears it. Preserve-always was not an option. There is no ALTER MICROFLOW … SET DOCUMENTATION — the SET DOCUMENTATION / SET COMMENT clauses exist only in the domain-model grammar — so an empty comment is the only clearing spelling a microflow has. The mechanism already existed. The same handler reads folder, allowed module roles, Excluded and the toolbox MicroflowActionInfo off the stored document and carries them; Documentation was never added to that set, so this is a fifth member of a list of four rather than new machinery. Scope is 26 statement types — every one with a doc comment and a rewrite flag — not the two in the report. Three are fixed and covered end to end. The other 23 are enumerated in a coverage test that reads mdl/ast/*.go and fails on any type in neither the done nor the pending list, so the remaining work is a number somebody can read and a new doctype has to make a decision about documentation rather than inheriting the bug. Controls, both of which needed a second attempt: - the bystander test (a second object, untouched, keeps its own documentation) separates "rewrites drop documentation" from "writes drop documentation"; - stubbing the carry proves the tests detect it — but deleting the block failed to COMPILE on unused variables rather than failing the test, so the condition is stubbed to `if false` instead. A control that does not build is not a control. The first survival probe chained `&& echo SURVIVED` to `head -1`, which exits 0 on empty input and reported success regardless of what grep found. Recorded in the finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s#1018) Queue, regular expression, scheduled event, nanoflow and rule join entity, microflow and enumeration: 8 of 29 doctypes now preserve a doc comment the rewrite did not restate, each covered by the survival table. A shared carriedDocumentation helper replaces the per-handler conditional, so the rule is stated once and every call site is greppable. Three things the survey caught that a mechanical pass would have got wrong: - The scheduled-event struct is built by a pure function with no access to the stored event, so the carry belongs at the CALL SITE, next to the existing Interval/IntervalType carry. Inserting it where the struct is built does not compile. - Java actions hand-roll the doc-comment search with extractDocComment rather than calling findDocCommentText, so the mechanical rewrite of the shared helper missed them entirely. - CreatePageStmtV3 and CreateSnippetStmtV3 were invisible to the coverage test: its pattern required a name ending in Stmt, so every ...StmtV3 type was excluded from a list whose entire job is to be complete. Widened, and image collections (which spell the field Comment) are now caught too. True scope is 29, not 26. The rule case needs the modelsdk engine — legacy refuses to author one — so the table carries a per-case engine flag. Without it the case fails at its own precondition and says nothing about the defect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s#1018) 15 of 29 doctypes now preserve a doc comment the rewrite did not restate: json structure, image collection, workflow, constant, association, view entity and business event service join the first eight. Every one is covered by the survival table. Two shapes needed more than the standard carry. Associations already preserved, by accident of an `if doc != ""` guard on the OR MODIFY path — which also made documentation UNCLEARABLE there. Routed through carriedDocumentation with a stated() helper that counts either a doc comment or a COMMENT clause, so preserve and clear now both work and the rule is the same one everywhere. Constants keep COMMENT taking precedence over the doc comment, which is pre-existing behaviour; the change is only that a rewrite mentioning NEITHER now keeps what is stored. The test grew a second assertion mode. DESCRIBE does not render an association's or a view entity's documentation, so a describe-based check reports a false failure at its own precondition. Those cases read the stored units instead, which is what the original mendixlabs#1018 measurement did and is the more faithful question anyway: what is in the model, not what the reader reports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…llections First slice of #272's implementation (proposal in docs/11-proposals/PROPOSAL_authorable_message_definitions.md). Parses only — nothing reaches storage yet. A message definition is the source for 74 of the 327 mappings in the demo corpus (22.6%), and was the only one of a mapping's four schema sources a script could not create. Unlike an XML schema or a WSDL it holds nothing external: every element names an entity, an attribute or an association. Statements: create [or modify] message definition collection M.Name [folder '...'] ( ... ) drop | describe message definition collection M.Name show message definition collections [in M] alter message definition collection M.Name add|drop|rename definition ... alter message definition M.Collection.Definition add|drop|set member ... Two lexer notes, since a new keyword is how scripts break. MEMBER is new (only MEMBERS existed); ANTLR prefers the longest match so `members` still lexes as MEMBERS, and MEMBER is added to identifierOrKeyword so an attribute called `Member` still parses. COLLECTION already accepted singular and plural, so `show message definition collections` needs no new token. An association member spells out its target entity (Assoc/Module.Entity, the shape mappings already use). That is load-bearing rather than decorative: the stored MaxOccurs tracks the DIRECTION of traversal and not the association's type — measured, all 927 resolvable associations in the corpus are `Reference` yet 526 store 1 and 401 store -1 — so naming the target is what makes the direction explicit instead of something a reader has to work out. Refs #272 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Java action, JavaScript action, menu, OData service and OData external entity join the carry. 18 of 29 doctypes are now DONE, where done means carried AND covered by the survival table — nothing weaker. That definition tightened because it had to. CreateODataServiceStmt has TWO update paths. The first carry patched one of them, the type sat in the done list passing every build, and the defect was fully intact. The test caught it. So JavaScriptAction and ExternalEntity, which are carried but have no fixture yet, are listed as PENDING rather than done — an untested carry has now been demonstrated to be worth nothing. Menus turned out to have a different bug. Their doc comment was parsed and read by nobody, so the documentation never reached the model at all — a write gap, not the rewrite gap of mendixlabs#1018, found only because the survival test failed at its own precondition rather than at its assertion. One line in visitor_menu.go, and the carry then applies. Two more paths were the accidental-preserve shape already seen on associations: an `if x != ""` guard on the OData external entity and the REST client kept the stored value but made documentation unclearable. Both now route through carriedDocumentation, so preserve and clear work the same way everywhere. Fixture notes worth keeping: java actions resolve their module from an uncached ListModules and cannot see a module created earlier in the same harness session, so that case uses a module the copied project already has; and DESCRIBE renders no documentation for associations, view entities, business event services or OData services, so those cases assert against the stored units instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
build(deps): x/crypto v0.55.0 → v0.56.0 for GO-2026-6354/6355
…served-on-rewrite
Fourth slice of #272. The feature works end to end. describe -> exec against ako/TestApp's hand-authored collection reproduces Studio Pro's document exactly: the executor reports "Unchanged", meaning canon.Reconcile found the rebuilt content semantically equal and elided the write. mxbuild 11.14.0 reports 0 errors on a project holding an mxcli-authored collection and an ALTERed one. The executor's job is resolution, and two resolutions need the domain model. Both were wrong first and were caught by the round trip: MaxOccurs on an association follows the DIRECTION of traversal, not the association's type. Holder is the FROM entity -> 1; holder is the TO entity -> -1. An association that connects the two entities in NEITHER direction is REFUSED rather than defaulted: a wrong cardinality exposes a list as a single object and builds cleanly, so refusing is the only honest answer. PrimitiveType is MAPPED, not passed through. Measured across 3,372 exposed attributes: Long -> Integer, AutoNumber -> Integer, Enumeration -> String, everything else identity. A pass-through gets 279 elements wrong, and the round trip caught it on TestApp's ProductId (a Long stored as Integer). Inherited attributes resolve to the entity that DECLARES them, reusing DeclaringMemberRef — 398 of 3,697 exposed attributes in the corpus are inherited, and qualifying one against the entity that merely uses it is CE1613. `example '...'` is new syntax, added because describe emitting nothing for it made describe -> exec lossy. Rare (1 of 4,707 elements) but real, and silently dropping an authored value is the failure this project keeps guarding against. ALTER edits the stored document rather than rebuilding it, so definitions the statement does not mention are never round-tripped through the describer. Dropping or renaming a definition a mapping still references is refused, naming the mappings. Refs #272 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fifth and final slice of #272. mdl-examples/doctype-tests/40-message-definition-examples.mdl runs clean end to end and mxbuild reports 0 errors, including an import mapping bound to the authored definition. It also demonstrates the same association traversed both ways, which is the property most likely to be got wrong. Writing it caught one real bug: after a CREATE that resolves a folder, the cached hierarchy predates that folder, so a later CREATE OR MODIFY did not find the collection and wrote a DUPLICATE (CE0122). The update branch gets the invalidation free from applyDocumentFolder; the create branch has to say it. JSON structures avoid this only because their create path happens to call applyDocumentFolder first. The skill section moved to reference/message-definitions.md — the body crossed the 700-line bound — leaving a pointer with the two facts a reader needs before they follow it. The proposal is marked implemented, with two scope decisions corrected against measurement rather than left as written: inherited attributes are IN (398 of 3,697 exposed attributes, 10.8%), and Example is IN (author-set, rare, and silently dropped if hardcoded). It also records the five derivations that only a hand-authored document revealed. Refs #272 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolving findings/mdl-executor.jsonl by hand: `merge=union` resurrected a record main had DELETED. main removed a 2026-09-01 navigation finding (consolidated into docs-wiki/bug-patterns/), and union never deletes — so the merged shard had 506 records where main has 504 plus this branch's one. Resolved as main's shard verbatim plus the new record: 505. Second time this has bitten (see the merge in #369). Union is right for concurrent appends to a file of independent records, which is what .gitattributes documents, and wrong the moment either side deletes or rewrites — and `make check-findings` counts records without noticing they came back from the dead, so the check is on line counts against main, not on the tool. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ain/ A single decisions file and a bare `docs/` were both carried over from the brief unexamined. Neither survives a 100-module Mendix project. One file makes the size cap a project-wide budget, so recording a Sales decision competes with a Finance one and `promote` starts refusing on exactly the projects that need the store most; and because the store is loaded every session, an agent working in one module pays context for the other ninety-nine. Shard on anchor scope instead — `project.md` for cross-cutting, and `modules/<Module>.md` for anchored entries. The key is derived, not chosen: an anchor's module prefix is its file name, so there is no index to maintain (A6 forbids one anyway). Two things become possible that a single file cannot support: `check` can assert a shard's anchors resolve with the matching `objects.ModuleName`, and `--changed` can validate only the shards a diff touches. Shards are created on demand and the universe is smaller than the module count — marketplace modules never carry decisions, and `modules.Source` already distinguishes them. Measured on the sample project: 9 modules, 7 of them Marketplace, so 2 could ever own a shard. This is not the one-file-per- record shape that turned the analogous store into 600 files. Home it at `docs/brain/` rather than `docs/`: a labelled subfolder can be added to a customer's or Studio Pro's docs tree without collision, and the folder name is what tells a reviewer the files are brain-managed. That answers open question 2. Not a root-level `brain/` (a Mendix project root is already crowded, and discoverability is the skill description's job), and not an `mxcli-` prefix (that marks files mxcli generates and may clobber — the opposite of entries a human promotes). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The proposal recorded documentation loss on rewrite as fatal to tier 1 and made the fix a precondition of phase 3. That fix has since shipped (#377): every rewrite path carries the stored documentation when the statement is silent, and a fixture asserts survival for all 29 rewrite-capable document types, with an untouched-object control and an empty-comment-clears case. Leaving the blocked status in a live proposal is the failure §3 warns about — a self-reported claim that goes stale — so it is corrected rather than left for the reader to discover. Also carries forward two testing mistakes from that work, because the brain will need the same controls: a control that does not compile is not a control (deleting the carry block failed on unused variables rather than failing the test), and a type is only done when the carry is both written and covered (one statement had a second update path no fixture exercised). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gh a rewrite describe -> exec — how a document is copied — silently dropped a mapping's OriginalValue (the sample parsed from the JSON structure's snippet) and reformatted the structure's snippet from one line to multi-line. No build error either way; pure diff churn against a Studio Pro original. The old behaviour rested on mendixlabs#882, which measured TWO mappings a blank app ships and concluded Studio Pro always leaves OriginalValue empty. At corpus scale the opposite is more common: 2,322 of 3,042 value elements whose structure carries a sample store it. But neither global default is right, and measuring the SPLIT is what shows it. It is per DOCUMENT, not per element: 145 mappings carry the sample on EVERY element 107 carry it on NONE 2 are mixed So which one a mapping gets is a property of how and when it was authored, not something mxcli can derive. Always-copy is wrong for 107 mappings; always-empty is wrong for 145. A REWRITE does not have to choose — it knows what was stored, so it carries it (guard-don't-drop, ADR-0005), matching stored to rebuilt by JsonPath because names and order can change while the schema binding cannot. mendixlabs#882's actual decision is left intact: a NEWLY authored mapping still writes empty. Its comment is corrected in place rather than deleted, since the decision stands and only its measurement was too narrow. The export side needed the field added to the semantic type and both readers — its codec writer hardcoded "" rather than carrying it at all. The snippet half is the same shape: keep the stored formatting when the JSON is semantically equal, comparing decoded values rather than strings. Anything that does not parse counts as different, so a malformed snippet is replaced rather than silently kept. Verified against ako/TestApp's hand-authored mappings: all three documents are now IDENTICAL after describe -> exec, and the executor reports Unchanged. Controls: reverting the carry reports Modified and loses the samples; reverting the JSON comparison fails the formatting cases. Closes #379 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(proposal): shard the project brain by module, home it at docs/brain/
CI's build-and-test failed on PR #380: --- FAIL: TestMxCheck_DoctypeScripts/40-message-definition-examples.mdl/legacy Execution error: failed to create message definition collection: creating a message definition collection requires the modelsdk engine The doctype round trip runs every example through exec + mx check on BOTH engines, and message definition authoring is modelsdk-only by design — the legacy writer has no serializer for the document, and building one would duplicate a shape the codec already gets right, including a typed-array marker of 2 and an empty-but-present Children list, neither of which is the codec's default. engineSkipList is the established mechanism for exactly this; menus, rules, layouts, navigation profiles and languages all have an entry for the same reason. This adds the sixth. Worth recording why local verification missed it: the test is behind //go:build integration, so `go test ./...` never compiles it. `make vet` now vets the integration tag set (from the #319 fix), which type-checks but does not RUN anything. Running it needs mxbuild, which is available locally — `MX_BINARY=~/.mxcli/mxbuild/11.13.0/modeler/mx go test -tags integration ./mdl/executor/ -run TestMxCheck_DoctypeScripts/<file>` — and that is what verified this fix: legacy SKIPs with the reason, modelsdk PASSes at 0 errors. Refs #272 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rding A8/A9 changed the storage layout, and the proposal still deferred the CLI surface and the skill to the brief with "stands as written". Six of the seven verbs no longer do, and a reader of the proposal cannot see the brief — so both are restated in full (§4.3, §4.4). What sharding changes: `init` creates `docs/brain/` with `README.md`, `project.md` and an empty `modules/`, and refuses a `docs/brain/` it did not write; `staged` shows the shard each queued entry would land in, so routing is visible before it happens; `promote` derives the destination from the first anchor's module, with `--to project` as the escape hatch; `drop` deletes a shard that becomes empty, so the directory does not accumulate husks reading as "this module has decisions"; `check` gains `--changed`; `show` reports per-shard headroom, computed (A6). What it deliberately does not change: `capture` stays unsharded. Staging is a queue, not a store, and routing it would force the decision before a human has looked at the entry. Two rules the table compresses. The cap is per shard and `promote` is where it bites, with `project.md` the tightest because it is the only file loaded unconditionally. And misfiling is a check with one deliberate relaxation: at least one anchor must resolve in the entry's own shard, while anchors into other modules are reported rather than failed — "Sales.Order is committed by Finance.ACT_Post" is genuinely two-module, and forcing it into project.md would grow the file that must stay small. It is a second axis, not a fourth anchor state: an anchor can resolve perfectly and still sit in the wrong shard. A7's host is settled as `mxcli lint`, not `mxcli check`. `check` is scoped to an MDL script, so a project-level staleness line has no business in its output; `lint` already takes -p, already runs in review, and is already where A3's generated rules surface. The skill gets four instructions sharding makes necessary, the first being the one that matters: read project.md plus the shards for the modules you are about to touch, and never the whole directory — an agent that reads every shard reinstates exactly the cost the sharding removed. The illustrated frontmatter was checked as real YAML (the folded description round-trips). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements phase 1 of PROPOSAL_project_brain.md: storage, anchors, the seven verbs and the skill. Opt-in — a project without docs/brain/ hears nothing. The governing rule is that anything derivable from the model is answered by a command and never written down. A note that transcribes the model disagrees with it silently, because nothing checks prose. The store holds only the negative space: why a pattern was chosen here, which marketplace version broke what, what a recurring mxbuild error means in this app. Records shard by anchor scope. An entry's first anchor names its file (@Sales.Order -> modules/Sales.md); an anchorless entry is cross-cutting (project.md). Nothing routes it — the module prefix IS the file name, so there is no index to maintain and none to go stale. That is what makes the size cap per shard instead of a project-wide budget, and what lets a session load project.md plus the modules it is touching. check answers two independent questions, and conflating them was the trap. Each anchor is resolved / not found / not indexable, and only the middle one fails: the catalog's objects view covers the describable types, so a scheduled event would otherwise read as missing and check would demand edits to entries that are perfectly current. FindDocumentUnit separates the two — it cannot miss a kind, because it never asks what kind anything is. Misfiling is a SECOND AXIS, not a fourth state: every anchor can resolve and the entry still be in the wrong file. Measured live on a real project: 3 of 3 anchors resolved and the misfiled entry was still caught. It is decided only when something resolved — judging it on an all-not-indexable entry put the same false staleness back through the other axis, which a table test caught and a stubbed guard confirmed as the cause. The rule is deliberately relaxed to one matching anchor, so a genuinely two-module fact keeps its home shard. capture is deliberately NOT sharded. Staging is a queue, not a store, and routing it would force the file decision before a human has looked at the entry — the decision promotion exists to make. The queue is content-addressed, so capturing the same fact twice is refused for free. mxcli lint prints the unpromoted-queue count: a report only brain check prints is a report nothing demands, which is how the analogous digest went three months without a run. Silent with no store and no queue, on stderr so it cannot corrupt --format json/sarif. Sizes are computed by brain show on every call and are written into no committed file, the store's own README included. Dropping the last entry from a module shard removes the file, so the directory does not accumulate husks that read as "this module has decisions". Controls run in both directions for each guard: the cap refusal (and a write that fits), a foreign docs/brain/ refused (and mxcli's own re-init accepted), init not clobbering entries, --changed selecting one shard (and none on a clean tree), and the lint notice appearing and disappearing with the queue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ettled All four slices shipped (#245, #250) while the proposal still read `draft`, so the one artifact that says what is built was the one saying it was not. Records what landed beyond the plan, each because running the feature exposed the need: the enabled-language statements, the out-of-scope report (ledger #137 — `in Ledger` never reaches the project-level NAVIGATION, so the sidebar stayed English under a message that read as success), the removal form, the lint rule and skill. `--untranslated` is the one planned item still unbuilt, and it is a cost optimisation rather than a gap. Two open questions are answered by measurement rather than deleted: 2. An unenabled language's translations ARE stored and kept — a stock 11.13 app enables one language while carrying nine — but the app does not serve them, so `create translations` warns and names `ALTER SETTINGS ADD LANGUAGE` instead of enabling or refusing. 5. Studio Pro does not care about `Items` order, unlike widget PropertyTypes. What actually bit was the `$ID` form, which is a different question and now has its own bug test. Homographs, catalog coverage, and the two default-language-less texts stay open, with what has been learned since attached to each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
…model The brain held decisions only, so requirements, slicing and roadmap had nowhere to live. That is a real gap: when the source of truth is a Word document, a Figma file or a conversation, it leaves no trace in git — not an issue, not a commit message — and hours of work end with nothing recording what they were for. Measured before designing, because it settles the shape in one command: recorded as an ordinary entry, a single not-yet-built requirement takes `brain check` to exit 1. The anchor syntax is identical but its MEANING is inverted. A decision's anchor points backward at what exists, so one that stops resolving means the decision is stale. A requirement's points forward at what is intended, so one that does not resolve means not built yet — the normal state of a requirement for most of its life. So requirements are a second kind, in plan/<slice>.md, and that inversion is what pays for the feature rather than merely accommodating it: a requirement is BUILT when its anchors resolve, so `brain plan` reports progress derived from the model. Measured end to end — a slice at 0 built / 1 planned went to 1 / 0 after creating the microflow its requirement names, with the plan file untouched. There is no status column to maintain and none that can be wrong. Consequences, each deliberate: - Requirements never fail a check, and slice progress is reported instead. NotIndexable counts as built, not planned: the thing exists, and calling it outstanding would report finished work as undone. - Misfiling is not checked for slices. A slice spans modules by design. - A requirement with no anchor is counted apart as unanchored rather than silently called planned — it cannot be measured yet. - The kind comes from the file, never from a second copy inside the entry, so there is nothing to drift. - A requirement's id folds in its slice, so the same sentence can legitimately be a requirement of two slices. - Slices sort by name, so a numeric prefix is how a roadmap gets its order — the user's choice, not a field mxcli maintains. - A slice's cap is generous but real: a slice too long to read is a slice that should be split, so here the cap enforces the slicing discipline rather than just bounding context cost. Also fixes a bug found by hand, not by the suite: --changed mapped a path to a shard by basename, so docs/brain/plan/01-accounts.md became "01-accounts" and every edited slice was invisible to it. The existing test had only ever used module shards, which is exactly the blind spot; it now covers a plan slice, and reverting the fix reproduces the miss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…em by default
The bootstrap interview asked what the app is for and what it tracks, but never
for the requirements themselves — so a spec that lives in a Word document, a
prototype or a chat window stayed there. Nothing in the repo then said what was
being built towards, which is precisely the context an idle-reaped session has
lost.
Adds Q8 ("Do you have requirements to work from?"), defaulting to yes, and a
provisioning step that runs `brain init` and records the requirements as slices
before any building starts. Opting out is one word at the interview.
Two things the step is explicit about, because both are easy to get wrong:
- Anchor each requirement at what WILL implement it. The anchor points forward,
so naming something that does not exist yet is correct here — and it is what
makes `brain plan` a progress report rather than a checklist. The model
proposal step now says to name elements the way the plan's anchors do, so the
count starts moving the moment work lands.
- Never write a status or a tick-box beside a requirement. Whether it is built
is computed from the model; a hand-kept one is wrong as soon as anyone builds
anything, and nothing will say so.
README.md stays the brief — one page, for a human landing on the repo.
docs/brain/plan/ is the scope — anchored, countable, appended to as
requirements emerge mid-build, which they do.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…kinds
SHOW LANGUAGES listed 8 of a project's 9 languages, and `search 'Opslaan'`
returned nothing for a caption `describe translations` had just listed.
CATALOG.strings was filled by per-type extractors written by hand — page
titles, enum captions, three microflow message templates — so a text
anywhere else was never indexed. Measured on testdata/expr-checker:
indexed actually in the project
translatable texts ~69 3265
languages 8 9 <- ar_DZ missing
en_US translations 66 1045
extraction sites 5 17
A language present only on an unindexed site is INVISIBLE rather than
undercounted, so it is missing from SHOW LANGUAGES entirely — and from
lint rule QUAL005, which discovers its language set from the same table.
The fix is not more cases. A sixth site cost a sixth hand-written case,
which is how five was ever the number. Rows now come from the
type-agnostic Texts$Text walk that DESCRIBE TRANSLATIONS already uses, so
the two subsystems cannot disagree about what the project contains; the
typed path keeps only the strings that are not translatable at all (URLs,
log node names, REST paths, documentation, and the Microflows$StringTemplate
a workflow name is stored in — a plain Text, not a Texts$Text).
StringContext now names the site (Forms$ActionButton.Caption rather than
page_title) and ObjectType is derived from the unit $Type mechanically, so
a document type Mendix adds later is named correctly with no list to
maintain. Nothing queried the old vocabulary outside test fixtures.
Atlas design templates are ~70% of the corpus and never render in a running
app, but they are indexed rather than excluded: CREATE TRANSLATIONS writes
them, and a SHOW LANGUAGES that excluded them would reopen the same split
this closes. ObjectType is how a consumer filters them out.
Same project after: 1496 rows, 9 languages, en_US 1045 / nl_NL 333 /
ar_DZ 4 — identical to an independent BSON walk of the units.
Control: stubbing the walk gives `strings: 3` and a SHOW LANGUAGES that
reports nothing, so "9 languages" is not equally consistent with a build
that never had the fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
The rule grouped by (QualifiedName, StringContext) while ElementId sat unused in the table. Sibling elements of one type share both — an enumeration's twelve values, a page's action buttons — so they became one group, and translating a single value made the whole set look complete. Eleven real gaps went unreported. Grouping now includes ElementId, which the strings index already carried. No test caught this because the test harness synthesized ElementId from QualifiedName+StringContext, giving every sibling the same value and reproducing the defect in the fixture. The new tests set it explicitly. Control: with every sibling translated the run stays at 0 violations, so the violation in the failing case is the missing translation and not an artifact of splitting the group. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
A `call external action` was written with neither the result variable's
type nor its parameters' types, so Mendix reported CE7269 ("the return
type for remote action '<x>' has changed") and CE7252 ("the parameters
... have changed"), and re-running CREATE OR MODIFY EXTERNAL ENTITIES
never cleared them.
It never could. Both codes are defined on CallExternalAction.cs
(extracted from Mendix.Modeler.Texts.dll, 11.13): they are raised by the
microflow ACTIVITY, not by the entity. That is what made the reported
remedy the wrong lever, and it cost the reporter a debugging session.
Two omissions of the same shape, each a DataTypes$ sub-document that was
never written:
- The return-type resolver mapped only EDM primitives and returned ""
for anything else, so an action returning an entity (or a collection
of them) got no VariableDataType at all. It now resolves to
DataTypes$ObjectType / DataTypes$ListType naming the external entity
imported for that type -- the same linkage the entity import writes.
- ExternalActionParameterMapping.ParameterType was never written,
though generated/metamodel declares it WITHOUT omitempty. Measured: a
call with ANY parameter, of any type, produced CE7252 plus one CE0117
"Error(s) in expression" per argument, because an argument cannot be
type-checked against an untyped parameter.
Measured on 11.13 against a contract with three action shapes: before, a
no-parameter entity return was CE7269, a one-string-parameter call was
CE7252 + 1x CE0117, and a two-parameter call CE7252 + 2x CE0117; after,
all three build at 0 errors. Reverting the return resolver reproduces
CE7269 verbatim.
`check --references` now resolves the call against the cached contract
too, so an unknown action, an undeclared argument, a missing parameter,
or an entity return whose entity has not been imported are reported with
the statement that fixes them.
Also: the catalog listed only entities stored as
Rest$ODataRemoteEntitySource, skipping every Rest$ODataEntityTypeSource
-- what CREATE EXTERNAL ENTITIES writes for any type the contract gives
no entity set, including an action's parameter and return types. The
consequence was worse than the under-count: contract_entities.
UsedByExternalEntity is filled by joining that table on RemoteName, so
for exactly those entities the column was structurally always empty and
read as "linked to nothing" whether or not the import had worked. That
column is the evidence the report was diagnosed from.
Fixes mendixlabs#1020
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
fix(odata): type an external action's return value and its parameters
docs(proposal): restate the brain's CLI surface and skill against sharding
fix(mappings): carry OriginalValue and the snippet formatting through a rewrite
feat(brain): project knowledge store — decisions, requirements and slices
Conflict was CHANGELOG.md only: both sides appended independent entries under [Unreleased]. Kept all nine — main's seven and this branch's two. Checked the findings shards for the union-merge trap this repo has hit twice (merge=union never deletes, so a record main removed comes back): every shard matches main exactly except mdl-other.jsonl at +2, which is this branch's own two records. Nothing resurrected. main's 19 commits touch mdl/catalog/ (builder_contract.go, builder_external.go) but none of the files changed here, and the grammar moved again so the parser was regenerated. Re-verified on the merged tree: go test ./..., make lint, make check-findings green, and the end-to-end measurement unchanged at 9 languages / en_US 1045 / ar_DZ 4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
feat(catalog): index every translatable string, not five hand-picked kinds
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.
Thirty-nine commits since the last sync. Two themes dominate: a new project
knowledge store, and a run of fixes to things that a rewrite silently
deleted — documentation, mapping samples, a SOAP binding.
A rewrite no longer deletes what the statement did not restate
Four separate cases, all the same shape:
create or replacerebuilds a documentfrom the statement, so anything the statement does not mention is gone. None of
them is a build error, which is why they survived.
create or modifysetDocumentationfrom thestatement unconditionally, so a statement with no
/** … */wrote the zerovalue over the stored one. Not a drop — an empty overwrite. Absent and
empty are different facts and only the parser knows which, so
findDocCommentTextnow distinguishes them. Carried across a full pass ofall 29 doctypes.
OriginalValue— the sample parsed from the JSON structure'ssnippet. The old behaviour rested on a measurement over two mappings a blank
app ships; at corpus scale 2,322 of 3,042 elements carry one. But neither
default is right: the split is per document (145 mappings all, 107 none, 2
mixed), so a rewrite carries the stored value forward instead of choosing. The
original decision stands for newly authored mappings.
not three; the fourth was absent from the semantic type, so every rewrite
destroyed it (
CE6896+CE0270). Now refused rather than rewritten —carrying it would claim the rest of the document survives too, which mxcli
cannot check.
describepretty-prints, sodescribe→execreformatted a one-line snippet. Now preserved when the JSONis semantically equal.
Message definitions are authorable
The last of a mapping's four schema sources that a script could create — 74 of
327 mappings in the demo corpus (22.6%). Unlike an XML schema or a WSDL it holds
nothing external: it is a selection over the domain model.