diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index 1f47e0c3f9..3552954e5e 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -574,6 +574,8 @@ {"area": "mdl/executor", "date": "2026-09-09", "symptom": "A widget-describe change measured as 'zero rules lost' had in fact dropped SIX visibility rules from Combo box — the widget the work was justified by. The before/after sweep enumerated widgets from `.mxcli/widgets/*.def.json` (33 of them) but `mxcli widget describe` also serves widgets straight from their `.mpk`, and Combo box has no def.json. The real describable set is 42; the missing 9 included the one that mattered.", "cause": "The sweep's widget list was derived from an artifact of the pipeline (generated def.json files) rather than from the capability under test (what `widget describe` can describe). Nothing compared the two lists, so the sample silently excluded a whole class — the same 'two lists, nothing comparing them' shape as the defects being fixed.", "file": "mdl/executor/editorconfig_extract.go, mdl/executor/editorconfig_shapes_test.go", "insight": "Derive a sweep's population from the CAPABILITY, never from a convenient artifact, and state the population in the claim: 'zero rules lost across the 33 widgets carrying a def.json' would have been true and would have invited the question. A second trap immediately after: the regression test written to lock the fix used a hand-written editorConfig snippet of the same APPARENT shape, and it passed with the fix reverted — the nesting that triggers the drop is three levels deep and specific, so the synthetic case was never flagged conjunctive and the test proved nothing. Pointing the test at the real committed .mpk made the control fail with all six names. Rule of thumb: when a defect was found in real vendor input, the regression test takes the real input; a reconstructed minimal case must be shown to fail without the fix BEFORE it is trusted, and here it did not. The fix itself is the policy that should have been there from the start: conjunction support may WITHHOLD a rule the extractor never produced before (emitting one conjunct over-fires), but may never drop one the older vocabulary already lifted — that rule's accuracy is unchanged by the new work.", "refs": ["mendixlabs/mxcli#1036"]} {"area": "mdl/executor", "date": "2026-09-09", "symptom": "Six of Combo box's editorConfig hide-rules could not be lifted with their full condition, and its coverage counter sat at 21 of 32. The rules sit inside `\"association\"===t.optionsSourceType && ( … )`, itself the ELSE branch of a ternary inside `\"context\"===t.source ? ( … )` — a chained ternary, where each branch's BODY is parenthesised but each branch's CONDITION is not.", "cause": "groupGuard read the condition of a group opened after `&&`/`||` with trailingExpr, which stops at a STATEMENT separator. A chained ternary contains none, so it returned the whole `A ? (…) : B` expression as the group's condition. That is not a comparison, guardToCondition refused it, and enclosingGroupConditions reported the chain unreadable.", "file": "mdl/executor/editorconfig_extract.go (groupGuard, operandBefore)", "insight": "The characterisation written into the PR body — 'ternary chains without parentheses, which the outward walk does not traverse' — was WRONG, and instrumenting the walk rather than re-reading it is what showed so: the walk reaches these groups fine; the failure is guard EXTRACTION at the group, one function away. A one-line ceiling written from reasoning is worth re-deriving before anyone builds on it. The fix is not a straight swap to lastGuardExpr, which bounds at `{` and hands back a fragment with an unbalanced `}` where the expression follows a block (ProgressCircle's ternary follows a whole switch); take lastGuardExpr's answer only when it stopped at an INSIDE-expression boundary (`:`, `?`, `,`) and fall back to trailingExpr otherwise. Also a measurement note: the 'before' number quoted from an earlier session (16 of 32) was stale — the branch had been restarted from a main that already carried the previous fix, so the real baseline was 21. Re-measure the baseline in the tree you are actually editing rather than quoting a figure from memory.", "refs": ["mendixlabs/mxcli#1036"]} {"area": "mdl/executor", "date": "2026-09-09", "symptom": "`alter page P { set NoSuchProperty = 10 on dgProducts; }` and `set PageSize = 12 on noSuchWidget;` both passed `mxcli check -p app.mpr --references` (exit 0, \"All references valid\") and were then refused by `exec`: `pluggable property \"NoSuchProperty\" not found` / `widget \"noSuchWidget\" not found`. exec applies statements one at a time, so the script had already written everything before the typo", "cause": "ValidateWidgetProperties resolves the properties of widgets a statement CARRIES \u2014 CREATE PAGE's tree, and ALTER's InsertWidgetOp/ReplaceWidgetOp trees. SetPropertyOp carries no widget: it names one already stored, so its property can only be resolved against the DOCUMENT, which that pass never opens. Same inversion validate_alter_target.go closed for the ALTER's target document, one level further in", "file": "`mdl/executor/validate_alter_set.go` (new), `mdl/backend/pagemutator/probe.go` (new)", "insight": "**Do not re-derive what a mutation accepts \u2014 run the mutation.** The vocabulary of an ALTER SET is partly a switch in `setRawWidgetPropertyMut` and partly the STORED widget's own PropertyTypes, which belong to whatever widget package the project installed; no registry in this repo can state it for an arbitrary project. So the check opens the document, runs the real setter against a throwaway deep copy (`Mutator.Probe`, whose `Save` is refused), and keeps only the error \u2014 check and exec cannot drift because there is one resolver. The author also gets exec's exact wording from the pre-flight. **Two false-positive sources, both measured, both silence rather than a finding**: a page the script CREATEs (nothing stored \u2014 skip, and do not even open it) and a widget an INSERT in the same script adds. The second cannot be a name match: a DataGrid 2 column is inserted as `colBrand` and addressed as `Brand` (derived from the bound attribute), so the rule is `ResolvesTarget` \u2014 suppress only when the document does not carry the target AND the script adds widgets to that document. **Gating on an optional interface assertion, not on backend.PageMutator**, keeps the pass off the MCP mutator, which has no pluggable path and would have reported its own difference as the author's mistake. Controls: 4 casings of a real property pass (a case-sensitive pre-flight would have re-broken #1069); insert-then-set passes check AND exec in both the same-statement and across-statement forms; 17 ALTER SET examples in mdl-examples show 0 new errors vs the baseline binary; the project's files are byte-identical (md5 over .mpr + mprcontents) after five check runs. Revert control: stubbing the pass makes the three gap tests fail", "refs": []} +{"area": "mdl/executor", "date": "2026-09-09", "symptom": "`mxcli widget describe COMBOBOX -p app.mpr` said nothing about 23 properties the widget's editor hides, and its generated MDL example offered two database-only properties (`optionsSourceDatabaseCaptionType`, `optionsSourceDatabaseCustomContentType`) for a combobox with `source: 'context'`, where the editor shows neither. The coverage counter read \"24 of 32 editor hide-rules recognized\" and did NOT move, because those calls were recognized — only their property lists were short", "cause": "`hideTargetKeys` read a hide call's property list by trimming a leading `[` and a trailing `]` off the argument. Combo box builds its biggest lists by concatenating module-level arrays onto the literal one — `hidePropertiesIn(e, t, [\"a\",\"b\"].concat(N))`, where `N` is ten database properties — so the trim never found the `]` (the argument ends in `)`), the top-level split stopped at the first element boundary, and every concatenated name was dropped silently. The call still incremented `stats.Recognized`, so the counter reported full coverage of a list it had read a third of", "file": "`mdl/executor/editorconfig_extract.go` (`stringArrayConsts`, `stringArrayMembers`, `concatMembers`; `hideTargetKeys` takes the consts map)", "insight": "**A coverage counter that counts CALL SITES hides a defect in what each call contributed.** \"Recognized\" meant \"I read the condition\", not \"I read the properties\", and the two came apart at exactly the calls that matter most — the branch-level ones hiding ten properties at a time, where a wrong answer is widest. What made it findable was not the counter but **the user-visible artifact**: the MDL example proposed properties the editor does not show, visible only by reading the example against the widget's own `editorConfig.js`. Measure the artifact, not the metric. **Resolution must withhold, not guess**: an argument that is not an inline string array or a known identifier (`.concat(n(b.static))` — a call on a computed key) makes the whole chain unresolvable and the literal keys stand alone; an identifier bound twice to different arrays is dropped, because the minifier reuses short names across scopes. Each of those three refusals needed its own **stubbed control** to show it was load-bearing — withhold-don't-guess is only real if a test fails when the guard is removed. **Measure lost rules as a SET, not a line diff**: `diff` on the sorted rule block reported five false losses that were re-ordering from the 23 insertions. Measured over the 42 describable widgets (population from the `.mpk` files, NOT the 33 `.def.json` — Combo box has none): 332 → 355 rules, 0 lost, 0 per-widget regressions, and the only example block that changed was Combo box's, losing exactly the two wrong bindings. 14 concat sites exist in the fixture; 6 resolve, 8 stay unresolvable and keep today's literal-only behaviour. **Still open after this**: rules inside the `? (…)` database branch and the `: else` branches keep only their own guard and lose the branch condition, so e.g. `selectedItemsSorting hidden when optionsSourceDatabaseItemSelection ≠ \"Multi\"` over-fires for a context-source combobox", "refs": []} +{"area": "mdl/executor", "date": "2026-09-13", "symptom": "`customAllSelected: []` on a pluggable widget passed `mxcli check -p` with ZERO diagnostics, exec printed \"Created page\", and the stored page carried 0 `CustomWidgets$WidgetProperty` entries — then mxbuild reported 3x CE0642 \"Property 'All selected' is required.\" The same hole swallowed `attributes: []` on an object list and any scalar written for a container-typed property (mendixlabs/mxcli#1056)", "cause": "#999 built `[(k: v)]` into its own AST type so MDL-WIDGET27 could report it, but keyed the rule on that type — which the visitor only produces when the brackets contain a parenthesised entry. An EMPTY `[]` falls through to the generic `[expr, …]` branch and becomes an empty []string that no writer claims, which is #999's silent drop reached by a different spelling. A scalar never looked like a list at all, so nothing examined it", "file": "`mdl/executor/validate_widget_object_property.go` (`isEmptyListValue`, `isDeclaredContainer`, `containerKeyword` resolving child slots as well as object lists)", "insight": "**The reporter only reached the wrong spelling because the right one was rejected.** #1056 reads as one bug and is two: the slot syntax their own generated docs showed did not parse (fixed four days after their build by bca5466e, verified by building their exact commit 89824921 in a worktree — it reproduces their error verbatim and HEAD passes), and the wrong spelling stayed silent. Fixing only the parser would have left every author who had already worked around it with a page that builds broken. **Key on emptiness, never on the brackets**: `visible: [$x != '']`, `editable: [true]` and a filter's `attributes: [Name]` are all how MDL spells those properties, so a rule keyed on `[` would have broken the corpus — the guard test asserts those stay silent. **Gate the scalar case on the definition and the empty case on shape**: `p: 'x'` is the ordinary property form, so calling it wrong without knowing the property is container-typed would be a guess, while `p: []` writes nothing under every current writer and needs no project (which matters because `make check-mdl` runs without one). **A child slot and an object list spell their remedy differently** — `kw name { … }` vs `kw name (…)` — so one message for both would print an example that does not parse; that is why containerKeyword returns isSlot. Verified the printed remedy end-to-end: pasting it verbatim checks, execs, and adds 0 errors to a build where the pre-fix page still contributes its 3 CE0642 — the control pair in one mxbuild run. **Measurement trap hit again**: the first corpus sweep piped `2>/dev/null` while mxcli prints diagnostics to stderr, so it scored 0 of 547 on BOTH sides; the before-side positive control (the #999 file must fire) is what caught it", "refs": []} {"area": "mdl/executor", "date": "2026-09-09", "symptom": "`mxcli check … --references` reports \"Check passed!\" for a script whose `create module role M.Admin` names a role that already exists; `exec` then fails with \"module role already exists: M.Admin\" — after the statements before it have been written, leaving the project half-modified.", "cause": "The last gap of the class dbe5cc2a closed for documents. A module role is not a *document*, so `CreateModuleRoleStmt` was never in `stmtCreateInfo` and the sweep that compared stmtCreateInfo against `projectNameSets.setFor` never saw it — the guard tests pass because both lists agree on the types they contain, and a type absent from BOTH is invisible to a test that only compares them.", "file": "`mdl/executor/validate_duplicates.go` (`stmtCreateInfo`, `stmtDropInfo`, `setFor`, `friendlyDocType`), `mdl/executor/helpers.go` (`buildModuleRoleQualifiedNames`)", "insight": "**A guard that compares two lists cannot catch a type missing from both.** `TestEveryCreateDocTypeIsProjectChecked` reads stmtCreateInfo and setFor out of the Go source and is exactly right about the defect it was written for, and it stayed green through this one. The complement it cannot express — every statement `exec` refuses with \"already exists\" is classified at all — has no list to read; it was found by running the statements, not by reading code. **Match the exec predicate exactly, or a fixed under-report becomes a false positive.** Two exec behaviours had to be reproduced: CREATE OR MODIFY succeeds (the `idempotent` flag), and a plain CREATE on a role mxcli AUTO-PROVISIONED also succeeds — `execCreateModuleRole` adopts the caller's casing, rewrites references and returns nil — so `buildModuleRoleQualifiedNames` excludes any role carrying `autoDocumentRoleDescription`. Without that, `check` refuses a working script over a role the user never asked mxcli to create (`defaultDocumentAccessRoles` adds it on the first document in a role-less module). Verified live with its control: auto-provisioned role → check clean and exec prints \"already exists (auto-provisioned)\"; an authored role in the SAME module → reported. **`stmtDropInfo` is the other half**: without the DROP case, `drop module role X; create module role X;` reads as a conflict. Known limit, recorded rather than closed: exec matches role names case-insensitively (CE0123) and this set is keyed exactly, like every other doc type, so `create module role M.admin` against a stored `M.Admin` is still reported only by exec — an under-report of a rare spelling, not a false positive. Repro `mdl-examples/bug-tests/1067-duplicate-module-role.mdl` (a plain .mdl: the check needs a project, and `make check-mdl` runs check without one). Issue mendixlabs/mxcli#1067", "refs": ["mendixlabs/mxcli#1067"]} {"area": "mdl/executor", "date": "2026-09-09", "symptom": "A microflow whose activity has a CUSTOM error handler comes back from `describe microflow` with the handler AND every activity in its branch missing. Output stays valid MDL, `mxcli check` passes, nothing on stderr \u2014 so describe\u2192edit\u2192exec deletes the handler from the model. Reported on a create-variable activity with \"custom with rollback\" (v0.21, Mx 11.12.3); reproduced on HEAD/11.14.0 with a pure mxcli round trip", "cause": "TWO STACKED DEFECTS. (1) `getActionErrorHandlingType` was a hand-maintained switch covering 17 of the 38 action types that store ErrorHandlingType. `emitActivityStatement` walks the error branch only when `hasCustomErrorHandler(errType)` agrees, so each of the 21 missing types lost the ENTIRE `on error { \u2026 }` block, not just the suffix \u2014 CreateObject and ChangeObject among them. (2) legacy only: 9 parse functions never read ErrorHandlingType off the BSON, so the value was gone before the describer was asked. Fixing (1) alone left legacy still broken, which is how they hid each other", "file": "`mdl/executor/cmd_microflows_show_helpers.go` (`getActionErrorHandlingType` \u2192 new `actionErrorHandlingField`); `sdk/mpr/parser_microflow.go`, `sdk/mpr/parser_microflow_actions.go` (9 parsers); grammar+visitor+builder for 8 statements; `mdl/executor/validate_microflow_error_handling.go` (MDL076 table, new MDL077)", "insight": "**Count the gap before fixing the instance** \u2014 the reported activity was 1 of 21, measured by diffing the switch's cases against the action types declaring the field. **Replace the enumeration, do not extend it**: the list had already been patched per-instance (#863) and silently regrew, so it is now a reflection lookup on the `ErrorHandlingType` field, with `RestOperationCallAction` the ONE deliberate exclusion (Mendix rejects a custom handler there, CE6035). Actions embed model.BaseElement, which has no such field, so no promoted field is picked up by accident. **The trap: fixing the describer alone makes things WORSE.** 8 statement forms had no `onErrorClause` in the grammar \u2014 `declare` (the reporter's own), `set`, `change`, `log`, `show page`, `close page`, `show message`, `validation feedback` \u2014 so DESCRIBE began emitting `declare $name String = 'v' on error { \u2026 };`, which fails to parse (`mismatched input 'on' expecting ';'`). Trading a silent drop for a broken script is not a fix; the grammar was extended instead. **Measured on 11.14.0, and the result is not guessable**: all 8 accept a custom handler (0 errors), but for `on error continue` create-VARIABLE and change-VARIABLE are fine while change-OBJECT, log, show page, close page, show message and validation feedback are CE6035 \u2014 now in MDL076's deny-list, whose comment claiming Log/Change were \"unreachable from a script\" this change invalidated. New MDL077 refuses `on error` on the list-operation/aggregate forms of `set`, which genuinely have no ErrorHandlingType in the metamodel \u2014 one MDL keyword spanning activities that can and cannot hold the clause. **A non-terminating handler causing CE0108 is NOT a bug**: the branch merges back and a later variable is out of scope on the error path; Studio Pro reports the same. Controls: revert the lookup \u2192 all 7 describer cases fail naming the dropped branch; revert one parser \u2192 the legacy case fails; a no-clause microflow must still render NO suffix (#840 in reverse). Repro `mdl-examples/bug-tests/microflow-1078-error-handler-roundtrip.mdl`; describe\u2192exec\u2192describe byte-identical and reported \"Unchanged microflow\"", "refs": ["#1078", "#863", "#840"], "ce": ["CE6035", "CE0108"]} {"area": "mdl/executor", "date": "2026-09-10", "symptom": "CI `make test-integration` fails where `go test ./...` is green: `TestMxCheck_DoctypeScripts/02b-nanoflow-examples.mdl` (both engines) and `/03-page-examples.mdl` report **CE6035 \"Error handling type is not supported\"** on 11 activities \u2014 every un-annotated Change object / Log message / Validation feedback / Close page in a NANOFLOW. Self-inflicted while fixing #1078", "cause": "Eight builders were switched from `fb.ehType(nil)` to `explicitErrorHandling(fb, s.ErrorHandling)` on the action. `explicitErrorHandling` returns EMPTY for \"no clause\", and the writers turn empty into a literal `\"Rollback\"` via orDefault \u2014 but `fb.ehType(nil)` is CONTEXT-DEPENDENT and returns **Abort** in a nanoflow. So every un-annotated nanoflow activity silently changed from Abort to Rollback, which mxbuild rejects", "file": "`mdl/executor/cmd_microflows_builder_actions.go` + `cmd_microflows_builder_calls.go` (8 sites reverted to `fb.ehType(s.ErrorHandling)`); `mdl/executor/nanoflow_validation.go` (`getErrorHandling`)", "insight": "**The same helper is correct for one action and wrong for its neighbour, and the difference is what the code did BEFORE.** `explicitErrorHandling` is right for Retrieve/Delete (#1020-era): their writers emitted a hardcoded `\"Rollback\"` that those two actions accept in every flow flavour, so empty\u2192Rollback is a no-op. It is wrong wherever the builder already supplied a *context-dependent* default \u2014 empty discards the flow flavour. The rule: before replacing a default-supplying expression, ask what the OLD expression returned in every context, not just the one you are testing. Its own doc comment names Abort/nanoflow and I still missed it, because I was reading it as \"the safe choice\" rather than \"the choice that preserves THIS call site's prior value\". **`go test ./...` does not run this suite** \u2014 the mx-check round trips are behind `-tags integration` (`make test-integration`, ~30 min), so a green unit suite says nothing about whether mxbuild still accepts what mxcli writes; run it before pushing anything that touches a serialized default. Regression test `TestAuthorOnError_NanoflowKeepsAbortWithoutAClause` (control: the same statements in a microflow must NOT become Abort) \u2014 a unit test, so it catches this in seconds instead of 30 minutes. A second, quieter gap in the same change: `getErrorHandling` in nanoflow_validation.go gates the walk that looks for disallowed actions INSIDE a handler body, so the eight new statements had to be added there too or a Java action nested in `declare \u2026 on error { \u2026 }` goes unreported (test carries a `commit` control, since that entry point is `validateNanoflowBody`, NOT the exported `ValidateNanoflowBody`, which is a different check)", "refs": ["#1078"], "ce": ["CE6035"]} @@ -604,3 +606,9 @@ {"area": "mdl/executor", "date": "2026-09-13", "symptom": "A boundary event path written from MDL fails the build with CE0105 'Call microflow cannot be the last object of a flow, it should end with a jump or end activity' (interrupting), or builds at 0 errors and the runtime refuses to start: 'Expected the flow to end with an end event' (non-interrupting). After the fix, `create or modify` of the same workflow was refused by the dropped-construct guard", "cause": "Mendix ends every boundary event path with `EndOfBoundaryEventPathActivity`; mxcli never wrote one, and MDL has no end statement. The rewrite guard then counted `$Type`s containing 'BoundaryEvent', which includes the marker, so a stored workflow with one event counted two", "file": "`sdk/workflows/workflow.go` (`EndBoundaryEventPath`), `mdl/executor/cmd_workflows_write.go` (`buildBoundaryEvents`), `mdl/backend/wfmutator/mutator.go` + `mdl/backend/mcp/workflow.go` (`InsertBoundaryEvent`), `mdl/executor/validate_workflow_rewrite.go` (`countRawBoundaryEvents`)", "insight": "**Same class as the parallel-split marker: a Studio Pro terminal marker that no MDL statement spells.** Grep the writers for every `EndOf*` type the reader knows before assuming one is the only one. The guard miscount was only visible by re-running the fixed script against the fixed workflow \u2014 a substring match on `$Type` is wrong the moment a sibling type shares the stem; match the suffix, and keep a control asserting the substring count so the test says why", "fix": "Append the marker unless the path ends in a jump or end-of-workflow (CE6692 otherwise); count boundary events by `strings.HasSuffix($Type, \"BoundaryEvent\")`"} {"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