Conversation
…sed out Triggered by the irreducible-graph work landing (named join points, then Mode 2 and Mode 3 describe). Sources gained the two new describe-side files, the graph classifier and the proposal; the existing three kept. Three changes, grounded in the 13 mdl/executor findings since the last sync. A FIFTH SHAPE at the end of the survival ordering: "means something else". The other four all leave evidence — output that will not parse, a missing property, an invented clause, vanished structure. This one parses, executes, yields a valid model, drops nothing and invents nothing, and denotes a different program. No automated signal can see it; only comparing behaviour finds it. SOME DOCUMENTS CANNOT BE DESCRIBED FAITHFULLY AT ALL, which is a vocabulary limit rather than a careless describer: MDL's `if` is single-entry/single-exit against an arbitrary digraph, so the remedy is to extend the language, and per Boehm-Jacopini one sub-class has no faithful rendering at any effort. The beat earns its place by telling a reader whether to reach for a fix or a proposal. VERIFICATION rewritten around the trap that let shape five survive: a round trip that is a fixed point on the corpus you have is not evidence it is faithful, because the describer's inference rule was reverse-engineered from that corpus and agrees with it by construction — the disagreeing case has to be constructed. Plus three measurement rules that each hid a defect until applied: compare identities rather than counts; diff the whole corpus against a baseline binary (a refactor must be byte-identical, a feature must move only its targets); algebra is not behaviour. Also removes the quoted "83 of 248" count — the area has since more than doubled to 615, so the figure was simply wrong. The Do-not-duplicate line now says counts are computed, per CLAUDE.md's "coverage is computed, never quoted". Separately, three findings written 2026-09-13/14 used the shard filename (mdl-executor) as their `area` where the other 613 use the path (mdl/executor), so digest-status bucketed them into "(32 areas < 5)". Corrected in place before using those counts to choose this run's scope. This is the THIRD occurrence — the same correction was made on 2026-09-12 — and the repetition is the tell that correcting is not fixing: check-findings validates that the field exists, not that its value is in the vocabulary. Two SYNC_LOG rows appended, one for the correction and one for the page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
The 2026-06-05 adoption plan stops at the cutover and still reads as though legacy were the default. This covers what it deferred to "Phase 5 — Cleanup" and never specified. The plan's whole job is refusing to treat one name as one job. Retiring the legacy ENGINE (mdl/backend/mpr, 2,808 lines, five production importers) is small, unblocked and reversible. Retiring the legacy SERIALIZER underneath it (sdk/mpr, 41,243 lines) is fifteen times larger and gated on consumers that never touched the engine seam — the public api/ package, the MCP backend, eight cmd/mxcli commands. One measurement reorders the sequence. The earlier plan deferred the mongo-driver v1->v2 migration "to the cutover", reasoning the engines could coexist on different module paths. They can, but the split does not fall on the engine flag: modelsdk/ is 117 files v2 and 0 v1; sdk/mpr is 113 v1 and 0 v2; the adapter between them straddles both. So deleting the legacy backend moves the driver migration NOT AT ALL — what unblocks v2 is deleting sdk/mpr, which is behind a product decision about the public API. Sequenced the other way, phase one looks like it owes a 41k-line migration and never starts. Everything in the "already true" section was verified rather than inherited: no "not supported by the modelsdk engine" message remains; errUnimplemented cannot fire on a default run (17 of 19 methods measured dead, the 2 live ones implemented, the set pinned by a passing test); the dependency runs one way (modelsdk does not import the legacy backend — a grep says it does and is matching a comment); legacy is strictly weaker. Also records what goes stale on removal, including that the modelsdk package doc still describes the engine as a read slice whose write methods are unimplemented and whose CLI prints a read-only warning — three false clauses, the warning having been gone for some time. Phases 3 and 4 are deliberately NOT scheduled: they need an answer about whether anything outside this repository depends on api/, which is a maintainer question rather than a measurement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Two of the workflow constructs only Studio Pro could set, which #466 made rewrites refuse rather than reset (phase 1 of PROPOSAL_workflow_studio_pro_constructs.md): user task … on created microflow Mod.MF create workflow … on workflow events (UserTaskStarted, …) microflow Mod.MF as '…' create workflow … on any workflow event microflow Mod.MF as '…' Both engines write and read them in the shape ako/TestApp (11.14.0) stores, describe emits them, and a rewrite that restates them proceeds; one that declares fewer than are stored is refused. Measured on mxbuild 11.13.0 first: on-created takes exactly (WorkflowUserTask, context) in either order (CE6683) and returns nothing (CE5012); a handler takes exactly (WorkflowEvent, WorkflowRecord, WorkflowActivityRecord) in any order (CE6691); and the build does NOT check event type names — an invented one builds at 0 errors — so mxcli refuses an unknown name (MDL-WF12) and one measured absent from the project version. Studio Pro stores the full list for "every event", so `any` expands per version from a table measured off the 11.6/11.10/11.13/11.14 mxbuilds (32/36/42/42), and is refused below 11.6. Fixes found on the way: - legacy parser read OnCreatedEvent as a string, so every on-created microflow read back as none; - none of the workflow rewrite guards worked on the legacy engine: its raw units carry arrays as primitive.A, which the guards' []any switches never matched, so a rewrite the modelsdk engine refused deleted stored state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(workflow): author event handlers and on-created microflows
…er problem Two questions from review, both of which the first draft got wrong by reasoning from line counts instead of measuring callers. MCP STAYS, and that is compatible. Its entire sdk/mpr dependency is ONE call — mpr.Open(path) for a deliberately read-only reader, since writes already go over MCP to Studio Pro. The naive port looks impossible and the reason is worth recording: MCP calls 37 methods on that reader and modelsdk/mpr.Reader has 7. But that is the wrong comparison — modelsdk/mpr is a unit/raw reader and the semantic decoding lives a layer up. Against the modelsdk BACKEND, 33 of 37 are already implemented. MCP composes the codec backend for reads; its 15,619 lines are MCP protocol surface and are untouched. api/ IS NOT DEPENDENT ON THE LEGACY BACKEND. It imports mdl/backend zero times — it bypasses the abstraction entirely and holds a concrete *mpr.Writer. The dependency is five symbols and seventeen methods, and FIFTEEN of the seventeen are already implemented on the modelsdk backend, with all seventeen already declared on FullBackend. The two missing (AddAttribute, UpdateAttribute) are missing precisely BECAUSE api/ is their only caller, and ALTER ENTITY's mutator is the template for both. The builder surface need not change, so the public API break the first draft treated as a gate is not required. The unifying finding, and the reason Phase 3 is now sequenced rather than deferred: unreachableUnimplemented is a CENSUS OF WHO BYPASSES THE ABSTRACTION, not dead weight. Its reason column names api/, the MCP backend, and the cmd/mxcli commands that hold a concrete reader on purpose. Those methods are on the interface because those callers exist and unreachable because those callers do not use a backend value. Closing the bypasses makes them reachable or genuinely deletable — and only after that is sdk/mpr's fate a question at all. Phase 3 accordingly drops from "L, gated on a product decision" to "M", the a/b/c options are replaced by a four-step sequence, and MCP is recorded as a fixed constraint rather than a decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Merging main moved three of the numbers the plan asserts: sdk/mpr 41,243 -> 41,418 lines and 113 -> 114 v1 files, mdl/executor 17 -> 18. The caller measurements that carry the argument are unchanged — api/ still imports mdl/backend zero times and needs 17 methods of which 15 are implemented, MCP still needs 33 of 37, the reachability test still passes, and there are still no "not supported by the modelsdk engine" messages. A plan whose figures rot is the specific failure this one was written to correct in its predecessor, so the verification line is now dated to the merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Phase 2 of PROPOSAL_workflow_studio_pro_constructs.md. An AI agent task
(Workflows$AIAgentTaskActivity, Mendix 11.9+) stores exactly the
call-microflow shape under a different $Type (ako/TestApp, 11.14.0), so it is
the call-microflow statement with `agent` added and, in the semantic model, a
CallMicroflowTask with IsAgent — every walker, validator, catalog edge, naming
rule and ALTER path applies unchanged; only $Type, describe and the rewrite
guard branch on it.
call agent microflow HR.Classify as aiAgentTask1 comment 'Classify'
with (Request = '$WorkflowContext')
outcomes true -> { … } false -> { };
Measured on mxbuild 11.13.0 by writing each shape as a call microflow and
switching only the $Type: everything builds the same (context, System.Workflow
or several parameters; Boolean and enumeration outcomes; boundary events)
except an agent microflow with no parameters, which fails CE1590 — the one
new check. Gated on workflows.ai_agent_task (11.9.0) for CREATE and ALTER.
Describe used to print an agent task as a comment, so a rewrite refused the
whole workflow; it now emits `call agent microflow` and the guard counts
restated agent tasks. The modelsdk engine writes and reads it; the retiring
legacy engine carries the $Type through rather than downgrading the task; the
MCP backend sends it as AIAgentTaskActivity — the shape Studio Pro 11.14's
ped_get_schema declares.
Verified in Studio Pro 11.14 over MCP: the agent task, handler and on-created
shapes are accepted and read back as written; the on-created and handler
signature errors match mxbuild's CE6683/CE6691 text; an agent task without
parameter mappings is flagged; an invented event type is refused at create.
Fixed on the way:
- MCP mapper hard-coded onCreatedEvent NoEvent and sent no onWorkflowEvent,
silently dropping phase 1 over MCP; and Studio Pro 11.14's workflow
constructor takes `context` and a string workflowName, so every workflow
create over MCP failed. The shape is now read off the live constructor
schema.
- describe printed a call microflow's caption only as a `--` comment, so
describe -> exec replaced it with the microflow's name; an authored caption
is now `comment '…'`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
api/ imported mdl/backend ZERO times. It was not "dependent on the legacy backend" — it bypassed the abstraction entirely, holding a concrete *mpr.Writer handed to api.New. That is why AddAttribute and UpdateAttribute sat unimplemented on the codec engine: api/ was their only caller and it did not call them through a backend value. api.New now takes a backend.FullBackend, with an api.Open convenience for the common case. The builder surface is untouched, so this is not a breaking change to anything but the constructor — and nothing in the repository imports the package, so the blast radius in-tree is its own tests. The gain is not tidiness: the same fluent builders now run against any backend, including a live Studio Pro over MCP, which was unreachable while the package held a concrete writer. Open owns the connection it makes; a backend passed to New belongs to the caller and Close leaves it alone. AddAttribute and UpdateAttribute are implemented on the codec backend and struck off unreachableUnimplemented, which failed loudly when they became reachable — exactly what that pin is for. Two properties are worth the tests they get: the attribute keeps its stored $ID (the runtime keys on identity), and the list keeps its order. The second is not free — the generated list offers only Append and Remove, so an in-place replace means rebuilding it, and the naive remove-then-add moves the edited attribute to the bottom of the entity. Control: breaking the rebuild fails the order test. THE SUITE THAT SHOULD HAVE VERIFIED ALL THIS HAD ONLY EVER SKIPPED. api/'s ten integration tests pointed at ../mx-test-projects/test-source-app, which is not in the repository, so every one of them skipped on every machine and in CI since they were written — the mendixlabs#808 shape, and it meant `go test ./api/` passing said nothing about a refactor that rewrote every call site in the package. They now run against the committed testdata/expr-checker fixture the codec backend's own tests use, and a missing fixture is FATAL rather than a skip, because a committed fixture's absence is a broken checkout rather than an optional dependency. Ten tests, zero skips, all passing against the codec engine. First step of Phase 3 of docs/plans/2026-09-14-retire-legacy-engine.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
MCP opens the project read-only and sends writes to Studio Pro, so it needs a semantic reader — and it held a concrete *mpr.Reader for one, because the codec backend did not offer three of the reads it takes: GetDomainModelByID, GetWorkflow and ListNavigationDocuments. That is the entire reason those three sat on FullBackend with no caller through a backend value; the reachability pin names MCP for each. Implements them in mdl/backend/modelsdk/mcp_bypass_reads.go. Each is a re-keying or a widening of a read this package already does, so all three share their conversion (domainModelFromGen, workflowFromGen, navProfileFromGen) with their siblings — workflowFromGen is extracted from ListWorkflows for this, so a single-item read cannot drift from the same item in a listing. Adds Backend.ConnectReadOnly: Connect opens ReadOnly:false and would lock the file Studio Pro owns, which is the one thing MCP must not do. Connect had no test — 190 in the package, not one called it — so the swap would have landed unverified with the suite green. connect_reads_test.go covers it, with two controls: reverting ConnectReadOnly makes the write-refusal test fail with the reported symptom, and a companion test shows the same write succeeds read-write, so the refusal is not incidental. The workflow read seeds a workflow first; the fixture has none, so looping over the listing would have passed without calling GetWorkflow at all. Step 2 of Phase 3 in docs/plans/2026-09-14-retire-legacy-engine.md. Five methods struck off unreachableUnimplemented across this and the api/ port. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
A layout whose placeholder is not named `Main` passed `mxcli check -p
--references` AND `mxcli exec` ("Created layout"), then failed the build
with CE0848 — with no DROP LAYOUT to undo it (mendixlabs#1063).
The rule, measured on Mendix 11.12.1 against a layout NO PAGE USES, so
none of it depends on a page binding to the layout:
placeholder Main -> 0 errors
placeholder Main + Content -> 0 errors (extra names are fine)
placeholder Content -> CE0848 "No placeholder with the name
'Main' found. There should be exactly one."
placeholder Main + Main -> CE0849 + CE0495
placeholder Main + Side + Side -> CE0495 "Duplicate name 'Side'."
So there are two independent halves, and CE0495 shows the second is not
about Main at all: placeholder names must be unique, whatever they are.
The root cause was a documented belief rather than an oversight. CLAUDE.md
said "Which placeholder is 'main' is a naming convention (22 of 22 name one
`Main`; a page binds by qualified name anyway)", the layouts skill said "the
convention is the mechanism", and the write-time guard implemented exactly
that: hasAnyPlaceholder accepted any placeholder under any name. The check
that existed matched the docs, which is why looking for a missing guard would
never have found this — the rationale was what needed verifying.
The rule now lives once, in types.CheckLayoutPlaceholderNames over bare
names, and is applied by both `mxcli check` (MDL081 for the Main rule,
MDL082 for duplicates, in the project-free pass so CI reaches it) and the
writer. Two copies in two currencies is how a resolver drifts.
An error rather than a warning: unlike MDL078 nothing here is a snapshot of
a Mendix asset that a later version might extend, so a flagged layout cannot
build today.
Verified end to end on mxbuild 11.12.1: the reporter's script is now refused
at check time (exit 1) and writes nothing, and `Main` + `Side` builds at 0
errors. Control: stubbing the shared rule to always-OK makes both the
executor and writer tests fail with the reported symptom.
Refs: mendixlabs#1063
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
`placeholder Main { }` inside a CREATE LAYOUT passed `mxcli check`, then
failed at exec with `layout "X" declares no placeholder` — a message that
flatly contradicts a script which plainly says `placeholder Main`. The
failed exec had already created the module (mendixlabs#1063).
One grammar rule serves two opposite jobs, told apart by shape rather than
by context: `if c.LBRACE() == nil` makes a bodiless placeholder a
DECLARATION widget, while the braced form is routed to
buildPagePlaceholdersV3 — the page-side job of FILLING a layout's slot. In
a layout there is no such job, so the braced form was dropped on the floor
and the layout ended up with zero placeholders.
When one parse rule is disambiguated by shape, the wrong shape has no error
path by construction: it silently means the other thing. Reaching for the
braced form here is also the natural mistake, since every other layout
element takes a body and `alter page` uses it for real. DESCRIBE emits the
bodiless form, so round-tripping never produces this and no test covered it.
The Builder now carries `inLayout`, saved and restored around the layout
body build because the same body builder serves pages, and collects the
dropped names into CreateLayoutStmt.BracedPlaceholders for MDL083 to report
at check time. The braced form is still dropped — recording it is a
diagnostic, not a decision to start honouring it.
Verified: the reported script is refused at check time and no longer creates
the module on the way to failing. Controls: a page's braced placeholder must
still fill its slot, and a layout's bodiless form must still produce a real
widget.
Refs: mendixlabs#1063
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Layouts were the only document mxcli could create and alter but not delete (mendixlabs#1063). Every other doctype has a DROP — pages, snippets, microflows, entities, queues, rules, folders — and the gap bit hardest on a layout mxcli itself had just written badly: the reported script created a layout that fails the build, and there was no headless way to remove it. This is wiring, not new capability. DeleteLayout already existed and worked on both engines, and is exercised on every CREATE OR REPLACE LAYOUT, whose replace half is a delete followed by a create. Only the grammar alternative, AST node, visitor branch and executor were missing. Pages still bound to the layout are named in a warning and the drop proceeds, rather than being refused. Three reasons, in order of weight: no other DROP in mxcli refuses on dependents; drop-then-recreate under the same name is how a layout is corrected, with the pages rebinding by qualified name (verified end to end — back to 0 errors); and the order of statements in a script is the author's business. The warning is load-bearing because of what the pages then do. Measured on 11.12.1, dropping a layout one page was bound to: [error] [CE1613] "The selected layout 'MyFirstModule.App_Mine' no longer exists." at Page 'MyFirstModule.Home_Web' [error] [CE1613] "The selected placeholder 'MyFirstModule.App_Mine.Main' no longer exists." at Content for placeholder 'Main' Both name the page end and never the layout that was removed, so an unwarned drop surfaces a build later attributed to a document the author did not touch. The warning names the pages and gives the `alter pages … where layout =` repoint. Control: a page bound to a different layout must not appear in the warning, an unused layout must produce none at all, and removing the warning makes the test fail with a bare "Dropped layout" line. Refs: mendixlabs#1063 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
CLAUDE.md said "Which placeholder is 'main' is a naming convention (22 of 22 name one `Main`; a page binds by qualified name anyway)" and the layouts skill said "the convention is the mechanism". mxbuild validates it as a rule, measured on 11.12.1 against a layout no page uses: CE0848 with no Main, CE0849 with two, CE0495 on any repeated name. That sentence is why the write-time guard accepted any placeholder under any name, so correcting it is part of the fix rather than housekeeping — the code matched the docs exactly, and the docs were wrong. Also documents DROP LAYOUT and the declared-vs-filled placeholder distinction (MDL083) across the same set: CHANGELOG, the layouts skill, MDL_QUICK_REFERENCE, `mxcli syntax` (new layout.drop topic), a bug-test example, and three findings records. Refs: mendixlabs#1063 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Phases 1 and 2 of docs/plans/2026-09-14-retire-legacy-engine.md. The codec engine has been the default since the 2026-06 cutover and nothing routes to legacy any more; it was five files away from having no non-test importers. Deleted: mdl/backend/mpr (the backend), mdl/enginecompare and the engine-diff make target (a package that compares two engines), and the `bson compare` command. Repointed mdl/repl, two examples and the test helpers at the codec backend, and dropped the nightly matrix's engines leg. --engine and MXCLI_ENGINE survive as a warning-only NO-OP rather than being removed. Deleting the flag would fail a script pinning `legacy` at argument parsing with "unknown flag", which says nothing about what changed; it now warns once and runs. Verified both spellings warn and still execute. Three things the plan did not anticipate, each invisible while two engines existed and none caught by the compiler: - errUnimplemented still told users to "rerun with MXCLI_ENGINE=legacy" — a runtime message naming a fallback that no longer exists is worse than none, because the user follows it into a second failure. It now asks for a report, which is what reaching it means. - setupTestEnv defaulted to the LEGACY backend, so most of mdl/executor's integration tests were exercising the retired engine rather than the one users get. They run on the codec engine now — coverage that was always intended and had silently not been happening. - TestODataService_EngineWriteParity compared two writers' key sets. The comparison is vacuous with one engine, so it is dropped and the property it was a means to is kept: a published OData service retains its role grants, which mx check reports as 0 errors either way. Renamed to match. Kept deliberately: the doctype gate's engine matrix, at one entry. Its selection still turns a stale MXCLI_TEST_ENGINES=legacy into a loud failure rather than a gate that selects zero engines and reports success. One test deleted: TestCustomHandlerLegacyRefuses, which asserted legacy's refusal of a construct it could not serialize. The codec engine's positive coverage of that construct is intact. Gates: build, vet (incl. -tags integration), go test ./..., check-mdl (543 pass), check-findings. Executor integration suite green at 562s, down from 642s now that the doctype gate runs once instead of twice; mdl/backend, api, marketplace and repl integration suites green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Retire the legacy engine: delete the backend, port api/ and MCP off it
…ec backend) into feat/workflow-ai-agent-task Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…emoval Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 3 step 4 of docs/plans/2026-09-14-retire-legacy-engine.md: re-run the reachability probe over what was left. Six of the eleven remaining entries were not bypasses awaiting a port — nothing anywhere wanted them. scripts/backend-reachability.sh reports DEAD for "nothing calls this through a backend value", and that one verdict covers three situations wanting opposite fixes: a BYPASS (a caller wants it but holds a concrete reader/writer — port the caller), an ORPHAN (no caller anywhere — delete), and a DUPLICATE (callers exist, but through a narrower package-local interface with a different signature — also delete). The probe cannot separate them; a grep for callers under any type can. Deleted, all measured DEAD: the whole WidgetSerializationBackend interface (SerializeWidget, SerializeClientAction, SerializeDataSource, SerializeWorkflowActivity), GetUnitTypes and UpdateLayout. - SerializeWidget/DataSource are superseded by WidgetBuilderBackend's *ToOpaque pair, whose own comment says "This replaces the direct mpr.SerializeWidget call" — the supersession was known and the old pair never removed. - SerializeClientAction and SerializeWorkflowActivity are reached through pagemutator's and wfmutator's own deps interfaces, which declare them returning bson.D rather than (any, error). The vestigial *Backend copy of SerializeWorkflowActivity carried a comment claiming ALTER WORKFLOW used it; ALTER WORKFLOW uses codecWorkflowDeps. - GetUnitTypes exists only on modelsdk/mpr.Reader; UpdateLayout was superseded by the page mutator when ALTER LAYOUT landed. Both generated stub files regenerated (276 -> 270 methods), mock stubs dropped. Census 11 -> 6, and all six that remain are genuine bypasses. This corrects a claim shipped in the previous PR: the census header and the plan both said the list "shrinks by closing a bypass, never by deleting methods", which holds for a bypass and is wrong for the other two. Both are rewritten to name the three causes, as is the probe script's own header, which still told the reader to fall back to the deleted engine. Gates: build, vet (incl. -tags integration), go test ./..., check-mdl, check-findings. The census pin (TestNoReachableUnimplementedBackendMethods) passes at six entries with its control intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
feat(workflow): AI agent tasks with `call agent microflow`, verified in Studio Pro over MCP
Conflict in CHANGELOG only, and the usual shape: both sides appended under `## [Unreleased]`'s `### Added` and `### Fixed`. Both sides kept — verified zero deletions in the CHANGELOG diff against origin/main. Main's #468 deleted the legacy sdk/mpr BACKEND (mdl/backend/mpr) and the flag selecting it. Nothing in this branch touched it: the writer guard here is on the modelsdk path, and sdk/mpr the package (readers/writers) is untouched, so the merge is content-only. Re-verified on the merged tree — build, full test suite, lint, corpus sweep (0 failures), findings — and the three fixes still fire: MDL081 on the reported script, MDL083 on the braced form, DROP LAYOUT parses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
Five TestConnect_* tests from #468 built the backend with an empty dial address, so defaultDial rewrote 127.0.0.1 to host.docker.internal wherever that name resolves. In a devcontainer the request went to the Docker host instead of the httptest listener and Connect failed with "connection refused"; on CI the name does not resolve, so the suite was green. Route the three constructor calls through backendFor, which passes the listener address explicitly the way fakePED.connectClient does. defaultDial is unchanged — it is correct for a real Studio Pro. Control: with the dial reverted to "" the same five tests fail in the devcontainer with the reported message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…de by, await all users
Phase 3 of PROPOSAL_workflow_studio_pro_constructs.md.
multi user task Vote 'Vote on the request'
page HR.VotePage
participants 80 percent
decide by threshold 60 percent fallback 'Reject'
await all users
outcomes 'Approve' { } 'Reject' { };
Rules: consensus / majority more than half / majority most chosen /
threshold <n> percent|votes, each with fallback '<outcome>'; veto
'<outcome>'; microflow M. Omitted clauses are what a rebuild has always
written, so existing describe output is unchanged.
The shapes come from Studio Pro, not gen: the examples were built in Studio
Pro 11.14 over MCP, saved to ako/TestApp, and the saved document is the
reader's fixture — "more than half" is an Absolute majority, a percentage
threshold Relative, and both outcome pointers hold the outcome's $ID.
Measured on mxbuild 11.13.0: fallback required for consensus/majority/
threshold (CE1866), veto outcome required (CE1867) — MDL-WF13 at check time,
also for a name that is none of the task's outcomes; a decision microflow
returns String (CE5012), parameters free; thresholds and participant counts
are not range-checked by the build, so not by mxcli.
Fixed on the way:
- a workflow rewrite silently reset every multi-user task's participant
count and "await all users" (never guarded); a rewrite not restating them,
or a stored completion rule, is now refused.
- MCP: multi-user tasks could not be created on Studio Pro 11.14 (the
constructor takes a taskPage element); PED's default consensus has no
fallback (CE1866) and its constructor drops a more-than-half majority's
fallback, which is now set by the outcome's $ID afterwards. Verified live:
created, read back, no errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
After the orphans and duplicates were deleted, the six remaining census entries
were all the same kind of caller: cmd/mxcli's bson dump/discover, diag,
extract-templates and examples/read_project — raw-unit debugging and export
commands holding a concrete sdk/mpr reader because raw access is the thing they
exist to provide.
Porting them was considered and declined. The backend interface speaks the
semantic model (ADR-0005); routing a BSON dumper through it would either widen
that interface with raw accessors, undoing the decision, or make the tools worse
at their only job. The framing this plan shipped ("the list shrinks by closing a
bypass") did not offer that option — a second way it was too narrow, alongside
the orphan/duplicate correction: some bypasses are correct.
unreachableUnimplemented becomes a standing record rather than a to-do list, and
its header now says so. It keeps its value as a tripwire: a NEW entry still
means either a new bypass appeared or a method was added that nothing calls, and
the two want opposite fixes, so the cause has to be established rather than a
row added to silence the failure.
sdk/mpr does not go away with this — those six commands still import it, as does
the workflow serializer #469 extended. What Phase 3 delivered is that no engine
path reaches it: the executor, the backends and api/ are clean. Removing the
serializer is downstream of Phase 4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
The plan's Phase 4 opens "with [sdk/mpr] gone" — and Phase 3 closed by keeping it, so its premise did not survive. Measuring it properly shows two migrations that do not gate each other, and the one worth doing first is not the driver migration. Also corrects §7.4, which said "those six commands still import sdk/mpr". That was wrong by a factor of four and pointed at the wrong files: the census tracks FullBackend METHODS with no caller through a backend value, not importers. 28 non-test files import sdk/mpr, including 7 in cmd/mxcli/docker, two mdl/executor validators, and modelsdk.go — whose Reader and Writer are type ALIASES to sdk/mpr's, so the published library's root API is the legacy serializer. 4a (port the 28 callers) is the half that pays: 23 of 28 use only mpr.Open, so the dependency is one read-only constructor rather than the serializer's bulk, and the port is the MCP move repeated — modelsdkbackend.New() + ConnectReadOnly. The 140-vs-47 reader method comparison that makes it look impossible is the same wrong comparison the MCP port already disproved: the semantic methods live on the codec backend, a layer up from modelsdk/mpr's raw reader. Its real gate is modelsdk.go's public API, which should be decided before 4a starts rather than during. 4b (convert mdl/'s mutator layer to v2) is independent: those 19 non-test v1 files are v1 because pagemutator/wfmutator/widgetobj declare their deps interfaces in v1 bson.D, not because they bridge to sdk/mpr, so they do not shrink when it goes — which is what §4 assumed. Measured: 249 v1 files, 174 v2, and only 6 importing both — all in mdl/backend/modelsdk, where the crossing is safe because the interchange is bytes and BSON bytes carry no driver version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
fix(layout): enforce mxbuild's placeholder rule, and add DROP LAYOUT
modelsdk.go — the published library's root API — had Reader and Writer as type ALIASES to sdk/mpr, so the documented entry point was the legacy serializer. That was the single biggest gate on deleting it. Open and OpenForWriting now return a backend.FullBackend from the codec engine. Repointed all 14 root-package importers: cmd/mxcli/marketplace (7 files), cmd/mxcli (4), and both examples. Close becomes Disconnect; the two places typing *modelsdk.Reader/Writer take backend.FullBackend. Two methods were needed to do it without a bypass: - ExportJSON on the codec backend. Pure fan-out over the backend's own List* methods, so it cannot drift from what every other reader reports. Its only caller was examples/read_project holding a concrete sdk/mpr reader, which is why it sat on the census — the probe now says LIVE, so it is struck off by closing the bypass rather than by deleting the method. Census 6 -> 5. - AddRawUnit on RawUnitBackend. The marketplace transplant copies a module's units between projects verbatim, and copying verbatim is the point: decoding and re-encoding would mint fresh identities, and an entity's GUID is what the runtime keys the database on. A behaviour difference found by RUNNING the example, not by any test: CreateEntity and CreateAssociation left the caller's element holding an EMPTY id, so the next call failed with "entity not found: " — note the blank after the colon. The legacy writer populated these. assignID only fills an empty id, so api/ was unaffected (its builders pre-assign one), which is exactly why the green suite never saw it. copyAssignedIDs writes the minted ids back, matching attributes BY NAME rather than by index: entityToGen can add, skip or reorder (an audit pseudo-type becomes a generalization, not an attribute), so an index-based copy would hand the caller another attribute's identity — which looks like it worked and is worse than the empty id. Both examples run end to end against a fixture and their writes verified in the model: 6, 3 and 2 attributes land where every one of them used to error. Gates: build, vet (incl. -tags integration), go test ./..., check-mdl (543), check-findings. The ID fix has a revert control that fails with the reported symptom, and a companion test that a caller-assigned id survives — without it the test would pass against an implementation that overwrites every id and breaks api/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
Two check-time validators imported sdk/mpr directly, which CLAUDE.md's backend-abstraction checklist forbids for the executor. Importers 15 -> 13. They open their own short-lived read-only connection rather than using ctx.Backend, because ValidateProgram takes a project PATH, not a backend — `check --references` validates a script against a project it never connects an executor to. Threading one down would change a public signature and every caller for no gain. openProjectForValidation is now the package's one way to do it. The verification problem is a third distinct shape, after the read port and the write port. Both validators FAIL OPEN: an unreadable project returns nil and silences the rule, which is right — a check should not fail on something it could not inspect — and it makes a broken reader silent. The rule stops firing, which is indistinguishable from a project the rule does not apply to. Coverage showed the risk was real: offlineProfilesIn, projectEntityFacts and openProjectForValidation were all at 0.0%, because the existing tests exercised only the pure helpers or passed an empty path. Now 78%, 72% and 100%. For a fail-open path the test must assert the rule FIRES on a project that should trigger it; asserting it stays quiet proves nothing, because quiet is also the failure mode. Two setup details decided whether that test was real, and the first attempt got both wrong: the fixture ships only an online navigation profile, so the offline rule is inert on it either way and the test must seed one — and Mendix fixes the legal profile names, so an invented one is refused by the executor. The stock-fixture control runs first, so a reader that invented a profile is caught before the positive assertion. Gates: build, vet (incl. -tags integration), go test ./..., check-mdl (544), check-findings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
TestRoundtripPage_MicroflowButtonWithCurrentObject has been failing on main since #476 — on every branch cut from it, mine included. It is not caused by the sdk/mpr ports in this PR; the full integration suite reproduces it at 648e346 and it is the only failure in the run. It is also not a describe regression. mdlIdent quotes any identifier that does not LEX as a bare identifier, running the real ANTLR lexer, and #476 added `TARGET: T A R G E T;` to MDLLexer.g4 for the notify-workflow target clause. The parameter named Target therefore began lexing as a keyword, and DESCRIBE started emitting "Target": $currentObject. The output became more correct and the exact-substring assertion went stale. The input side did not break: TARGET was added to the non-reserved-keyword rule, so scripts writing `Target:` unquoted still parse — which is why check-mdl's 544 scripts stayed green and only this one output assertion moved. The assertion is now quoting-agnostic, which is what it was always for: the roundtrip is about the mapping surviving, not about how the identifier is spelled. Controlled by renaming the expected parameter to one that is absent — it still fails, so it continues to detect a dropped mapping rather than passing on anything. Adding a token to the lexer silently reformats DESCRIBE output for every element whose name matches it, and the grammar change and the broken test sit in different packages with no compile-time link between them. Finding recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…ption Every ALTER WORKFLOW op failed with `ambiguous activity "X" (2 matches)` whenever the workflow contained `jump to X`. buildJumpTo defaults a jump's caption to its target's name, and both activity resolvers (MCP mcpWorkflowMutator.searchActivities, MPR wfmutator) matched name OR caption into one pool, so the jump's caption shadowed the target. Measured live over MCP against Studio Pro 11.14. Resolve in two tiers in both backends: name matches win; captions are consulted only when no activity carries that name. Ambiguity and @n are judged within the chosen tier, in the same DESCRIBE traversal order. The jump's default caption is left unchanged: existing projects already carry that shape, and changing it would rewrite every mxcli-authored jump on the next re-run. Tests in both backends fail with the reported message against the pooled resolver (control) and pass with the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rewriting a microflow from its own DESCRIBE output moved every workflow action ~800px right, the first one onto the end event. @position was parsed and dropped: the visitor's setStatementAnnotations and the builder's getStatementAnnotations were hand-written type switches with no case for the eleven workflow statements or import/export mapping and transform json. The setter also had an empty case for EnumSplitStmt (a no-op in Go) and the getter lacked SendRestRequestStmt. Both now delegate to reflection: the existing ast.StatementAnnotations (mendixlabs#884) and a new ast.SetStatementAnnotations, so a statement type with an Annotations field is covered when it is declared. Test parses @position before 17 statement forms and checks the built activity's position; with the fix reverted 17 subtests fail at the builder default while the log control passes. Measured on i956 (11.13): describe -> create or modify -> describe diffed every position before, identical after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ructor
Every CREATE MICROFLOW over MCP failed on 11.14: the Microflows$Microflow
constructor rejects untyped flows and a {type: …} returnType. It also takes
only the canvas now — object constructors declare x/y (plus caption or
loopType), and a create still carrying relativeMiddlePoint, action,
returnValue or a split/loop source is accepted with those silently dropped.
Detect the shape from the live constructor schema (serverInfo.version is
frozen at 1.0.0), send the skeleton in ped_create_document, and set each
object's action, return value, split condition and loop source plus
returnVariableName in one ped_update_document. Older servers keep the
single full create.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…leton-constructor
Measured against Studio Pro 11.14 over MCP, timer boundary events written by mxcli were stored wrongly, while mxbuild 11.13 built all of them: - Neither timer boundary event constructor has firstExecutionTime, so every delay was dropped (CE0126 "Missing value for parameter 'Timer'"). The delay is now set on the stored event after the write. - The interrupting constructor rewrites the path's terminator. A jump outside a parallel split, stored as an End, is now put back. Paths Studio Pro rejects are refused before sending, naming the remedy: an interrupting timer that runs to its end, one inside a split not ending in a jump, and a non-interrupting one ending in a jump. isInsideOfParallelSplit is now sent for interrupting timers. - A single user task's constructor drops its boundary events; timer events are now re-added along with notification events. An add to boundaryEvents does not keep order, so each event is added on its own and located by the persistentId that appeared, and delays are set at the stored position. - ALTER WORKFLOW ... INSERT BOUNDARY EVENT applies the same rules. Refusing up front matters: ped_update_document re-validates the whole document, so one malformed path makes every later update fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… MCP `create or modify workflow` over MCP stored the flow's activities reversed, left an old activity in and dropped a new one, and `alter workflow ... replace activity` changed nothing, while every update reported SUCCESS. One ped_update_document batch is not applied in the order sent. Measured with raw PED calls on Studio Pro 11.14, every batch fits one rule: ops run highest index first, and at one index the adds go in as a block, in op order, before the removes, so a remove where something was just added takes the added element. mxcli sent removes and adds for the same list in one batch, with the adds reversed, index-less or at incrementing indices. A list's new elements now go in at one index in the statement's order, and the elements they replace are removed in a second update. This covers the flow, event sub-processes and event handlers of a rewrite, and ALTER's insert-after and replace. Adding first means a failure between the two updates leaves duplicates rather than a workflow without its activities. The fake PED gains a list simulator that implements the rule and replays every measured batch, so the tests assert the stored order, not the ops. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Port cmd/mxcli/docker and mdl/executor off sdk/mpr (Phase 4a, slices 2–3)
docs(wiki): digest the 2026-09-15 findings into three bug-pattern pages
…osition-roundtrip
main (#476) now refuses `notify workflow` without a `target` (MDL-WF16, CE0166 at build), so the two bare notify lines failed `make check-mdl` on the PR merge ref. A target has to name a notification element of a real workflow, which a standalone repro cannot supply; notify's position is still covered by TestPositionAnnotationPlacesEveryActionStatement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five files — cmd_new, setup, serve, sync_java_deps, cmd_check_post_migration — plus one deliberately refused. Importers 13 -> 8. Every method they call is already on FullBackend; catalog.CatalogReader documents itself as satisfied by it, and executor.NewContainerHierarchyFromBackend already existed. openProjectReadOnly is now this package's one way in, and the two inline ConnectReadOnly sites from the first slice use it too. setup.go had TWO readers, not one — grep found the second where reading the first would have left it behind. cmd_extract_templates.go stays on sdk/mpr on purpose. It calls FindCustomWidgetType, which is unimplemented on the codec backend: measured at runtime it returns "not implemented on the model engine. This should be unreachable." Porting it would have compiled and then failed for anyone extracting a widget template. The type error was the lucky part. RawType/RawObject are bson.D on sdk/mpr and any on types.RawCustomWidgetType, so the port would not build — and the one-line cast that silences that is the only thing between this and a runtime break. When a port hits a type mismatch at a backend boundary, check whether the method is implemented before reconciling the types. Note the direction: the unimplemented method's error says "should be unreachable", and porting a caller to the backend is exactly what makes it reachable. One measurement trap, because it looked like a regression and was not: a baseline diff of `check --post-migration` showed 50 lines disappearing. The first run built and cached a catalog inside the project and the second reused it. Each binary needs its own fresh copy of the fixture — a command that caches into the project directory makes consecutive runs non-independent even when nothing is written on purpose. With that fixed the run is 63 identical lines, catalog build and legacy-widget scan included. That scan prints "No legacy native widgets found" whether the project has none or the reader handed it zero pages, so openProjectReadOnly has tests asserting the reads these commands depend on return real data. Gates: build, vet (incl. -tags integration), go test ./..., check-mdl (544), check-findings. Full repo-wide integration suite: exit 0, no failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
fix: keep @position on workflow and mapping actions across a rewrite
…leton-constructor
fix(mcp): create microflows against Studio Pro 11.14's skeleton constructor
The first version of the name-before-caption fix also scoped @n to the chosen tier. CI caught it: mdl-examples/doctype-tests/24-workflow-examples.mdl addresses a nested call activity as `ACT_Process@2` and failed with "at position 2 not found (found 1 matches)". @n is an established positional index over every activity whose name or caption matches, in DESCRIBE order. Name precedence now applies only to an unqualified reference, which is the only case the jump-caption ambiguity affected. Tests in both backends assert `bugSplitJump@2` still reaches the jump. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(workflow): resolve ALTER WORKFLOW activity refs by name before caption
Brings in #482 (resolve ALTER WORKFLOW activity refs by name before caption), #481, #480 and the page roundtrip test fix that main's CI needed. One conflict, in mcpWorkflowMutator.searchActivities: #482 hoisted the activity's name into a variable on the same lines where this branch records whether the match sits under a parallel split. Resolved to #482's `name` plus this branch's `inSplit` field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TestWFResolve_NameBeatsJumpCaption (#482) inserts a boundary event through a fake PED that answers every boundaryEvents read with null. InsertBoundaryEvent now finds the event it added by the persistentId that appeared, so against that fake it failed with "expected one new element". The fake now keeps the lists, like the other ALTER boundary-event tests; what the test asserts is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(mcp): timer boundary events and workflow list order over MCP
Port the rest of cmd/mxcli off sdk/mpr (Phase 4a, fourth slice)
The refusal from the fourth slice, resolved: cmd/mxcli/cmd_extract_templates.go is now on the backend and NOTHING that ships in the binary imports sdk/mpr. Importers 8 -> 7. The implementation was never missing. modelsdk/mpr.Reader has had FindCustomWidgetType, FindAllCustomWidgetTypes and the collectCustomWidgets walker all along — and its version populates UnitName/WidgetName, which the one I started writing from scratch would have left empty. Only the wiring onto Backend was absent, which is exactly what the old error meant by "this should be unreachable". Grep for an existing implementation before writing one: a method listed in unimplemented_gen.go says nothing about whether the logic exists a layer down. A straight delegation then extracted 0 of 6 templates, reporting for each widget: "[SKIP] Combo box: widget type is bson.D, want bson.D". modelsdk/mpr builds RawType/RawObject with the v2 BSON driver while sdk/mpr and every caller use v1 — unrelated Go types that print under the same name. types.RawCustomWidgetType declares the fields `any`, so neither the compiler nor the error text can tell them apart. An `any` field crossing an engine boundary can carry the right type name and the wrong package; a cast written to silence that panics at runtime. Fixed by converting at the boundary with the package's existing v2ToV1BSON, as widget_pluggable_write.go already does for writes. All six templates extract byte-for-byte identically to the pre-change binary, the 1.2MB datagrid.json included, and reverting the conversion fails the new test with "RawType is bson.D, want v1 bson.D". unimplemented_gen.go still emits the stub after a method is implemented — the generator writes a complete fallback set and Backend's own method shadows it — so the list to update is unreachableUnimplemented in unimplemented_reachability_test.go. It failed loudly on the stale entry, which is how it was caught. Gates: build, vet (incl. -tags integration), go test ./..., check-mdl (547), check-findings, and the full repo-wide integration suite: exit 0, no failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
…n chain The XPath constraint-member check added in 3aa2ee0 rejected the stock Administration.Account_Overview page: the constraint on Administration.Account names "System.UserRoles", which is neither an attribute nor an association of it Administration.Account extends System.User and System.UserRoles is declared from System.User, so it is an association of it by inheritance — and mx check on the rejected page reports 0 errors, which settles it. Every inherited association was a false positive, and separately so was every cross-module one. Because that lands on a Marketplace module almost every app has, check stopped being usable as a gate for anyone whose entities inherit. associationTargetFrom matched the start entity against the association's two ends by exact equality and scanned only dm.Associations. Its comment said the specialisation case was deliberately not chased — "the cost of being wrong is a false error on a working script" — which was sound while its only caller treated false as silence. The new check treated the same false as evidence, so the case the comment declined to chase became the finding. noteQualified did carry a three-valued guard, but on the wrong axis: it asked whether the BASE ENTITY was known, and Administration.Account is known. The resolver is three-valued now, like memberResolution ten lines up: resolved the start entity, or an ancestor of it, is an end missing the module was read and has no association of that name notAnEnd the chain was walked to its root and no link is an end unknown a module, domain model or generalization could not be read The start entity is matched through generalizationChain, a cross-module association is found where it is actually stored (CrossAssociations, far end held by name rather than by element ID), and only missing and notAnEnd are reported. Resolving the hop also types the far end, so steps after an inherited association are now checked at all. Measured against ako/mxcli-sudoku on 11.13.0: the two rejected shapes now pass check and build at 0 errors; a retrieve over an inherited association followed by a bogus member is reported where it was silent before; and an association the entity genuinely lacks — including a specialisation's association named on its generalization — is still reported. Each half of the fix was stubbed in turn and the new tests fail with the reported symptom. Reported in ako/mxcli-sudoku FINDINGS #57. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
Implement FindCustomWidgetType on the codec backend (Phase 4a, fifth slice)
examples/ (5) and scripts/mprsnapshot (2). Importers 7 -> 0, completing Phase 4a of docs/plans/2026-09-14-retire-legacy-engine.md. Three shapes, not one. Four examples held the writer: mpr.NewWriter -> Backend.Connect, Close -> Disconnect, and writer.Reader() dropped because the backend is both halves — CreateEntity, CreateAssociation, DeleteEntity, DeleteAssociation and CreatePage are identical in signature on both. Two held a reader. The rest were the utilities GenerateID and BlobToUUID, and sdk/mpr's copies are already one-line delegations to mdl/types, so pointing the call sites at types is provably the same function rather than a same-named one — the distinction the v1/v2 BSON trap in the previous slice turned on. Verified per shape. mprsnapshot is a canonicalisation tool, so its output is the evidence: identical in all four modes — default (2,870 lines), -refs (12,707), -canon (374), -all (43,979). The write examples cannot be diffed that way, so add_entities was run under both binaries and the resulting projects compared with mprsnapshot: every element path and type matches, and once UUIDs are normalised the only remaining difference is the domain model's content hash, which digests bytes that embed those UUIDs. Fresh identities on new elements are required, so that is the correct result rather than a discrepancy. The count is now guarded. TestNothingImportsTheLegacyEngine parses every .go file's imports and fails naming any file that imports sdk/mpr. Without it the invariant lived in a plan document and a habit, and one import would restore exactly the blind spot Phase 4a existed to close: a caller holding a concrete reader is invisible to the unimplemented-method census. A zero-count invariant needs two positive controls or it passes vacuously forever, because every way of breaking it is silent — a wrong root, an over-broad skip rule and an import-parsing mistake all report "0 importers". So it asserts a plausible number of files was scanned (2,551) and that the detector can see imports at all, by counting one the repo definitely has (mdl/backend, 120 files). Dropping a one-line file importing sdk/mpr into examples/ fails it and names the file. Gates: build, vet (incl. -tags integration), go test ./..., check-mdl (547), check-findings, and the full repo-wide integration suite: exit 0, no failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hifgRSawfaRWXS44YKtSJ
fix(check): resolve constraint associations through the generalization chain
Take the last seven off sdk/mpr — importers reach zero (Phase 4a complete)
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.
Syncs
ako/mxcli:maininto upstream. 39 non-merge commits since #1099,in five themes.
Retiring the legacy engine — complete
sdk/mprwent from 27 importers to 0. The codec (modelsdk) engine is nowthe only local engine;
--engine/MXCLI_ENGINEsurvive as a warning-onlyno-op.
be a bypass problem rather than a serializer problem, and Phase 4 two
independent migrations rather than one
api/routed through the backend abstraction; the MCP backend's local readspointed at the codec
FullBackendsurface the census had mislabelled
cmd/mxcli's readers,cmd/mxcli/docker,mdl/executor, and the rest ofcmd/mxcliFindCustomWidgetTypeimplemented on the codec backend — the last methodforcing a caller to hold a concrete reader
examples/andscripts/mprsnapshotfinish it, with a guard test that failsnaming any file which reintroduces the import
Two findings from that arc are worth the reviewer's attention, because both were
invisible to the compiler and to a green test suite:
reader. It lists methods with no implementation; a caller reaching an
implemented method through
*sdk/mpr.Readernever appears. That hidproject_tree.go's 36 semantic reads andcmd_extract_templates.go'sFindCustomWidgetType. The complete list of bypasses is the importer list,not the census.
anyfield crossing an engine boundary can carry the right type name andthe wrong package.
modelsdk/mprbuilds widget BSON with the v2 driver andevery caller asserts v1, so a mismatch reads
widget type is bson.D, want bson.Dand a cast written to silence it panics. Converted at the boundary.Workflow authoring
events
notify workflow … target, which also fixed notify actions that could notbuild
call agent microflowALTER WORKFLOWresolves activity refs by name before caption;@Ncountsevery name-or-caption match;
@positionsurvives a rewrite on workflow andmapping actions
Layouts
DROP LAYOUT— layouts were the only doctype mxcli could create and alter butnot delete, which bit hardest on a layout mxcli itself had just written badly
be named
Main(CE0848 / CE0849) and names must be unique (CE0495). This filehad documented it as a naming convention, and the write-time guard implemented
that belief — so a bad layout passed
checkandexecand failed a buildlater
MCP / Studio Pro 11.14
ped_update_documentbatch is not applied in the order sent, which stored aflow's activities reversed while every update reported SUCCESS
Fixes, tests and docs
check --referencesresolves constraint associations through thegeneralization chain
TARGETbecoming a lexer token:adding a keyword silently requotes DESCRIBE output for every element whose
name matches it
Verification
Every commit went in green. On the final state:
make build,make vet(including
-tags integration),go test ./...,make check-mdl(547),make check-findings(1,086 records), and the repo-widemake test-integration— exit 0, no failures