diff --git a/.claude/skills/fix-issue/findings/mdl-backend.jsonl b/.claude/skills/fix-issue/findings/mdl-backend.jsonl index 71727447b8..ee9ecbcb90 100644 --- a/.claude/skills/fix-issue/findings/mdl-backend.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-backend.jsonl @@ -95,3 +95,14 @@ {"area": "mdl/backend", "date": "2026-09-12", "symptom": "The modelsdk (default) engine leaves 19 of FullBackend's 276 methods to the errUnimplemented stub, which tells the user to \"rerun with MXCLI_ENGINE=legacy\" — the engine being retired. Nineteen unported methods reads as nineteen reasons legacy has to stay shipped and tested", "cause": "Seventeen of the nineteen cannot be reached at all: they are interface surface that only the MPR backend's own delegation and callers holding a concrete *mpr.Reader / *mpr.Writer (api/, examples/, cmd/mxcli commands that open a reader directly) ever touch, so the stub can never fire. The two that ARE reachable — GetRawUnitByName and ParseMicroflowBSON, four call sites in mdl/executor/cmd_microflows_builder.go — were invisible because each is a fast path with a working slow-path fallback: the stub errored, the code fell through to an O(n) module walk, and the result was identical", "file": "`mdl/backend/modelsdk/raw_lookup.go` (new), `mdl/backend/modelsdk/unimplemented_reachability_test.go` (new), `scripts/backend-reachability.sh` (new)", "insight": "**Grep cannot answer \"is this interface method reachable\" and the compiler can.** `b.reader.GetRawUnitByName(...)` inside the MPR backend and `ctx.Backend.GetRawUnitByName(...)` in the executor are indistinguishable to a regex, and receiver names vary, so a grep-based count said all nineteen had callers. Deleting one method from its interface at a time and rebuilding gives a yes/no per method with no judgement involved: 17 DEAD, 2 LIVE. Slow (one `go build ./...` each, minutes for the set) but decisive — script it, commit it, and pin its OUTPUT in a test rather than re-running it in CI. **A fallback hides an unimplemented method completely.** Nothing was broken and no test failed, because the fast path's failure was indistinguishable from a cache miss; the only symptom was work being done twice. Look for `if x, err := …; err == nil` fast paths when auditing what an engine cannot do. **Measure the speedup before claiming one**: restoring the fast path made no measurable difference (620ms vs 608ms over three runs on a 138-microflow project, within noise), because `mxcli exec` runs `check` first and check's own helpers load ListMicroflows anyway — so the cache the slow path builds is already warm. The value here is the removal of the errUnimplemented cliff, not speed", "refs": []} {"area": "mdl/backend", "date": "2026-09-11", "symptom": "`ALTER PAGE REPLACE`/`INSERT` inside a data view bound `datasource: selection ` re-scopes the new widget's attribute binding to the OUTER data view's entity (**CE1613** \"The selected attribute 'Mod.Outer.Attr' no longer exists\"), and inside a Gallery/DataGrid 2 sourced by a **microflow/nanoflow** drops it entirely (**CE0402** \"No value specified\", `describe` shows `ContentParams: [{1} = ]`). `mxcli check --references` and `exec` both report success; `CREATE PAGE` binds the same widget in the same position correctly", "cause": "The mutator resolved a widget's scope in TWO walks that each knew a different subset of the ten Forms$*Source kinds. `Forms$ListenTargetSource` carries no EntityRef at all \u2014 only the listen target's NAME \u2014 so the entity walk saw no source on the selection data view and left the context at the enclosing one. The flow walk (`findNearestDataSourceDoc`) read only a widget's TOP-LEVEL `DataSource` key, so a pluggable list \u2014 whose source sits at `Object.Properties[datasource].Value.DataSource` \u2014 contributed nothing, and its `Objects[].Properties[].Value.Widgets` descent (the one the entity walk gained in #935) was missing too", "file": "`mdl/backend/pagemutator/mutator.go` (`resolveSourceScope`/`resolveSourceScopeVia`, `listenTargetDataSource`, `widgetOwnDataSourceDoc`, `pluggableDataSourceDoc`; `EnclosingEntity`/`EnclosingEntityForChildren`/`EnclosingDataSourceFlow` now share the one walk `findNearestDataSourceDoc`, and `findEnclosingEntityContext` + its two helpers are deleted)", "insight": "**Count the source kinds before fixing one.** `generated/metamodel/types.go`'s `DataSource is implemented by` list closes the set at ten, and they divide exactly three ways \u2014 seven carry an EntityRef, two are flows, one (ListenTarget) borrows the scope of the widget it names \u2014 so one resolver can be complete, where three successive per-kind patches (FINDINGS #55 association+flow, #935 pluggable, this one) each left a hole. **A nearer source that resolves to no entity must SHADOW the outer one**: inheriting is what wrote the wrong entity, and it also mis-scoped a flow-sourced list nested in an entity-bound data view \u2014 a case the report did not name and the old code got wrong. The listen target is found by a shape-independent search for \"a document with this Name that has a data source\", which is what makes it work when the target is a pluggable widget keeping its source three levels inside its Object; a visited-set guards a hand-written listen cycle. **Measurement trap: a CE1613 SUPPRESSES the CE0402s in the same `mx check` run** \u2014 the first reading said mxbuild tolerated the unbound parameter, and the CE0402s only appeared once the re-scoped binding was fixed, so count bindings in `describe`, not errors. The issue's own second repro (a Gallery over a DATABASE source) no longer reproduced \u2014 #935 had fixed it \u2014 and the live defect was its flow-sourced variant, so re-measure a report against main before trusting its class. Tests `mdl/backend/pagemutator/mutator_selection_source_test.go` (7, incl. dangling/cyclic listen targets and the shadowing control); repro `mdl-examples/bug-tests/1076-alter-page-selection-and-flow-scope.mdl` \u2014 2 \u00d7 CE1613 + 3 unbound before, 0 errors after, on mxbuild 11.10.0. Each half proven load-bearing by stubbing it alone and rebuilding the CLI", "refs": ["mendixlabs/mxcli#1076", "#55", "#935"], "ce": ["CE0402", "CE1613"]} {"area": "mdl/backend", "date": "2026-09-15", "symptom": "On the default engine, `describe workflow` printed `wait for notification x;` without its boundary events (and their handler flows); the legacy engine described them. A describe -> exec round trip lost them.", "cause": "workflowActivityFromGen's typed switch had no case for *genWf.WaitForNotificationActivity, so it fell to workflowSimpleActivityFromGen, which reads only Name and Caption from raw BSON. That path's comment said the wait activities 'have no genWf struct' — true once, stale by the time gen gained WaitForNotificationActivity with BoundaryEventsItems().", "fix": "Typed case reading boundaryEventsFromGen(a.BoundaryEventsItems()), like the five other activities that carry boundary events.", "file": "mdl/backend/modelsdk/workflow_read.go", "insight": "A fallback path documented as 'for types gen does not model' silently keeps catching a type after gen starts modelling it; the reader then under-reads without any error. When gen is re-vendored, grep the typed switches for types that newly have structs. The bug surfaced only because a probe described what it had just written — reading back your own write, per activity type, is the cheap check."} +{"area": "mdl/backend/mcp", "date": "2026-09-15", "symptom": "Over MCP (--mcp) against Studio Pro 11.14, `create workflow` fails with `ped_create_document …: Validation errors … {\"/context\":\"Expected reference (string), got undefined\",\"/workflowName\":\"Expected string, got object\"}`; and on any version a workflow's on-created microflows and event handlers were silently absent from the created document", "cause": "(1) Studio Pro 11.14's Workflows$Workflow CONSTRUCTOR takes `context` (entity qualified name) and plain-string `workflowName`/`caption`; the mapper sent the older `parameter` element and a StringTemplate. The element (update) shape is unchanged. (2) mapWorkflowActivity hard-coded `onCreatedEvent: {$Type: Workflows$NoEvent}` and mapWorkflow never sent `onWorkflowEvent`, so constructs added to the semantic model later were dropped", "file": "`mdl/backend/mcp/workflow.go` (`workflowConstructorTakesContext`, `mapWorkflowContent`, `mapOnCreatedEvent`, `mapWorkflowEventHandlers`)", "insight": "`ped_get_schema` is a live oracle — ask it rather than guessing a version: the constructor schema text says `context: Reference<'DomainModels$Entity'` on servers that need the new shape. Any hard-coded default in a PED mapper (NoEvent, empty list) becomes silent data loss the moment the semantic model gains the property — grep the mappers for literals when a construct becomes authorable. ped_check_errors lags: a fresh document reports 'No errors found.' and its real errors only on a later call, so re-check before trusting a clean verdict"} +{"area":"mdl/backend","date":"2026-09-15","symptom":"AddAttribute and UpdateAttribute were unimplemented on the default engine, so api/ could only work on the retired legacy one — and api/'s whole integration suite reported green while verifying nothing","cause":"api/ imported mdl/backend zero times: it held a concrete *mpr.Writer and bypassed the backend abstraction, so the two methods had no caller through a backend value and were never ported.","file":"api/api.go","fix":"api.New takes a backend.FullBackend (plus an api.Open convenience that owns its connection); the two methods are implemented on the codec backend and struck off unreachableUnimplemented.","insight":"unreachableUnimplemented is a CENSUS OF WHO BYPASSES THE ABSTRACTION, not dead interface surface. Read its reason column as a map and it names api/, the MCP backend, and the cmd/mxcli commands holding a concrete reader; a method is on it BECAUSE such a caller exists and unreachable BECAUSE that caller does not use a backend value. So the list shrinks by closing a bypass, never by deleting methods — and the pin fails loudly when you do, which is how it should be used. Two traps in the port itself. (1) The generated gen list offers only Append and Remove, so replacing an element in place means rebuilding the list; the naive remove-then-add moves the edited attribute to the bottom of the entity, which is a diff on every edit — control: break the rebuild and the order test catches it. (2) THE SUITE THAT WOULD HAVE VERIFIED THE PORT HAD ONLY EVER SKIPPED. api/'s integration tests pointed at ../mx-test-projects/test-source-app, which is not in the repository, so all ten skipped on every machine and in CI since they were written (the #808 shape). `go test ./api/` passing said nothing. Repointed at the committed testdata/expr-checker fixture the codec backend's own tests use, and made a missing fixture FATAL rather than a skip, since a committed fixture's absence is a broken checkout. Before trusting a suite to verify a refactor, check it is not skipping."} +{"area":"mdl/backend","date":"2026-09-15","symptom":"The MCP backend held a concrete *mpr.Reader for its local reads, keeping three methods (GetDomainModelByID, GetWorkflow, ListNavigationDocuments) on FullBackend with no caller through a backend value — and 190 tests in the package, not one of which called Connect, so swapping the reader underneath it would have landed unverified with the suite green.","cause":"Those three reads had no codec-backend implementation, so MCP could not compose a backend and kept a legacy reader instead. Each was a re-keying or widening of a read the package already did, not new decoding.","file":"mdl/backend/mcp/backend.go","fix":"Implemented the three on the codec backend (mdl/backend/modelsdk/mcp_bypass_reads.go), sharing domainModelFromGen / workflowFromGen / navProfileFromGen with their siblings; added Backend.ConnectReadOnly and pointed MCP's reader field at backend.FullBackend. Wrote mdl/backend/mcp/connect_reads_test.go — the first test to call Connect at all.","insight":"A test can be vacuous in two different ways in one file, and the second is the one you miss. The read-only assertion SKIPPED (MyFirstModule has no entities), which the runner prints — so it was obvious. The GetWorkflow assertion looped over the listing and the fixture has ZERO workflows, so the loop body never ran and the test reported PASS having called nothing. Same failure class as #808, but silent. The fixes differ by shape: for the skip, pick a probe needing nothing pre-existing (CreateEntity, not AddAttribute); for the empty loop, SEED the thing through a read-write backend, then reconnect read-only and assert a specific named item is in the listing BEFORE fetching it — that guard is what converts a future vacuous pass into a failure. Cheapest way to find both: print the counts a test loops over before trusting it. MCP's read-only constraint also has a real control: revert ConnectReadOnly to Connect and the write-refusal test fails with the reported symptom, and the companion TestConnect_TheSameWriteSucceedsReadWrite proves the probe write is not failing for an unrelated reason."} +{"area":"mdl/backend","date":"2026-09-15","symptom":"Deleting the legacy sdk/mpr backend (mdl/backend/mpr) turned up three things the removal plan had not sized: a runtime error telling users to rerun on the engine that had just been deleted, an integration suite that had been exercising the retired engine rather than the default one, and a cross-engine parity test whose comparison became vacuous.","cause":"Each is a reference to the second engine that was invisible while two engines existed. errUnimplemented's message named MXCLI_ENGINE=legacy as the fallback; setupTestEnv in mdl/executor/roundtrip_helpers_test.go hardcoded mprbackend.New() as its default, so most of the package's integration tests ran on legacy; TestODataService_EngineWriteParity compared writer A's key set against writer B's.","file":"mdl/backend/modelsdk/backend.go","fix":"errUnimplemented now asks for a bug report instead of naming a fallback. setupTestEnv defaults to the codec backend. The parity test keeps its value assertion (a published OData service retains AllowedModuleRoles) and drops the comparison, renamed to match. --engine/MXCLI_ENGINE kept as a warning-only no-op; `bson compare` and mdl/enginecompare deleted.","insight":"Deleting one of two implementations is not finished when the code compiles — grep for the deleted thing in three places the compiler cannot reach. (1) RUNTIME STRINGS: an error message naming a removed fallback is strictly worse than no fallback, because the user follows it and gets a second failure; this one had been shipping the instruction 'rerun with MXCLI_ENGINE=legacy' from 19 generated stubs. (2) TEST DEFAULTS: a shared setup helper's hardcoded choice of implementation decides what a whole package actually covers, and here it silently pointed most of mdl/executor's integration tests at the engine being retired — deleting the other one moved them onto the default engine for the first time, which is coverage gained, not lost. (3) DIFFERENTIAL TESTS: a test that compares A to B has no meaning with one implementation, but the PROPERTY it was a means to usually does — for the OData one, that a service keeps its role grants, which mx check reports as 0 errors either way. Drop the comparison, keep the property, rename the file so the next reader is not misled. One counter-case worth noting: the doctype gate's engine MATRIX was kept at one entry rather than collapsed, because its selection function still converts a stale MXCLI_TEST_ENGINES=legacy into a loud failure instead of a gate that selects zero engines and reports success."} +{"area": "mdl/backend/mcp", "date": "2026-09-15", "symptom": "Over MCP against Studio Pro 11.14, `create workflow` with any `multi user task` fails: `ped_create_document …: {\"/flow/activities/1/taskPage\":\"Expected an object, but the value is missing.\"}`", "cause": "The 11.14 Workflows$MultiUserTaskActivity CONSTRUCTOR takes `taskPage: Element<'Workflows$PageReference'>`; mapWorkflowActivity sent the older bare `pageReference` string. Same class as the workflow constructor's `context` change in the same release", "file": "`mdl/backend/mcp/workflow.go` (`multiUserTaskConstructorTakesTaskPage`, `adaptMultiUserTaskPages`)", "insight": "PED constructor shapes drift per Studio Pro release independently of the element (update) shapes, and serverInfo.version is frozen at 1.0.0, so probe the constructor schema text rather than gating on a version. When one constructor of a document family changed shape in a release, check the others in that family before trusting the mapper — this one surfaced only because a phase-3 live probe used a multi-user task"} +{"area":"mdl/backend/modelsdk","date":"2026-09-15","symptom":"`describe microflow` prints a notify action as `notify workflow $Workflow;` with no `$X =` output variable, although the stored action has one; a rewrite from that output drops it","cause":"`modelsdk/gen` binds NotifyWorkflowAction.outputVariableName to the BSON key \"VariableName\"; Studio Pro 11.14 and the 11.6 metamodel store \"OutputVariableName\". The reader used gen's accessor and got nothing; the hand-built writer wrote the right key, so only reads were wrong","file":"`modelsdk/gen/microflows/types.go` (`initNotifyWorkflowAction`, STORAGE-NAME OVERRIDE), `mdl/backend/modelsdk/microflow_read_actions.go`","insight":"A property whose writer is hand-built and whose reader goes through gen can be wrong in one direction only, which a write-then-read test on the codec cannot catch — both sides agree with themselves. Read a Studio Pro-saved document instead (ako/TestApp's ZzMxcliExample_Notify is the fixture). 16 gen types bind `VariableName`, and most are right (the metamodel stores `variableName` for aggregate, cast and create actions), so fix by type against the metamodel, never by search-and-replace. Settled on mxbuild 11.6/11.10/11.13: under `VariableName` the variable is undefined (CE0109)."} +{"area": "mdl/backend", "date": "2026-09-15", "symptom": "Five `TestConnect_*` tests in `mdl/backend/mcp/connect_reads_test.go` (added by ako/mxcli#468) fail in a devcontainer — on pristine origin/main — with `Connect: connect to MCP server \"http://127.0.0.1:NNNNN/mcp\": ... dial tcp 192.168.65.254:NNNNN: connect: connection refused`, while CI is green", "cause": "The tests built the backend with `New(ped.srv.URL+\"/mcp\", \"\")`. An empty dial address falls through to `defaultDial` → `dialFor` in `mdl/backend/mcp/client.go`, which rewrites a localhost endpoint to `host.docker.internal:` whenever that name resolves — the intended convenience for reaching a Studio Pro on the Docker host. Inside a devcontainer it resolves, so the request goes to the host gateway and never reaches the httptest listener on the container's own 127.0.0.1. On a CI runner the name does not resolve and the rewrite is a no-op, which is why the suite was green there", "file": "`mdl/backend/mcp/connect_reads_test.go` (`backendFor`, used by `connected`, `TestConnect_GetWorkflowReadsASeededWorkflow`, `TestConnect_DisconnectIsIdempotent`); precedent `fakePED.connectClient` in `client_test.go`", "insight": "**A fake server must be dialled at the address it is listening on, never through production address resolution.** Any helper that takes an optional dial/host override and defaults to environment-sensitive logic (DNS lookups, proxy env, gateway rewrites) makes a test pass or fail by where it runs, not by what it tests — and CI is precisely the environment where such a rewrite is inert, so it cannot catch it. Pass the listener address explicitly (as `connectClient` already did) rather than changing `defaultDial`, which is correct for real connections. The tell in the error is the dialled IP differing from the URL's host. Control: revert the dial to `\"\"` in the devcontainer and the same five fail with the reported message; `TestConnect_TheSameWriteSucceedsReadWrite` passes either way because it never builds an MCP backend, which is what localises the defect to the constructor call", "refs": ["ako/mxcli#468"]} +{"area":"mdl/backend","date":"2026-09-15","symptom":"Six FullBackend methods sat on the unreachable census being treated as abstraction bypasses awaiting a port, when nothing anywhere wanted them: the whole WidgetSerializationBackend interface (SerializeWidget/ClientAction/DataSource/WorkflowActivity), GetUnitTypes and UpdateLayout. They had been superseded and simply never removed.","cause":"scripts/backend-reachability.sh reports DEAD for 'nothing calls this through a backend value', and that one verdict covers three different situations. The census header read all of them as bypasses, so the standing instruction was to port them — which for these would have meant implementing methods with no caller.","file":"mdl/backend/mutation.go","fix":"Deleted WidgetSerializationBackend, GetUnitTypes and UpdateLayout from the interface; regenerated unimplemented_gen.go and mcp/unsupported_gen.go (276 -> 270 methods); dropped the mock stubs and a duplicate *Backend impl. Census 11 -> 6, and the remaining six are genuine bypasses. Rewrote the census header and the probe script's own header to name the three causes.","insight":"A reachability probe that removes a method and rebuilds answers ONE question — is anything calling this through the interface — and DEAD has three causes wanting opposite fixes: BYPASS (a caller wants it but holds a concrete type; port the caller), ORPHAN (no caller anywhere; delete), DUPLICATE (callers exist but through a narrower package-local interface with a DIFFERENT SIGNATURE; delete). The probe cannot separate them; a grep for callers under any type can. DUPLICATE is the one that misleads, and it is worth knowing its two tells. First, a raw call-site count looks healthy — SerializeWidget had 6 and SerializeClientAction 4 — so the name reads as live; only comparing signatures shows the interface copy is unused (bson.D on the real path vs (any, error) on FullBackend). Second, the supersession is usually DOCUMENTED at the replacement, not at the corpse: WidgetBuilderBackend.SerializeWidgetToOpaque says 'This replaces the direct mpr.SerializeWidget call' — so grepping the NEW method's doc comment finds the old one faster than auditing the old one does. A stale comment on the dead method actively lies (the vestigial *Backend SerializeWorkflowActivity claimed the ALTER WORKFLOW paths used it; they use codecWorkflowDeps). Generalisable: when a helper has both an (any, error) and a concrete-typed variant, suspect the general-typed one is the abandoned interface obligation."} +{"area":"mdl/backend","date":"2026-09-15","symptom":"Backend.CreateEntity and CreateAssociation left the caller's semantic element holding an EMPTY ID, so the very next call failed with \"entity not found: \" (note the blank). The legacy writer had populated them, so this was a silent behaviour difference across the engine swap that no test caught.","cause":"assignEntityIDs/assignAssociationIDs mint identities on the gen element built by entityToGen/assocToGen, and nothing copied them back to the *domainmodel.Entity the caller passed in. assignID only fills an EMPTY id, so api/ was unaffected (its builders pre-assign one) — which is exactly why no existing test saw it.","file":"mdl/backend/modelsdk/domainmodel_write.go","fix":"copyAssignedIDs writes the minted entity and attribute ids back onto the caller's element, matching attributes BY NAME rather than by position; CreateAssociation does the same for its own id. Both only fill an empty id, so a caller-assigned one is preserved.","insight":"Found by RUNNING examples/modify_project after repointing it, not by any test — the unit suite, check-mdl and the whole integration gate were green. That is the argument for keeping example programs runnable and actually running them: an example is the only caller that uses the API the way an outside user would, with no pre-assigned ids and no knowledge of internals. Two specifics worth carrying. (1) The empty-string ID makes the error message read \"entity not found: \" with nothing after the colon — a trailing-blank in an error is a strong tell that an identity was never populated rather than looked up and missed. (2) When copying minted ids back, match sub-elements BY NAME, never by index: entityToGen can add, skip or reorder (an audit pseudo-type becomes a System-module generalization, not an attribute), so an index-based copy hands the caller ANOTHER attribute's identity — which looks like it worked and is worse than the empty id it replaced. The control matters here too: a test that only asserts 'id is non-empty' passes against an implementation that overwrites a caller-assigned id, which would break api/; assert the reported id equals the STORED one, and add a companion test that a pre-assigned id survives."} +{"area":"mdl/backend","date":"2026-09-15","symptom":"Moving cmd/mxcli/project_tree.go from a concrete sdk/mpr reader to the codec backend silently dropped System.VerifyPassword from `mxcli project-tree` output. Build clean, all tests green, 131 bytes missing from a 78KB JSON tree.","cause":"The System module's Java actions are platform built-ins with NO stored unit in the .mpr. sdk/mpr's ListJavaActions/ListJavaActionsFull appended them via BuildSystemJavaActions(); the codec backend only decoded stored units, so it reported them as absent.","file":"mdl/backend/modelsdk/java.go","fix":"Moved the System Java action definitions from sdk/mpr to modelsdk/meta (which already owns the virtual System module's entities and associations) and appended them in both codec ListJavaActions and ListJavaActionsFull; sdk/mpr now delegates rather than holding a second copy. Regression tests plus a control that the STORED actions are still returned.","insight":"Found by diffing the command's output against a binary built from the pre-port commit — not by any test, and no test would have caught it. That baseline-diff is the technique worth keeping for any reader swap: build the old binary first (git stash -u; make build; cp bin/mxcli /tmp/before), then require byte-identical output on every command whose reader you touched. A 131-byte difference in 78KB of JSON is invisible to eyeballing and to 'it still runs'. Two structural lessons. (1) A SYNTHESIZED element is the thing a reader swap loses, because it exists in one reader's code rather than in the data — grep the old reader for 'not stored in' / 'virtual' / 'Build*' helpers before trusting a port. (2) The census in unimplemented_reachability_test.go CANNOT catch this class of bypass: it only lists methods with no implementation, so a caller holding a concrete reader while calling only IMPLEMENTED methods is invisible to it. project_tree.go called 36 semantic methods, every one on FullBackend, and never appeared — which is also why the Phase 3 write-up wrongly described all remaining bypasses as raw-unit debugging tools. The complete list of bypasses is the sdk/mpr IMPORTER list, not the census."} +{"area": "mdl/backend", "date": "2026-09-15", "symptom": "Every ALTER WORKFLOW op addressing an activity (e.g. `insert boundary event on bugSplitJump …`) failed with `ambiguous activity \"bugSplitJump\" (2 matches); use @N to disambiguate` whenever the workflow also contained `jump to bugSplitJump`. Measured live over MCP against Studio Pro 11.14; `bugSplitJump@1` worked.", "cause": "buildJumpTo (mdl/executor/cmd_workflows_write.go) names every jump `JumpTo` and, with no MDL caption, sets Caption = the target's name. Both activity resolvers — mcpWorkflowMutator.searchActivities and wfmutator's findActivitiesRecursive/findActivityIndexRecursive — matched `name == ref || caption == ref` into ONE pool, so the jump's caption shadowed its target's name. Same defect in the MPR mutator, it was only reported over MCP.", "file": "mdl/backend/mcp/workflow.go", "fix": "Resolve in two tiers in both backends: collect name matches and caption matches separately in the same walk, use the name tier when non-empty, else the caption tier; ambiguity and @N are judged within the chosen tier. The jump's default caption was left alone — existing projects already carry that shape, and changing it would rewrite every mxcli-authored jump on the next re-run.", "insight": "A reference that can match either an identity (name, deduplicated) or a label (caption, free text) must rank them, never pool them — any label that happens to repeat an identity turns a unique name ambiguous, and mxcli's own defaults manufacture exactly that collision. The tell is `N matches` where N counts a jump/marker you did not think of as a candidate. When fixing a resolver, grep for its twin in the other backend (wfmutator vs mcp): the two were written to agree on @N numbering and had the same bug. Controls: both new tests fail with the reported message against the pooled resolver."} diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index d4d727be65..22a7a7f904 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -607,13 +607,20 @@ {"area": "mdl/executor", "date": "2026-09-13", "symptom": "A workflow with `boundary event timer '\u2026'` (no interrupting / non interrupting) passes check and builds at 0 errors; the runtime then fails to start: `Class 'Workflows$TimerBoundaryEvent' could not be found`", "cause": "The bare form maps to `Workflows$TimerBoundaryEvent`, which exists in no cached 11.x runtime (only Interrupting/NonInterruptingTimerBoundaryEvent). mxbuild tolerates the unknown type. It was the documented syntax example", "file": "`mdl/executor/validate_workflow_refs.go` (`bareTimerBoundaryEventErrors`, MDL-WF07), `cmd/mxcli/syntax/features_workflow.go`", "insight": "**A type mxbuild accepts is not a type the runtime has.** Found only because a verification boot of an unrelated fix loaded it. When a grammar has a default branch that maps to a storage type, check that type against the runtime's class list, not against `mx check`", "fix": "Refuse the bare form on 11+ at check and exec, CREATE and every ALTER op that can carry a boundary event; update syntax help, skill table and the ako/mxcli#415 bug-test script to name the kind"} {"area": "mdl/executor", "date": "2026-09-13", "symptom": "A view entity whose association column is also declared as an attribute (`MeterRef: Trends.Meter` or `MeterRef: Trends.Meter.ID` beside `select m.ID as MeterRef`) passes `mxcli check`; `check -p` says 'OQL select has 1 columns but 2 attributes declared'; exec writes `Enumeration(Trends.Meter)` and mx check reports CE1613, or throws 'An error occurred when trying to set the Enumeration property' for the three-part form", "cause": "A bare qualified name parses as TypeEnumeration (the entity/enum ambiguity), and execCreateViewEntity converted it with convertDataType without asking what it names. The alias-to-attribute alignment skips association columns, so the declared attribute had no column and was compared against the next one", "file": "`mdl/executor/oql_view_associations.go` (`ValidateViewAttributeDeclarations` MDL080, `viewAttributeEntityTypeErrors`), `mdl/executor/cmd_entities.go` (`execCreateViewEntity`), `mdl/executor/validate.go`, `mdl/executor/validate_program.go`, `cmd/mxcli/lsp_diagnostics.go`", "insight": "**The TypeEnumeration/TypeEntity ambiguity has a consumer wherever a data type becomes a stored type, and view entity attributes were one nobody had listed.** Split the refusal by what it needs: an association column's alias and a three-part name are decidable from the script, so they belong in the no-project phase that exec's pre-check also runs; entity-vs-enum needs the project, so it goes in check -p AND the handler, because exec --no-check skips both phases. Verify the handler refusal by counting changed files, not by the error text", "fix": "Refuse in ValidateProgram/LSP (MDL080) and at the top of execCreateViewEntity before any backend call; report an attribute once"} {"area": "mdl/executor", "date": "2026-09-13", "symptom": "Three new MDL-WIDGET27 tests passed locally and failed in CI on the same commit: two reported the fallback remedy (\"move the entries into the widget body as container blocks\") instead of naming the container keyword, and the third found 0 violations where it wanted 1", "cause": "The tests resolved the widget through `LoadWidgetRegistry(fixtureProject(t))`, which reads `.def.json` files from `testdata/expr-checker/.mxcli/widgets/`. That directory is GITIGNORED — the definitions are derived, not tracked — so they exist for any developer who has ever run `mxcli widget docs` against the fixture (I generated them earlier in the same session, while investigating) and never exist on the runner. With no definition, `containerKeyword` returns \"\" and the two definition-dependent branches degrade exactly as designed: fallback wording, and silence for the scalar case", "file": "`mdl/executor/validate_widget_object_property_test.go` (`fixtureProjectWithDefs`)", "insight": "**A gitignored fixture makes a test environment-dependent in the one direction nobody checks** — the developer's tree is a superset of the runner's, so the test is green exactly where it is not being tested. The fix is to DERIVE the artifact from tracked inputs inside the test (`RefreshWidgetDefinitions` over the fixture's tracked `.mpk` files, into a temp copy, after removing any `.mxcli` the developer's tree carries), so local and CI see identical inputs. Reproduce by moving the gitignored directory aside before believing any diagnosis. **The sibling lesson is why this was not caught by the existing suite**: #999's test asserted `strings.Contains(msg, \"attribute\")` on a widget whose property is named `attributes`, so the property name alone satisfied it and the assertion passed with NO definition loaded — a substring assertion whose needle is a substring of the data it is meant to distinguish from proves nothing. Tightened to the remedy shape (`` `attribute (…)` blocks ``) and verified with the derivation stubbed: all four then fail, where before only the three new ones did", "refs": []} -{"area": "mdl-executor", "date": "2026-09-13", "symptom": "DESCRIBE silently deletes an ExclusiveMerge: describe -> exec leaves the microflow with fewer merge nodes than the stored graph, with no warning, no MDL-FLOW01 and mx check clean", "cause": "The nested describer walks straight through a merge with a single incoming path without emitting anything for it, so the rebuild has no reason to create it. Only two merge shapes were represented: a split's join point (rendered by `end if`) and a labelled error rejoin (`merge