diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl b/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl index dbd41266cc..9f453c80d2 100644 --- a/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl +++ b/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl @@ -107,3 +107,5 @@ {"area": "cmd/mxcli", "date": "2026-09-11", "symptom": "`mxcli theme create --from ` seeds the palette and nothing else: the scaffolded brand theme still described itself in `theme list` as 'Cool slate, one teal signal colour' with Signal's six swatches, and vendored ~500 KB of IBM Plex woff2 the seeded --mxt-font never names, plus a SIL OFL licence for fonts it does not use. Separately, the primary button was never the brand colour: Atlas derives --btn-primary-bg from --brand-primary-600 = color-mix(brand, contrast 20%), so a brand blue #10069F rendered rgb(21,13,140)", "cause": "`manifest()` copied the base theme's Summary and Colorway verbatim and the walk copied every file unconditionally. The Atlas map pinned `--btn-primary-color` to `--mxt-brand-ink` \u2014 an ink each theme picks to sit on `--mxt-brand` (console pairs near-black #04211d with bright teal #2dd4bf) \u2014 while leaving the background to Atlas's derivative, so the pairing the theme designed for was never the pairing that rendered. The map's own comment already called it 'a brand-filled button'", "file": "`cmd/mxcli/theme/create_seeded.go`, `create.go` (`manifest`, the scaffold walk), `assets/*/files/theme/web/_mxcli-atlas-map.scss`", "insight": "**Inheriting a statement ABOUT the base theme into a theme whose palette is no longer the base's is a confident lie; derive it or drop it.** Two traps in the font half. (1) The decision must be made BEFORE the walk: it is taken by reading the partial and applied to files elsewhere in the tree, so doing it inline depended on WalkDir's lexical order putting `_mxcli-.scss` before `mxcli-fonts/` \u2014 true only because of the leading underscore. (2) It touches two halves \u2014 the @font-face rules and the woff2 files \u2014 and getting either alone wrong is silent: a surviving rule for a deleted file 404s, a surviving file nothing loads is the dead weight being removed. Unit tests on each half cannot catch a mismatch; the guard is an integration assertion that a scaffolded theme ships exactly the fonts it loads (control: stubbing the file half fails it with 'X is shipped but no @font-face loads it'). Reported by ako/ChipCoV1", "refs": ["ako/ChipCoV1 FINDINGS.md"]} {"area": "cmd/mxcli", "date": "2026-09-13", "symptom": "`mxcli run --local --ensure-db` cannot provision PostgreSQL in a non-root devcontainer (Debian/Ubuntu base, remoteUser vscode): reported as a bare \"PostgreSQL did not become ready at 127.0.0.1:5432 within 20s\", or as `exec: \"initdb\": executable file not found in $PATH`. Fires on every fresh Claude Code session in an initialized project, since `mxcli init` wires `run --local --setup --ensure-db` into the SessionStart hook", "cause": "THREE independent defects on one path, each sufficient to block it. (1) `service postgresql start` ran unelevated: Debian's /etc/init.d/postgresql runs under `set -e` and calls create_socket_directory FIRST, which chmods /var/run/postgresql — refused for a non-root user, so the script aborts before it looks at a single cluster. (2) The #823 user-owned-cluster fallback was INERT on Debian/Ubuntu: postgresql-common wraps only the CLIENT tools (psql, pg_isready, pg_ctlcluster) into /usr/bin, while initdb/pg_ctl live in /usr/lib/postgresql//bin — so the safety net for a failed service start could never deploy on the platform that needs it most. (3) `resolveSuperuser` used `sudo -n -u postgres psql`, but mcr.microsoft.com/devcontainers/base grants its user sudo to root ONLY (`vscode ALL=(root) NOPASSWD:ALL`, confirmed in devcontainers/features main.sh) — so the target is refused even though the user is effectively an administrator. Plus a diagnostic defect: the 20s readiness timeout discarded the service-manager output the package had already collected", "file": "`cmd/mxcli/docker/ensuredb.go` (`serviceStartAttempts`, `postgresServerBinDir`/`postgresTool`, `superuser.viaRoot`, `withServiceDiag`)", "insight": "mxcli GENERATES the broken environment — `generateDockerfile` emits that exact base image, installs postgresql, and runs as vscode — so this was not user misconfiguration, and fixing it in code (not the template) also repairs projects already scaffolded. Root may target any account, so `sudo -n -- sudo -n -u postgres` reaches postgres under a root-only sudoers policy; try the direct form first and nest only on refusal. Resolve initdb and pg_ctl from the SAME bin directory and rank majors NUMERICALLY — a lexical sort puts \"9\" above \"16\", and a data directory made by one major cannot be started by another. **Measurement trap that cost the most time**: reasoning about which error the user would see is unreliable here — four plausible code paths produce four different messages, and the reported wording was reproducible by none of them on Ubuntu 24.04/PG16. What settled it was building a harness that calls `EnsureDatabase` directly and running it as a real non-root user with the real sudoers rule, then isolating each defect with a one-variable control (widen sudoers to `(ALL)` and nothing else changes → provisioning succeeds; prepend /usr/lib/postgresql/16/bin → the fallback completes). Each of the four fixes was reverted individually and its test re-run: two controls initially failed to COMPILE rather than reproducing the symptom, which proves nothing — they were redone faithfully before being believed", "refs": ["mendixlabs/mxcli#984", "#823"]} {"area": "cmd/mxcli", "date": "2026-09-13", "symptom": "`build-and-test` fails in CI on `TestSettleSourceReturnsPromptlyForOneChange` \u2014 \"a quiet source took 196.975373ms to settle, want under 100ms\" \u2014 while the SAME tree passes in another run of the same workflow minutes earlier", "cause": "The test bounded elapsed wall-clock time as a multiple of the poll interval (`poll * (sourceSettleWindow + 3)`, 100ms against a nominal 40ms). settleSource waits on `time.After(poll)`, which guarantees AT LEAST the duration and nothing about the upper bound, so a loaded runner blows the budget with no defect present.", "file": "cmd/mxcli/docker/runlocal.go (settleSourceWith, the injected tick), cmd/mxcli/docker/runlocal_settle_test.go", "insight": "The property being guarded was a POLL COUNT, not a duration \u2014 'a quiet source costs one extra poll' \u2014 so the fix is to make polls countable (inject the timer) rather than to widen the budget, which only moves the flake threshold. Diagnosis shortcut worth reusing: the same workflow ran twice on the same tree, once from the push event and once from the pull_request merge commit, and disagreed \u2014 two runs of one tree is direct evidence of nondeterminism and cheaper than reading the test. Two things the controls settled that reasoning did not: (1) the assertions are written in terms of `sourceSettleWindow`, so WIDENING that constant leaves both tests green \u2014 they assert the loop honours whatever window is declared, never the number itself, and the real control is a loop that costs one poll MORE than it declares (both fail). (2) Each tick call must return a freshly-armed channel; returning one shared channel makes the multi-file test HANG rather than miscount, so the re-arm is load-bearing and not a style choice. The seam also made a previously untestable guarantee expressible: the window must be sourceSettleWindow CONSECUTIVE quiet polls, and dropping `quiet = 0` from the change branch was green against every pre-existing test in the file.", "refs": ["ako/mxcli#449"]} +{"area":"cmd/mxcli","date":"2026-09-15","symptom":"Porting cmd/mxcli/docker off sdk/mpr moved two WRITE paths (ensureDemoUsers, applyHarvest) onto the codec backend. A baseline diff of `docker check` showed the project byte-identical across 421 files — which proved nothing, because the run had not written anything.","cause":"docker check's widget-update harvest is a no-op on an already-clean fixture, so an output+filetree diff against a pre-port binary exercises only the READ paths. Coverage then showed ensureDemoUsers at 0.0% — a write path the port touched that no test in the package ran.","file":"cmd/mxcli/docker/build.go","fix":"Added TestEnsureDemoUsers_CreatesAdminWhenNoneExist and _SkipsWhenUsersExist, plus a clearDemoUsers helper that establishes the precondition. Coverage 0.0% -> 76.5%. The read paths keep the baseline-diff evidence; applyHarvest was already at 76.9% via TestRunUpdateWidgets_RestoresV2AfterConversion.","insight":"A byte-identical baseline diff is strong evidence for a READ port and near-worthless for a WRITE port, because the natural control (nothing changed) is also what a no-op produces. The two need different instruments, and the cheap way to tell which you have is `go test -coverprofile` + `go tool cover -func` grepped for the functions you touched: it answers 'did my port's code even run' in one command, where a passing suite does not. Here it separated applyHarvest (76.9%, genuinely exercised including its UpdateRawUnit) from ensureDemoUsers (0.0%) inside the same package, so the gap was specific rather than a general absence of tests. Second trap, hit while fixing it: the shared v2 fixture ALREADY HAS two demo users, so the create-path test skipped and the idempotence test asserted the wrong count. Skipping on an unmet precondition is the #808 shape — set the precondition up instead (RemoveDemoUser in a helper, then assert the helper actually emptied it before proceeding). Third: read back through a FRESH connection, since asserting on the value the writer still holds passes against a write that never reached disk."} +{"area":"cmd/mxcli","date":"2026-09-15","symptom":"Porting the last cmd/mxcli readers off sdk/mpr, cmd_extract_templates.go compiled with a type error (RawType/RawObject are bson.D on sdk/mpr, any on types.RawCustomWidgetType). Casting past it would have compiled — and broken the command at runtime, because FindCustomWidgetType is UNIMPLEMENTED on the codec backend.","cause":"mdl/backend/modelsdk/unimplemented_gen.go carries FindCustomWidgetType; measured at runtime it returns 'FindCustomWidgetType is not implemented on the model engine. This should be unreachable'. cmd_extract_templates.go was calling it through a concrete *mpr.Reader, so it was reachable only by NOT going through the backend.","file":"cmd/mxcli/cmd_extract_templates.go","fix":"Left this one file on sdk/mpr with a comment saying why and what would fix it (implement FindCustomWidgetType on the codec backend), and ported the other five. cmd/mxcli is otherwise clean; importers 13 -> 8.","insight":"The type error was the lucky part. A compile error is the ONLY reason this did not ship as a runtime failure — the cast that silences it is one line, and nothing else would have objected. When a port hits a type mismatch at a backend boundary, check whether the backend method is implemented at all before reconciling the types: `grep -n '' mdl/backend/modelsdk/unimplemented_gen.go` answers it in one command, and a runtime probe (connect read-only, call it, log the error) confirms it in under a minute. Note the direction of the trap: the unimplemented method's own error says 'This should be unreachable', and porting a caller to the backend is precisely what MAKES it reachable — so the #477 census blind spot (callers holding a concrete reader are invisible) cuts both ways. Second, smaller measurement trap in the same slice: a baseline diff of `check --post-migration` showed 50 lines vanishing, which looked like a regression and was not — the FIRST run built and cached a catalog inside the project, so the second run reused it. Two binaries must each get their own fresh copy of the fixture, exactly as for a write port; a command that caches into the project directory makes consecutive runs non-independent even when nothing is being written on purpose."} diff --git a/.claude/skills/fix-issue/findings/mdl-backend.jsonl b/.claude/skills/fix-issue/findings/mdl-backend.jsonl index 71727447b8..5cb3649c7e 100644 --- a/.claude/skills/fix-issue/findings/mdl-backend.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-backend.jsonl @@ -95,3 +95,19 @@ {"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/mcp", "date": "2026-09-15", "symptom": "Over MCP against Studio Pro 11.14, every `create microflow` fails at ped_create_document: `{\"/flows/0/$Type\":\"Expected an element with $Type property.\",\"/returnType\":\"Expected one of [Void, Boolean, …], got {\\\"type\\\":\\\"Void\\\"}\"}`", "cause": "The 11.14 Microflows$Microflow CONSTRUCTOR became a canvas skeleton: flows are `$Type`d elements, returnType is a bare enum with returnTypeEntity/returnTypeEnumeration beside it, parameters go in a `parameters` list, and every object constructor declares only x/y (plus caption/loopType). Fixing just the two rejected keys is not enough: a create that still sends relativeMiddlePoint, action, returnValue or a split/loop source is ACCEPTED and those are silently dropped (read back: positions 0,0, `action: null` → 'No action defined', empty return value)", "file": "`mdl/backend/mcp/microflow.go` (`microflowConstructorTakesSkeleton`, `adaptMicroflowSkeleton`, `CreateMicroflow`)", "insight": "A validation error lists only what the validator rejects, not what it discards — after making a PED create pass, READ THE DOCUMENT BACK before calling it fixed. The skeleton create plus one ped_update_document that `set`s /objectCollection/objects/N/{action,returnValue,splitCondition,loopSource} (N counts the parameters, which the constructor's $id(/objects/N) does not) round-trips cleanly. Reads expand one level, so a nested value (messageTemplate text, variableType entity, case value) looks empty until its own path is read — don't mistake the stub for loss"} +{"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: without @N, activities whose name equals the reference win when any exist, else caption matches are used; with @N, every name-or-caption match still counts in DESCRIBE order. A first attempt also scoped @N to the chosen tier and CI caught it: mdl-examples/doctype-tests/24-workflow-examples.mdl addresses a second call activity as `ACT_Process@2` (found 1 matches) — @N is an established positional index over the pooled matches, so only the unqualified case may change. 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."} +{"area": "mdl/backend", "date": "2026-09-15", "symptom": "Timer boundary events written over `--mcp` (Studio Pro 11.14): every one reports CE0126 \"Missing value for parameter 'Timer'\"; an interrupting timer's `jump to` outside a parallel split is stored as an End, and inside a split an End path gets a target-less jump; a `{ }` path gets an End appended after the end-of-path marker and is refused; the timer events of a single user task are not stored at all. mxbuild 11.13 builds all of these", "cause": "Neither timer boundary event constructor has `firstExecutionTime` (ped_get_schema), so the delay mxcli sent was ignored; the interrupting constructor normalizes the path terminator (End outside a split, jump inside) and needs `isInsideOfParallelSplit`, which mxcli sent only for notification events; the single user task constructor drops `boundaryEvents`, and the re-add covered notification events only", "file": "`mdl/backend/mcp/workflow.go` (`markBoundaryEventsInSplits`, `timerBoundaryEventOps`, `applyTimerBoundaryEvents`, `applyUserTaskBoundaryEvents`, `InsertBoundaryEvent`)", "insight": "**Three traps that each looked like success.** (1) `ped_update_document` re-validates the WHOLE document, so one malformed boundary path makes every later update fail — the post-write `set` of the delay included; refusing bad shapes before sending is load-bearing, not cosmetic. (2) An `add` to a `boundaryEvents` list does NOT append and does not keep add order (I, NI-A, NI-B in one batch stored NI-B, NI-A, I); finishing timers by statement index put each delay on the OTHER event and `ped_check_errors` said \"No errors found.\" — only reading `firstExecutionTime` back per event caught it. Identify an added element by the `persistentId` that appeared, never by index. The constructor, by contrast, keeps order. (3) `create or modify` over MCP cannot see a workflow created earlier over MCP and not yet saved — the executor's existence check reads the .mpr on disk — so a live update-path probe must target a document that is on disk", "fix": "Send no `firstExecutionTime`; set it afterwards at the event's stored path. Refuse interrupting-timer paths running to their end, interrupting-timer paths in a split not ending in a jump, and non-interrupting-timer paths ending in a jump. Set `isInsideOfParallelSplit` on interrupting timers. Restore a jump outside a split by removing the constructor's End and adding the jump (measured clean). Re-add a single user task's events one `add` at a time, mapping statement index to stored index by persistentId, and address nested paths through that map; ALTER insert reads ids before/after the add"} +{"area": "mdl/backend", "date": "2026-09-15", "symptom": "`create or modify workflow` over `--mcp` (Studio Pro 11.14) stored the flow's activities reversed, with an old activity left in and a new one missing ([Start, a, b, End] rewritten as A, B, C → Start, B, A, a, End; a larger flow also lost its parallel split and held a name twice); `alter workflow … replace activity X` left X in place. The update calls all reported SUCCESS", "cause": "One `ped_update_document` batch is not applied in the order sent. Every measured batch fits: 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 at an index where something was just added removes the added element. `UpdateWorkflow` sent the flow's removes plus its middles in reverse at index 1 in one batch (and index-less adds for event sub-processes/handlers, which come out reversed); `ReplaceActivity` sent remove @k plus adds at k, k+1; `InsertAfterActivity` sent incrementing indices", "file": "`mdl/backend/mcp/workflow.go` (`UpdateWorkflow`, `InsertAfterActivity`, `ReplaceActivity`, `addAtOp`); simulator `pedListSim` in `mdl/backend/mcp/workflow_listops_test.go`", "insight": "**The code comment claimed the reverse-at-index-1 trick worked, and no fake PED modelled batch semantics, so the unit tests asserted the ops sent rather than the list stored.** Fix tests by simulating the server's list semantics and asserting the resulting ORDER, and keep a table test that replays each raw-PED measurement through the simulator — that is what makes the simulator trustworthy and each control meaningful. The first guess at the rule (\"adds first, then removes\") fit two measurements and failed the third; fit the model to every data point before building on it. Also: a live update-path probe needs a workflow the executor can see — either on disk, or created earlier in the same exec (the backend's session list)", "fix": "Never add to and remove from the same list in one batch: add the statement's elements at a single index in their own order (flow middles @1, event sub-processes and handlers @0, replacement activities @k+1), then remove the stored/replaced ones in a second update. Adding first leaves duplicates, not a gutted workflow, if the second update fails"} +{"area":"mdl/backend","date":"2026-09-15","symptom":"Wiring FindCustomWidgetType from modelsdk/mpr.Reader onto the codec Backend by straight delegation made `mxcli extract-templates` extract 0 of 6 templates, reporting for each widget: '[SKIP] Combo box: widget type is bson.D, want bson.D'. The type assertion in the caller names the same type on both sides of 'want'.","cause":"modelsdk/mpr builds RawType/RawObject with the v2 BSON driver (go.mongodb.org/mongo-driver/v2/bson) while sdk/mpr and every caller use v1 (go.mongodb.org/mongo-driver/bson). They are unrelated Go types that both print as 'bson.D', so the mismatch is invisible in the error text. types.RawCustomWidgetType declares the fields as `any` to avoid a BSON dependency, which removes the compiler's ability to catch it too.","file":"mdl/backend/modelsdk/widget_custom_find.go","fix":"Convert at the backend boundary with the package's existing v2ToV1BSON helper, so RawType/RawObject always hold v1 bson.D — the currency sdk/mpr established and callers assert. Verified by extracting all 6 templates byte-for-byte identically to the pre-change binary (1.2MB datagrid.json included); reverting the conversion fails the new test with 'RawType is bson.D, want v1 bson.D'.","insight":"An `any` field crossing an engine boundary can carry the RIGHT type name and the WRONG package, and the error message will look like a tautology. When a type assertion fails with identical type names on both sides, the question is which import path each came from, not what the type is — the two BSON drivers coexist in this repo on purpose (modelsdk is v2, sdk/mpr and the CLI are v1) and widget_pluggable_write.go's v2ToV1BSON already existed for the write direction. A cast written to silence that compile/assert error panics at runtime instead. Two process notes from the same change. (1) GREP FOR AN EXISTING IMPLEMENTATION BEFORE WRITING ONE: the walker had been in modelsdk/mpr all along (FindAllCustomWidgetTypes + collectCustomWidgets, and it populates UnitName/WidgetName which a fresh implementation would omit); only the backend wiring was missing, which is exactly what 'this should be unreachable' in the unimplemented error meant. (2) 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 thing to update is the unreachableUnimplemented map in unimplemented_reachability_test.go, which fails loudly if a listed method becomes implemented."} +{"area":"mdl/backend","date":"2026-09-15","symptom":"Phase 4a took sdk/mpr from 27 importers to 0, but nothing stopped the count from creeping back — there was no build or test guard, only the plan document and a habit.","cause":"The invariant lived in prose. A single new `import \"github.com/mendixlabs/mxcli/sdk/mpr\"` compiles, passes every test, and reintroduces exactly the blind spot Phase 4a existed to close: the unimplemented-method census in mdl/backend/modelsdk lists methods with NO implementation, so a caller reaching one through a concrete *sdk/mpr.Reader never appears in it. That is what hid project_tree.go's 36 semantic reads (#477) and cmd_extract_templates.go's FindCustomWidgetType (#484) until each was found by hand.","file":"mdl/backend/sdkmpr_import_guard_test.go","fix":"TestNothingImportsTheLegacyEngine parses every .go file's imports (go/parser, ImportsOnly) and fails naming any file that imports sdk/mpr, with the remedy in the message. Controlled by dropping a one-line file importing sdk/mpr into examples/ — it fails and names the file.","insight":"A zero-count invariant needs TWO positive controls or it passes vacuously forever, and the failure mode is silent by construction: a walk rooted at the wrong directory, a skipped-dir rule that is too broad, or an import-parsing mistake all report '0 importers' and read as success. So assert (1) a plausible number of files was actually scanned (here >500; it sees 2551) and (2) the detector can see imports AT ALL, by counting a package the repo definitely does import (mdl/backend, 120 files). Only then does 0 mean zero. This is scripts/check-tunnel-deps.sh's pattern — it asserts chisel IS in the linux graph before asserting it is absent from windows/darwin — and the same reasoning as a bug-fix control: a test that only ever passes has not been shown to detect anything. Practical note: skip sdk/mpr's own directory by comparing the path to the repo root rather than by basename, or a directory named mpr elsewhere is skipped too."} diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index d4d727be65..64b2543910 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -607,13 +607,23 @@ {"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