From fb511893cdf980462f0d14c8e8b634b3533b8d8d Mon Sep 17 00:00:00 2001 From: Ako Date: Tue, 1 Sep 2026 07:12:39 +0000 Subject: [PATCH 1/2] docs(wiki): digest the mdl/executor findings into four bug-pattern pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bug-pattern layer had three pages, all synthesised on 2026-05-24 from the ~24 findings that existed then. mdl/executor alone now holds 248, and no page mentioned the area at all. Four classes, covering roughly 134 of the 248: - describe-round-trip-gaps (83 findings). DESCRIBE is a second implementation of MDL, written in the opposite direction and validated by nothing: mxbuild never sees its output, and a describer that drops a property still produces a valid model at 0 errors. Four shapes in increasing order of how long they survive — won't parse, silently drops, invents a clause nobody wrote, destroys structure. - unloadable-model-writes (12). Load failure is not validation failure: no CE code, no document named, and Studio Pro will not open the project. The recurring cause is a reference whose SHAPE is wrong, the recurring remedy is refusal in both passes, and `mx check` at 0 errors is not evidence the project opens. - silent-property-drop (22). A typo and a real-but-unrouted property fail identically, because the grammar accepts `Key: value` generically and a permissive allow-list turns every unrouted name into a silent drop. - check-mxbuild-drift (17). `mxcli check` is a model of mxbuild and drifts both ways. The false-positive direction stopped being cosmetic when exec began refusing on check errors. One correction made while syncing, and recorded in the log: the silent-property-drop draft named a source file that does not exist and claimed the remedy is derived rather than enumerated. Reading the real validate_widgets.go showed both halves exist and for good reason — derived for pluggable widgets, a guarded hand-maintained union for built-ins, which have no .mpk to subtract from. Declaring a source without reading it is exactly what the sources: discipline exists to prevent, and it produced a wrong claim on the first try. Seed table and SYNC_LOG updated, per the maintain-wiki contract. Co-Authored-By: Claude Opus 5 --- .claude/skills/maintain-wiki.md | 4 + CLAUDE.md | 2 +- docs-wiki/SYNC_LOG.md | 4 + docs-wiki/bug-patterns/check-mxbuild-drift.md | 88 +++++++++++++++ .../bug-patterns/describe-round-trip-gaps.md | 100 ++++++++++++++++++ .../bug-patterns/silent-property-drop.md | 92 ++++++++++++++++ .../bug-patterns/unloadable-model-writes.md | 89 ++++++++++++++++ 7 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 docs-wiki/bug-patterns/check-mxbuild-drift.md create mode 100644 docs-wiki/bug-patterns/describe-round-trip-gaps.md create mode 100644 docs-wiki/bug-patterns/silent-property-drop.md create mode 100644 docs-wiki/bug-patterns/unloadable-model-writes.md diff --git a/.claude/skills/maintain-wiki.md b/.claude/skills/maintain-wiki.md index df3ffd0ba..06c4b3d4c 100644 --- a/.claude/skills/maintain-wiki.md +++ b/.claude/skills/maintain-wiki.md @@ -143,6 +143,10 @@ isn't better served by an existing page or a different doc artifact. | `bug-patterns/bson-numeric-width.md` | bug-pattern | int32/int64 mismatches (links #583, #585 findings) | | `bug-patterns/visitor-wiring-gaps.md` | bug-pattern | parsed-but-not-stored (links #393 finding) | | `bug-patterns/widget-type-object-drift.md` | bug-pattern | CE0463 family | +| `bug-patterns/describe-round-trip-gaps.md` | bug-pattern | DESCRIBE as a second, unvalidated MDL implementation: won't-parse / drops / invents / destroys | +| `bug-patterns/unloadable-model-writes.md` | bug-pattern | writes that break LOAD rather than validation — no CE code, whole project down | +| `bug-patterns/silent-property-drop.md` | bug-pattern | a property parses, passes every check, and never reaches the model | +| `bug-patterns/check-mxbuild-drift.md` | bug-pattern | `mxcli check` as a model of mxbuild, drifting in both directions | ## Adding a new page diff --git a/CLAUDE.md b/CLAUDE.md index adce9e034..b3ae251b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -564,7 +564,7 @@ mxcli uses a layered documentation system — each artifact type has a single ca **ADRs are immutable once accepted.** Supersede with a new ADR rather than editing in place. Conventions and template in [`docs/13-decisions/README.md`](docs/13-decisions/README.md). -**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest is currently thin (3 pattern pages against 630 findings, none re-synced since the initial synthesis), so a pattern miss means "not yet digested", not "not seen before". +**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest is currently thin (7 pattern pages against 631 findings; `make digest-status` reports the gap), so a pattern miss means "not yet digested", not "not seen before". **The wiki is synthesized, not stated.** It frames and connects across the other artifacts — it never restates content that has a canonical home. Rules and seed page list in [`.claude/skills/maintain-wiki.md`](.claude/skills/maintain-wiki.md). diff --git a/docs-wiki/SYNC_LOG.md b/docs-wiki/SYNC_LOG.md index 56c9ea484..f0fb1e61b 100644 --- a/docs-wiki/SYNC_LOG.md +++ b/docs-wiki/SYNC_LOG.md @@ -31,3 +31,7 @@ not capture, because sources are upstream of the commit. | 2026-06-10 | models/ped-mutation-constraints.md | docs/03-development/PED_MCP_CAPABILITIES.md, mdl/backend/mcp/workflow.go, mdl/backend/mcp/domainmodel.go, mdl/backend/mcp/microflow.go | New page (outside seed list): synthesises the counter-intuitive PED invariants (simplified constructors, leaf-only sets, array-edit rules, acceptance≠validity, verify-exact-payload) learned empirically across the MCP increments; capability doc holds the facts, this holds the mental model | | 2026-08-11 | architecture/mpr-read-write.md | sdk/mpr/reader.go, sdk/mpr/writer_core.go, sdk/mpr/writer_units.go, sdk/mpr/parser.go, modelsdk.go, modelsdk/mpr/writer_core.go, modelsdk/canon/canon.go, modelsdk/canon/identity.go, docs/13-decisions/0008-identity-and-idempotence.md | Writes are now conditional (ADR-0008) and the page described only one engine. Added the two-engines/one-choke-point framing and the canonical-comparison model. **Corrected a pre-existing inaccuracy**: the page claimed v2 writes stage through WriteTransaction with temp files — in sdk/mpr that type has no callers and the ordinary path is a direct WriteFile; temp-file staging is the modelsdk engine. Added modelsdk sources; dropped sdk/mpr/writer_widgets.go (the BSON-convention claim is fully grounded in parser.go, and widget specifics belong to the widget-engine page) | | 2026-08-11 | models/element-identity.md | docs/13-decisions/0008-identity-and-idempotence.md, modelsdk/canon/identity.go, CLAUDE.md, .claude/skills/fix-issue.md | New page (outside seed list, added to seed table): the $ID / GUID / StableId distinction had no wiki home and is the invariant that cost a reverted PR. Does not fit storage-vs-qualified-names ($Type naming) or association-pointers (pointer direction); mpr-read-write is a pipeline narrative, this is the mental model. ADR holds the decision and evidence, this holds the framing | +| 2026-08-31 | bug-patterns/describe-round-trip-gaps.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (83 describe-related records), mdl/executor/cmd_workflows.go, mdl/executor/cmd_pages_describe_pluggable.go | New page (added to seed table). Largest class in the area — 83 of 248 executor findings. Frames DESCRIBE as a second MDL implementation with no oracle (mxbuild never sees its output), and the four shapes: won't-parse, silently-drops, invents, destroys. Triggered by `make digest-status` reporting 607 of 631 findings undigested | +| 2026-08-31 | bug-patterns/unloadable-model-writes.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (12 load-failure records), mdl/executor/validate_workflow.go, mdl/executor/validate_association_module.go | New page (added to seed table). Separates LOAD failure from validation failure: no CE code, no document named, whole project down. The recurring remedy is refusal in both passes, and `mx check` at 0 errors is not evidence the project opens | +| 2026-08-31 | bug-patterns/silent-property-drop.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (22 silent-drop records), mdl/executor/validate_widget_props.go, mdl/executor/validate_widgets.go, modelsdk/widgets/definitions/ | New page (added to seed table). Why a typo and a real-but-unrouted property fail identically. **Corrected while syncing**: the first draft named a non-existent `validate_widget_props.go` and claimed the remedy is derived rather than enumerated — reading the real file showed both halves exist and for good reason, derived for pluggable widgets (mpk keys minus mapped keys) and a guarded hand-maintained union for built-ins, which have no mpk to subtract from | +| 2026-08-31 | bug-patterns/check-mxbuild-drift.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (17 check-gap records), mdl/executor/validate_program.go, docs/11-proposals/PROPOSAL_check_mxbuild_gap_heuristics.md | New page (added to seed table). Both directions of drift, and why a false positive stopped being cosmetic once exec began refusing on check errors | diff --git a/docs-wiki/bug-patterns/check-mxbuild-drift.md b/docs-wiki/bug-patterns/check-mxbuild-drift.md new file mode 100644 index 000000000..5a0ea6517 --- /dev/null +++ b/docs-wiki/bug-patterns/check-mxbuild-drift.md @@ -0,0 +1,88 @@ +--- +title: When `mxcli check` and mxbuild Disagree +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-executor.jsonl + - mdl/executor/validate_program.go + - docs/11-proposals/PROPOSAL_check_mxbuild_gap_heuristics.md +--- + +> **Do not duplicate**: the rule catalogue and its rationale live in +> `PROPOSAL_check_mxbuild_gap_heuristics.md`; each rule's exact predicate and CE +> number live in the findings and in `mdl/executor/validate_*.go`. This page +> describes the failure class on both sides. + +## What this is + +`mxcli check` is a **model of mxbuild**, not a port of it. It runs before a +build — often before a project even exists — so every rule is a prediction, and +predictions drift in two directions: + +- **A gap.** `check` passes, `exec` writes, and the build then fails with a `CE` + code. The user has already changed their project before anything told them. +- **A false refusal.** `check` rejects MDL that mxbuild accepts at 0 errors. + +Both appear repeatedly in the executor findings. The second used to be a mere +annoyance and is not any more: since `exec` began refusing scripts whose check +reports an **error**, a false positive is a blocker rather than a warning. + +## How it fits + +**Verify a rule against mxbuild, not against intuition.** Rules have been added +on a plausible reading of a CE code and later measured to be wrong: one flagged +format functions over association navigation as a build error and was deleted +outright; another demanded a `return` on every path from a microflow that builds +cleanly, because the builder synthesises one. A rule that predicts mxbuild has to +be checked against mxbuild. + +**A rule reproduced from failing neighbours can still be mis-premised.** The +deleted rule was written after reproducing several failures that shared the +construct it flagged — and the construct was not the cause. They shared a +*different* hidden defect, in the write path. When a write-path fix lands, +re-validate the checks that were derived from the same symptoms; a +correlation-based rule outlives the correlation. + +**Mirror the builder's own condition rather than inventing a second one.** Where +`check` predicts something the builder decides, the two conditions must be the +same expression, not two readings of the same intent — otherwise they drift on +the first edit to either. + +**Run a new rule over `mdl-examples/` before wiring it up.** One candidate hit 4 +of 374 example files and 3 of the hits were false positives, because the rule +read the AST while the outcome depended on what the builder synthesises. The +corpus is the cheapest false-positive test available. + +**Severity is the design decision, not an afterthought.** A rule whose vocabulary +cannot be proven complete — anything about widget properties, or about a name +that might be legal in a context the rule cannot see — must be a *warning*, or +it trades a silent defect for a false refusal. That only works if warnings +genuinely do not block: an exec guard written as `if len(violations) > 0` makes +every warning fatal, which is how a warning-severity rule silently became a +blocker for everything it touched. + +**Close a gap in both passes or in neither.** A check-time rule does not protect +a script that runs `exec` directly, and `--no-check` exists. Both call the same +function so they cannot diverge — the convention exists because they did. + +**When probing a gap, probe every sibling.** A missing reference check is almost +never one reference kind: the workflow case turned out to have *nothing* +validated — called microflow, called workflow, user task page, targeting +microflow, context entity, and the workflow's own module. Fixing the reported one +leaves the class open and the next report looks new. + +**Grade by consequence.** The same statement can produce a recoverable build +error or an unopenable project depending on one detail — a qualified-but-missing +name versus an unqualified one. Those want different gates: the first needs a +project and belongs in the reference check; the second is a static property of +the statement and can be refused with no project at all, which is also what makes +it testable as a `.fail.mdl` fixture in CI. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — every rule's + predicate, its CE code, and the measurement behind it +- [[unloadable-model-writes]] — the failures that have no CE code because the + build never gets that far +- `PROPOSAL_check_mxbuild_gap_heuristics.md` — the design rationale for + predicting mxbuild at all diff --git a/docs-wiki/bug-patterns/describe-round-trip-gaps.md b/docs-wiki/bug-patterns/describe-round-trip-gaps.md new file mode 100644 index 000000000..3cf4ff73a --- /dev/null +++ b/docs-wiki/bug-patterns/describe-round-trip-gaps.md @@ -0,0 +1,100 @@ +--- +title: DESCRIBE Round-Trip Gaps +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-executor.jsonl + - mdl/executor/cmd_workflows.go + - mdl/executor/cmd_pages_describe_pluggable.go +--- + +> **Do not duplicate**: the per-construct fix recipes live in the findings +> (`grep -l describe .claude/skills/fix-issue/findings/*.jsonl`), the MDL syntax +> in `docs/01-project/MDL_QUICK_REFERENCE.md`, and the round-trip requirement in +> CLAUDE.md's PR checklist. This page describes the failure class only. + +## What this is + +`DESCRIBE` is a **second implementation of MDL**, written in the opposite +direction and validated by nothing. Every construct has a writer (MDL → BSON) +and a describer (BSON → MDL) built separately, and no mechanism forces them to +agree. This is the single largest class of defect in the executor: **83 of the +248 findings** for `mdl/executor` involve a describe path. + +The reason it accumulates is that the write path has an oracle and the read path +does not. `mxbuild` validates the *model*, and it never sees DESCRIBE output at +all — so a describer that drops a property produces a perfectly valid model, at +0 errors before and after. The only thing that notices is a person replaying the +output and finding their work gone. + +That matters most where the feature is used most. `describe → edit → exec` is +mxcli's copy operation, and `DESCRIBE LAYOUT` emitting re-executable MDL is +explicitly why there is no `COPY DOCUMENT` verb. A lossy describer is costliest +exactly when someone is trying to reuse work. + +## How it fits + +**Four shapes, in increasing order of how long they survive.** + +*Won't parse.* The emitter produces text MDL's own grammar rejects — a +reserved-word name emitted bare, an internal spelling (`call_microflow X`, +`ReadMode: CallMicroflow:…`) that no rule accepts, `Param = $v` where the +grammar wants `Param: $v`, a quote inside a string that was never doubled. Loud +and cheap: running `mxcli check` on the output finds it. The recurring cause is +that **storage form is not input form** — whenever DESCRIBE prints a value read +back from the backend, the question is whether the *parser* accepts that +spelling, and nothing else in the toolchain asks it. + +*Silently drops.* A property is written correctly, present in the `.mxunit`, live +in the app — and absent from the description. The round trip deletes it. Output +parses, executes, and the model stays valid, so every automated signal is green. + +*Invents.* The describer emits a clause the author never wrote: `comment 'Review'` +on a jump whose caption was only ever a default, `on error rollback` on an +activity with no error handling, a bare `else` on a split that has none. Each +round trip accumulates another, so the model drifts toward the emitter's +defaults. The governing rule is an **asymmetry**: omitting a value the writer +re-derives is lossless, while emitting it is lossy in the direction that +matters — it puts something in the user's script that they did not write. When a +formatter renders an enum whose fallback value is also a legal authored value, +read-back cannot invert the write; render only the values that are never +defaults. + +*Destroys.* Rarest and worst — the round trip removes structure rather than +losing a field. A list view's specialization templates went 4 → 0; an accordion +group's contents vanished; a loop body was emitted empty because an annotation +sat to its left. `mx check` reported 0 errors on both sides of each. + +**The defect is usually the copy, not the case.** Describers get written +per-widget and per-container, so one lookup exists four or five times and some of +the copies are wrong. Patching the switch named in the report leaves the others to +drift again. A datasource means the same thing wherever it sits; so does an +action slot, and so does a text template. The fix that holds is one reader and one +renderer, with the type set taken from `generated/metamodel` rather than from the +copies. + +**Fixing one half is worse than the bug.** Where a describer has two defects at +once — say, quoting *and* a missing property — shipping the quoting fix alone +turns unparseable output into output that parses cleanly while silently dropping +something. That is strictly worse: a wrong page that validates. + +**The round trip is the only sound test**, and it has to be measured against the +model rather than the text. A describe-to-describe text diff matched exactly on a +page that had lost its datasource. `mx check` on the source proves nothing, +because the lossy model is valid. Counting failures before and after hid an +equal-sized swap of which pages failed. What works: describe → exec → describe +byte-identical, plus `mx check` on the *rebuilt* model. + +One consequence worth knowing: other code re-parses DESCRIBE output. +`use building block … (datasource: …)` matches against the rendered form, so +fixing a renderer can break a consumer that never reads the model. Grep for +callers of the emitter before changing what it emits. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the per-construct + recipes; `grep -l describe *.jsonl` reaches this class +- [[widget-type-object-drift]] — the neighbouring class where the *written* widget + is wrong rather than the described one +- `.claude/skills/verify-in-runtime.md` — for the cases where neither the model + nor its description is the thing that is wrong diff --git a/docs-wiki/bug-patterns/silent-property-drop.md b/docs-wiki/bug-patterns/silent-property-drop.md new file mode 100644 index 000000000..c173f19b7 --- /dev/null +++ b/docs-wiki/bug-patterns/silent-property-drop.md @@ -0,0 +1,92 @@ +--- +title: Properties That Parse but Never Persist +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-executor.jsonl + - mdl/executor/validate_widgets.go + - modelsdk/widgets/definitions/ +--- + +> **Do not duplicate**: the per-widget property tables live in +> `sdk/widgets/templates/` and the generated `.def.json`; the individual aliases +> and rule IDs live in the findings. This page describes why the class exists. + +## What this is + +An MDL property is accepted, `mxcli check` passes, `exec` reports success, +`mx check` reports 0 errors — and the value never reached the model. The widget +renders, and does nothing. Twenty-two of the executor findings are this. + +The outcome is indistinguishable from a typo, and that is the core of the +problem: `Contnet:` and a real-but-unrouted `DynamicCellClass:` failed in +exactly the same silent way, so nothing in the pipeline could tell the author +which one they had written. + +## How it fits + +**Parsing is not persisting.** A widget property crosses several independent +hops — grammar, AST, the executor's property switch, the widget definition's +mapping, the builder, the codec — and falling off any of them produces the same +nothing. The grammar cannot help: `Key: value` is generic by design, because the +parser has no idea which keys a given widget routes. + +**The permissive allow-list is the usual mechanism.** Widget property names were +deliberately not rejected, so that `Label:` and `Class:` on an unfamiliar widget +would not produce false errors. That choice quietly converts every *unrouted* +name into a silent drop, and the cost stays invisible until a build error names a +property the author believed they had set. + +**Three sources of truth compete, and the wrong one usually wins.** Pluggable +widget mappings are generated into `.def.json` from the `.mpk`, but a +hand-written built-in definition beats the generator — so fixing the generator +alone changes nothing for the widgets that have one. And the generated defs are +version-stamped and cached per project: a code-only alias addition is invisible +until `WidgetDefGeneratorVersion` is bumped and the stale defs regenerate. + +**Where a vocabulary can be derived, derive it.** For pluggable widgets the +*known but unmapped* set falls out of two artifacts already present: every +property key the `.mpk` declares, minus everything the generated definition maps. +That needs no per-widget knowledge and cannot go stale, and it turns one silent +outcome into a three-way answer — an unrecognised key gets a "did you mean" +warning, a recognised-but-unmapped key gets "this will be dropped, set it in +Studio Pro", and a mapped key stays quiet. + +**Where it cannot, enumerate generously and guard the list.** Built-in widgets +have no `.mpk` to subtract from, so their vocabulary is a hand-maintained union — +grammar keyword properties, every key the builders consume, and every property +`describe page` can emit. It is deliberately a union across *all* widget types +rather than per-type, because a per-type list would produce false warnings, and +the describe half is included so a describe → create round trip never warns about +its own output. A manually maintained list is a maintenance risk, which is why it +carries a drift test rather than a promise. + +**Warn; do not reject.** Neither the pluggable nor the built-in vocabulary can be +proven complete, so an error would trade silent drops for false refusals. The +same reasoning appears wherever mxcli names a *specific* alternative — those +lists are deliberately explicit rather than inferred, because inferring them +would mean reimplementing the engine's dispatch and getting it wrong in the other +direction. + +**Wire the read at the same time as the write.** DESCRIBE is how a dropped +property gets *found*, so a write fix without its describe counterpart leaves the +model holding a value that the tool reports as absent — the same lossy round trip +wearing the right answer. Several findings here are the follow-up half of a fix +that shipped write-only. + +**A half-wired feature is worse than a missing one.** When the grammar already +accepts a form, scripts using it look correct and produce models without it. +Before assuming a spelling works, follow the whole chain: AST → model → both +writers → both readers → describe. + +**And a check wired into nothing is invisible.** `exec` used to apply scripts +that `check` rejected outright; a validator that no pass calls reports no +violations and reads exactly like a clean project. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + aliases, rule IDs and widget-specific spellings +- [[describe-round-trip-gaps]] — the read-side half of the same failure +- [[widget-type-object-drift]] — when the property *is* written and the widget + definition is what disagrees diff --git a/docs-wiki/bug-patterns/unloadable-model-writes.md b/docs-wiki/bug-patterns/unloadable-model-writes.md new file mode 100644 index 000000000..be793e712 --- /dev/null +++ b/docs-wiki/bug-patterns/unloadable-model-writes.md @@ -0,0 +1,89 @@ +--- +title: Writes That Make the Project Unloadable +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-executor.jsonl + - mdl/executor/validate_workflow.go + - mdl/executor/validate_association_module.go +--- + +> **Do not duplicate**: the per-construct refusals and their CE numbers live in +> the findings; the storage-name and overlay rules are canonical in CLAUDE.md. +> This page describes the failure class and why it is graded differently from a +> build error. + +## What this is + +A Mendix model can be wrong in two ways that look nothing alike. **Validation** +errors — the `CE####` codes — are found by mxbuild in a model it has already +loaded successfully: one document is wrong, the rest of the project is fine, and +the message names the document. **Load** errors happen before validation starts. +The `.mpr` is structurally malformed, `mx check` dies with a .NET exception and +no error code, no document is named, and **Studio Pro will not open the project +at all**. + +Twelve of the executor findings are in the second category. They are the most +expensive defects mxcli has produced, because the blast radius is the whole +project rather than one page, and because the diagnostic is a stack trace. + +## How it fits + +**The recurring cause is a reference whose *shape* is wrong**, not a reference +that points at the wrong thing. Mendix reconstructs each stored property into a +typed identifier as it loads, and a value that cannot be parsed into that type +takes the loader down. The shapes seen so far: a one-qualifier member name +written where an attribute reference is expected (an attribute is bare or +`Module.Entity.Attribute`, never `Module.Name`); an unqualified entity name in a +generalization; a literal string where the property is a `ConstantIdentifier`; +an empty `DestinationEntity`; an index column pointing at a GUID that no longer +exists; a sequence flow dangling from a `break`; an association whose `ParentPointer` +addresses an element in another unit; a child appended to a list whose parent +type has no constructor taking that parent. + +**`mx check` passing is not evidence that the project opens.** mxbuild's +deserializer tolerates properties it does not recognise; Studio Pro resolves +every stored property against its type's property list and throws when there is +no match. A settings write with three wrong storage names built at 0 errors and +made the unit unopenable. Where a write touches preserved BSON, only a real +Studio Pro reference document settles the shape — which is the reasoning behind +CLAUDE.md's overlay rules. + +**The remedy is almost always to refuse, not to write something else.** In each +of these the tempting alternative turned out to be unavailable or harmful: +minting a Constant from a literal password would bake a secret into the model as +a design-time default; a cross-module association has no storage form at all +(neither `Association` nor `CrossAssociation` can express a by-name *parent*); +the correct container for a floating workflow annotation is not determinable from +the gen model. Refusing costs the user one statement; writing costs them the +project. + +**Refuse in both passes.** A check-time rule does not help a script that never +ran `check`, and `exec` is reachable directly — the lesson that has now been +relearned enough times to be a convention. Both call the same function, so they +cannot drift. + +**Grade the two failure modes separately when a statement can produce either.** +A qualified-but-missing name is recoverable: the project loads and the build +reports `CE1613`. The *unqualified* form of the same name makes the `.mpr` +unopenable. The first needs a project to detect and belongs in the reference +check; the second is a static property of the statement and can be refused with +no project at all — which is also what makes it catchable by a `.fail.mdl` +fixture in CI. + +**Diagnosis without Studio Pro.** The .NET stack names a setter, and that +setter's property is the lead — `EntityRefStep.set_AssociationId`, +`MprProperty.cs`, `ResolvePostponedProperties`. From there, dump the stored unit +and compare the property against a document Studio Pro wrote. Verifying a +storage-name hypothesis by swapping *only* the name is the cheap control: if the +error is byte-identical afterwards, the bug is somewhere else. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + refusals, their rule IDs and their reference documents +- [[element-identity]] — why a pointer's target and a GUID are different questions +- [[association-pointers]] — the `ParentPointer` / `ChildPointer` inversion behind + the cross-module association case +- `.claude/skills/debug-bson.md` — the workflow for comparing a written document + against a Studio Pro reference From 9438276ea69a6547b167be1e7779b3fd212ea593 Mon Sep 17 00:00:00 2001 From: Ako Date: Tue, 1 Sep 2026 07:24:59 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(wiki):=20finish=20the=20mdl/executor?= =?UTF-8?q?=20digest=20=E2=80=94=20five=20more=20bug-pattern=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second pass over the area. With the four from the previous commit, 205 of the 248 mdl/executor findings (83%) now belong to a named class. - platform-semantics-gaps (18). MDL is deliberately SQL-shaped and permissive; Mendix's expression language, XPath dialect, microflow variable model and entity rules are each narrower, with no syntactic marker. Also records that several reports in this class were measured and closed as correct behaviour — quoting escapes parser keywords and not platform-reserved names, `else` on a type split really is the empty flow, `create` really is non-idempotent. - duplicate-resolver-drift (23). The most common structural cause in the area, and the one whose symptoms look least alike: check vs exec, legacy vs modelsdk, write vs read, per-doctype copies. The tell is that the fix for the reported instance is obviously incomplete. - rewrite-drops-unauthored-state (13). Guard-don't-drop stated as a failure class: the loss is invisible at every checkpoint, the most expensive variant is identity rather than content, and a delete-then-create replace opts out of every protection there is. - flow-graph-geometry (10). A microflow is a graph with coordinates and mxcli generates both. Geometry fails ugly, wiring fails invalid, and a flow that looks redundant is often load-bearing. - integration-contract-drift (30). The one area where neither mxcli check nor mxbuild is an oracle, so a silent downgrade can return HTTP 200 and every signal reads as success. Every sources: path verified to exist and every [[wiki-link]] verified to resolve before committing — the check that caught a fabricated source in the previous pass. Seed table extended and five rows appended to SYNC_LOG.md. Co-Authored-By: Claude Opus 5 --- .claude/skills/maintain-wiki.md | 5 ++ CLAUDE.md | 2 +- docs-wiki/SYNC_LOG.md | 5 ++ .../bug-patterns/duplicate-resolver-drift.md | 78 ++++++++++++++++++ docs-wiki/bug-patterns/flow-graph-geometry.md | 72 +++++++++++++++++ .../integration-contract-drift.md | 73 +++++++++++++++++ .../bug-patterns/platform-semantics-gaps.md | 80 +++++++++++++++++++ .../rewrite-drops-unauthored-state.md | 67 ++++++++++++++++ 8 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 docs-wiki/bug-patterns/duplicate-resolver-drift.md create mode 100644 docs-wiki/bug-patterns/flow-graph-geometry.md create mode 100644 docs-wiki/bug-patterns/integration-contract-drift.md create mode 100644 docs-wiki/bug-patterns/platform-semantics-gaps.md create mode 100644 docs-wiki/bug-patterns/rewrite-drops-unauthored-state.md diff --git a/.claude/skills/maintain-wiki.md b/.claude/skills/maintain-wiki.md index 06c4b3d4c..fbb261879 100644 --- a/.claude/skills/maintain-wiki.md +++ b/.claude/skills/maintain-wiki.md @@ -147,6 +147,11 @@ isn't better served by an existing page or a different doc artifact. | `bug-patterns/unloadable-model-writes.md` | bug-pattern | writes that break LOAD rather than validation — no CE code, whole project down | | `bug-patterns/silent-property-drop.md` | bug-pattern | a property parses, passes every check, and never reaches the model | | `bug-patterns/check-mxbuild-drift.md` | bug-pattern | `mxcli check` as a model of mxbuild, drifting in both directions | +| `bug-patterns/platform-semantics-gaps.md` | bug-pattern | legal MDL, illegal Mendix — expression / XPath / variable-scope rules the grammar cannot carry | +| `bug-patterns/duplicate-resolver-drift.md` | bug-pattern | one question answered in two places, and the two disagree | +| `bug-patterns/rewrite-drops-unauthored-state.md` | bug-pattern | CREATE OR REPLACE losing what the statement did not mention | +| `bug-patterns/flow-graph-geometry.md` | bug-pattern | generated microflow coordinates and sequence-flow wiring | +| `bug-patterns/integration-contract-drift.md` | bug-pattern | OData / REST / mappings, where neither check nor mxbuild is an oracle | ## Adding a new page diff --git a/CLAUDE.md b/CLAUDE.md index b3ae251b7..ede6c1467 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -564,7 +564,7 @@ mxcli uses a layered documentation system — each artifact type has a single ca **ADRs are immutable once accepted.** Supersede with a new ADR rather than editing in place. Conventions and template in [`docs/13-decisions/README.md`](docs/13-decisions/README.md). -**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest is currently thin (7 pattern pages against 631 findings; `make digest-status` reports the gap), so a pattern miss means "not yet digested", not "not seen before". +**Bug findings are read in the opposite order from how they are written.** A fix *appends* one record to `.claude/skills/fix-issue/findings/.jsonl`; a diagnosis *starts* at `docs-wiki/bug-patterns/`, which digests those records into failure classes, and drills into the findings only for the specific instance. The findings are append-only evidence — grep them, or query them with DuckDB (`select … from 'findings/*.jsonl'`), never read them whole. The digest covers `mdl/executor` (12 pattern pages, 83% of that area's findings) and little else; `make digest-status` reports the gap per area, so a pattern miss means "not yet digested", not "not seen before". **The wiki is synthesized, not stated.** It frames and connects across the other artifacts — it never restates content that has a canonical home. Rules and seed page list in [`.claude/skills/maintain-wiki.md`](.claude/skills/maintain-wiki.md). diff --git a/docs-wiki/SYNC_LOG.md b/docs-wiki/SYNC_LOG.md index f0fb1e61b..b99fa30a6 100644 --- a/docs-wiki/SYNC_LOG.md +++ b/docs-wiki/SYNC_LOG.md @@ -35,3 +35,8 @@ not capture, because sources are upstream of the commit. | 2026-08-31 | bug-patterns/unloadable-model-writes.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (12 load-failure records), mdl/executor/validate_workflow.go, mdl/executor/validate_association_module.go | New page (added to seed table). Separates LOAD failure from validation failure: no CE code, no document named, whole project down. The recurring remedy is refusal in both passes, and `mx check` at 0 errors is not evidence the project opens | | 2026-08-31 | bug-patterns/silent-property-drop.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (22 silent-drop records), mdl/executor/validate_widget_props.go, mdl/executor/validate_widgets.go, modelsdk/widgets/definitions/ | New page (added to seed table). Why a typo and a real-but-unrouted property fail identically. **Corrected while syncing**: the first draft named a non-existent `validate_widget_props.go` and claimed the remedy is derived rather than enumerated — reading the real file showed both halves exist and for good reason, derived for pluggable widgets (mpk keys minus mapped keys) and a guarded hand-maintained union for built-ins, which have no mpk to subtract from | | 2026-08-31 | bug-patterns/check-mxbuild-drift.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (17 check-gap records), mdl/executor/validate_program.go, docs/11-proposals/PROPOSAL_check_mxbuild_gap_heuristics.md | New page (added to seed table). Both directions of drift, and why a false positive stopped being cosmetic once exec began refusing on check errors | +| 2026-08-31 | bug-patterns/platform-semantics-gaps.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (18 records), mdl/executor/validate_microflow_ce_gaps.go, mdl/executor/validate_microflow_loop_scope.go | New page (added to seed table). Second pass over mdl/executor. Frames the seam between a permissive SQL-shaped grammar and four narrower Mendix sub-languages (expressions, XPath, the microflow variable model, entity rules), and why some rules are safe to enforce unconditionally while builder-predicting ones are not. Notes that several reports in this class were measured and closed as correct behaviour | +| 2026-08-31 | bug-patterns/duplicate-resolver-drift.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (23 records), mdl/executor/validate_program.go, mdl/backend/backend.go | New page (added to seed table). The most common structural cause in the area: check vs exec, legacy vs modelsdk, write vs read, per-doctype copies. The tell is that the fix for the reported instance is obviously incomplete | +| 2026-08-31 | bug-patterns/rewrite-drops-unauthored-state.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (13 records), docs/13-decisions/0005-semantic-model-interface-currency.md, docs/13-decisions/0008-identity-and-idempotence.md | New page (added to seed table). Guard-don't-drop as a failure class rather than a decision: the loss is invisible at every checkpoint, the most expensive variant is identity rather than content, and delete-then-create opts out of every protection | +| 2026-08-31 | bug-patterns/flow-graph-geometry.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (10 records), mdl/executor/layout.go, mdl/executor/cmd_microflows_builder_control.go | New page (added to seed table). A microflow is a graph with coordinates and mxcli generates both; geometry fails ugly, wiring fails invalid, and a flow that looks redundant is often load-bearing | +| 2026-08-31 | bug-patterns/integration-contract-drift.md | .claude/skills/fix-issue/findings/mdl-executor.jsonl (30 records), mdl/executor/cmd_odata.go | New page (added to seed table). The one area where neither mxcli check nor mxbuild is an oracle, so a silent downgrade can return HTTP 200; defaults, EDM mapping and lagging modify paths | diff --git a/docs-wiki/bug-patterns/duplicate-resolver-drift.md b/docs-wiki/bug-patterns/duplicate-resolver-drift.md new file mode 100644 index 000000000..01cb27976 --- /dev/null +++ b/docs-wiki/bug-patterns/duplicate-resolver-drift.md @@ -0,0 +1,78 @@ +--- +title: One Question, Two Answers +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-executor.jsonl + - mdl/executor/validate_program.go + - mdl/backend/backend.go +--- + +> **Do not duplicate**: the individual resolvers and their fixes live in the +> findings; the backend-abstraction rationale is canonical in ADR-0002 and +> [[backend-abstraction]]. This page describes the shape. + +## What this is + +The same question — *does this name resolve? what entity is this? is this +property set?* — gets answered in more than one place, the answers differ, and +the disagreement surfaces as a defect in whichever consumer asked the losing +copy. Roughly twenty-three executor findings are this shape, which makes it the +most common *structural* cause in the area. + +It is worth naming separately from the symptoms it produces, because the +symptoms look unrelated to each other. A forward reference accepted by `check` +and rejected by `exec`, a guard that works on one engine and is inert on the +other, a widget property read by one describer and not its twin — all the same +defect wearing different clothes. + +## How it fits + +**Four places the duplication keeps appearing.** + +*`check` versus `exec`.* The two passes historically ran different validator sets +over the same script, so `check` could reject what `exec` wrote and `exec` could +write what `check` rejected. They also disagreed about *order*: reference +validation collected every definition in the script up front, making forward +references invisible, while `exec` resolves in statement order — so "Check +passed!" and an exec failure on the same file, with earlier statements already +written. + +*Legacy versus modelsdk.* A guard that walks stored BSON can be correct on one +engine and a silent no-op on the other, because the document reaches it in a +different shape. The failure is invisible from either side alone: the test suite +is green, the guard reports nothing, and nothing distinguishes "no violations" +from "never ran". + +*Write path versus read path.* A property written correctly and read back by +nobody is the [[silent-property-drop]] class; a property read by one describer +and not by its twin is this one. Several fixes here amount to deleting a copy — +one reader, one renderer, one resolver — and taking the type set from +`generated/metamodel` rather than from whichever copy the report named. + +*Per-doctype copies.* A behaviour implemented per document type drifts by +construction. The folder clause is the clean example: every doctype's `FOLDER` +handling had the same bug, and the report that named one of them read as a +doctype-specific defect. + +**The tell is that the fix for the reported instance is obviously incomplete.** +When a symptom's cause is "this switch was missing a case", the next question is +how many other switches answer the same question — the answer has repeatedly been +two, three or five. Patching the named one closes the report and leaves the class +open, so the next instance arrives looking new. + +**The durable remedy is to remove the second answer**, not to synchronise the two. +Route both callers through one function; take the enumeration from the metamodel +rather than a hand-maintained copy; make the check and the exec guard *the same +function* so they cannot diverge. Where a second implementation genuinely has to +exist — the two engines — the protection is a test that asserts the *structure*, +such as every exported validator being reachable from the one entry point, rather +than a test per rule. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + resolvers, and which copy was wrong +- [[check-mxbuild-drift]] — the special case where one of the two answers is + mxbuild's +- [[backend-abstraction]] — why two engines exist at all diff --git a/docs-wiki/bug-patterns/flow-graph-geometry.md b/docs-wiki/bug-patterns/flow-graph-geometry.md new file mode 100644 index 000000000..a207e99b1 --- /dev/null +++ b/docs-wiki/bug-patterns/flow-graph-geometry.md @@ -0,0 +1,72 @@ +--- +title: Generated Microflow Geometry and Wiring +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-executor.jsonl + - mdl/executor/layout.go + - mdl/executor/cmd_microflows_builder_control.go +--- + +> **Do not duplicate**: the measurement formulas and per-construct fixes live in +> the findings and in `mdl/executor/layout.go`. This page describes why the class +> is easy to get wrong and hard to notice. + +## What this is + +A microflow is a **graph with coordinates**, and mxcli generates both. Ten +executor findings are in this class, and they split into two halves that fail +very differently: geometry that is merely ugly, and wiring that is invalid. + +Neither is caught by anything automatic. A too-wide loop box builds at 0 errors — +it is a human looking at Studio Pro who notices. A missing outgoing flow *is* a +build error, but only sometimes, because which flows are required depends on the +shape around them. + +## How it fits + +**Geometry: the measurement runs before the thing it measures.** A container's +size came from a pre-pass over the AST, executed before the body was built — so +it was a function of statement *count*, not of the activities actually placed. +Varying only the children's size changed nothing. The related arithmetic error is +treating `HorizontalSpacing` as a gap when it is a centre-to-centre *pitch*: the +builder centres each activity and advances by exactly that, so adding it on top +of each width over-measures a run of n activities by `(n-1) × ActivityWidth`. + +**Do not guess the advance for a compound element.** For a run of simple +activities the span is derivable. For an `if`, a split or a nested loop it comes +out of merge geometry, and guessing *under*-sizes the box so children land +outside it — worse than a box that is too wide. Those runs fall back to the +conservative measure deliberately. The containment check is the honest test: +every child of a looped activity must lie inside the container's box. + +**Wiring: the invalid shapes come from branches that do not merge.** A decision +whose non-terminal branch falls off the end of a loop body, a `break` nested +inside an `if`, a split followed by a statement — each is a place where a +sequence flow has to be synthesised or deferred, and getting it wrong produces +either a missing outgoing flow (`CE0079`, `CE0089`) or a dangling reference that +takes `mx check` down during **load**. The valid Mendix representation of +"nothing happened, go round again" is an explicit Continue event, not an absent +flow. + +**A flow that looks redundant is often load-bearing.** Removing the empty-entity +branch of a type split to stop DESCRIBE emitting a phantom `else` failed the +build with `CE0089`: `(empty)` and the base type cover different things, and +`else` cannot substitute for either. That wrong fix was implemented first and +caught only because every shape was re-run through mxbuild — not because a unit +test failed. + +**The read-side twin.** Several findings here pair with a describe defect, +because the same structure has to be recovered from coordinates on the way back +out: a loop body was rendered empty when an annotation sat to its left, and a +describe → exec round trip moved a Studio Pro-authored start event. Verify a +coordinate fix against the *whole* coordinate set — dump every point before and +after and diff — or a change that pins one element while shifting another reads +as success. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the formulas, + the CE codes and the controls +- [[unloadable-model-writes]] — where a dangling sequence flow ends up +- [[describe-round-trip-gaps]] — the read side of the same coordinates diff --git a/docs-wiki/bug-patterns/integration-contract-drift.md b/docs-wiki/bug-patterns/integration-contract-drift.md new file mode 100644 index 000000000..7e0fbb7d9 --- /dev/null +++ b/docs-wiki/bug-patterns/integration-contract-drift.md @@ -0,0 +1,73 @@ +--- +title: Integration Documents and the Contract They Answer To +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-executor.jsonl + - mdl/executor/cmd_odata.go +--- + +> **Do not duplicate**: the per-property fixes, EDM mappings and mapping-element +> shapes live in the findings; the MDL spellings live in the skills and +> `MDL_QUICK_REFERENCE.md`. This page describes what makes the area distinctive. + +## What this is + +Published OData services, consumed REST and OData clients, and import/export +mappings all describe a document whose correctness is defined **outside the +model** — by a schema, a `$metadata` document, or the EDM type system. Roughly +thirty executor findings live here, and they share a property that the rest of +the executor does not: for much of it, **neither `mxcli check` nor mxbuild is an +oracle**. + +## How it fits + +**The validation floor is lower than elsewhere.** A database connection's `type` +string is passed straight through, and mxbuild does not validate it either — so a +misspelled driver builds at 0 errors and simply does not connect. A mapping +naming a schema source that does not exist passes both checks. When neither tool +can tell you, the only remaining signal is a runtime failure, which is why +several findings in this class were first seen as an exception in a running app +rather than as an error anywhere. + +**A silent downgrade that returns HTTP 200 is the worst available outcome.** A +consumed operation whose file body was written as literal text sent the string +`$Doc`, got a 200 back, and every signal a person or an agent would check said +success. Where mxcli cannot express a construct, refusing is strictly better than +writing a different one. + +**Defaults are the recurring mechanism.** These documents carry many properties +Studio Pro sets and MDL does not mention — a service's `ServiceName` as distinct +from its document `Name`, a published attribute's `EdmType`, an association end's +`IsMany`, a mapping element's key flag, an activity's range and cardinality +pointers. Each omitted default is a build error or a runtime fault at some +distance from the statement, and several were invisible on one engine because its +`$Type` handling filled a gap the other left open. + +**The type system is a real mapping, not a passthrough.** Mendix publishes +Integer as `Edm.Int64`; a name like `name` is not a system-managed attribute and +must not be disambiguated as if it were; `PublishAssociations` selects a +*representation* rather than a yes/no, and choosing "as an associated object id" +imposes a key requirement. Getting these wrong produces `CE5016`-family errors +across every exposed member at once, which reads as one large failure rather than +one small mapping bug. + +**Modify paths lag create paths.** Re-running `create or modify` on a service +left the exposed entity sets untouched; on a client it left the cached contract +stale, so the tool kept reporting the old entity types after the backend had +changed. Where a create path fetches or derives something, check whether the +modify path does too — the asymmetry is easy to introduce and invisible until +someone edits an existing document. + +**A rule that can be satisfied two ways will be satisfied the wrong way.** Two +OData advisories shared one predicate, so answering either concern silenced both: +adding an `HttpRequest` parameter for a key lookup also silenced the paging +warning, with no paging implemented. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + properties, EDM mappings and mapping-element shapes +- [[silent-property-drop]] — the same "written, then absent" shape inside pages +- `.claude/skills/verify-in-runtime.md` — for the failures that only appear when + the request is actually made diff --git a/docs-wiki/bug-patterns/platform-semantics-gaps.md b/docs-wiki/bug-patterns/platform-semantics-gaps.md new file mode 100644 index 000000000..19166709a --- /dev/null +++ b/docs-wiki/bug-patterns/platform-semantics-gaps.md @@ -0,0 +1,80 @@ +--- +title: Legal MDL, Illegal Mendix +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-executor.jsonl + - mdl/executor/validate_microflow_ce_gaps.go + - mdl/executor/validate_microflow_loop_scope.go +--- + +> **Do not duplicate**: each rule's predicate, CE number and measured fix live in +> the findings and in `mdl/executor/validate_microflow*.go`; the MDL syntax lives +> in `MDL_QUICK_REFERENCE.md`. This page describes why the category exists at all. + +## What this is + +MDL is deliberately SQL-shaped and permissive: a statement is well-formed if it +parses. Mendix's *semantics* are considerably narrower, and the narrowing has no +syntactic marker — the same statement is well-formed MDL and an illegal model. +This is the largest remaining class of executor finding after the read/write +ones, and it is not a defect in any single component. It is the seam between two +languages. + +## How it fits + +**Four sub-languages, each with rules the MDL grammar cannot carry.** + +*Mendix expressions.* An association path is not a value — reaching an object +over an association has to be materialised with a `retrieve` before it can be +passed as an argument. Word operators must be lowercase; an uppercase `AND` in a +condition builds as `CE0117`. `dateTime()` takes numeric constants, not +variables. Each of these is a plain expression as far as MDL is concerned. + +*XPath.* Mendix's dialect reaches at most **one** hop off a variable; `= empty` +tests attributes, not associations; there is no `id` member to constrain on. A +constraint is a string to MDL, so nothing about its shape flags any of them. + +*Microflow variable model.* A loop iterator is scoped to the **whole microflow**, +not to its loop — so two loops reusing `$R` is a duplicate name (`CE0111`), while +a reference to `$item` *after* `end loop` is out of scope (`CE0108`). Both halves +are counter-intuitive in opposite directions, and both read as ordinary code. +`$x = call M.F()` is a variable *creation* each time, so the natural "try A else +try B" fallback chain is also a duplicate. + +*Entity and document rules.* `not null` and `unique` are validation *rules* in +Mendix's model, which is why they are rejected on a non-persistent entity +(`CE0070`). A `declare` maps to a Create Variable activity, which may not produce +a list. An `else` on a type split is the `(empty)` flow — a null object — not a +default branch, so it never catches an unlisted subtype. + +**None of this is discoverable from the MDL side.** The author writes something +that reads correctly, `mxcli check` has no rule for it, `exec` writes it, and the +build names a CE code against a construct the author believed was ordinary. The +gap is closed one rule at a time, and each rule is a claim about the *platform* +that has to be measured against a real mxbuild rather than reasoned from the +error text. + +**The rules divide by how confidently they can be enforced**, and that decides +where they are wired. Rules that predict a *platform* constraint — the XPath and +expression ones — are safe to enforce unconditionally, because the constraint +does not depend on anything mxcli generates. Rules that predict what the +*builder* emits are not: building one such change turned up four shapes where the +AST reads "broken" and the build disagrees, two of them in mxcli's own shipped +examples. Those stay off the unconditional write barrier and leave `--no-check` +as an escape hatch. + +**Not every report in this class is a defect.** Several were measured and closed +as correct behaviour: quoting an identifier escapes MDL's parser keywords and not +Mendix's *platform*-reserved member names; `else` on a type split really is the +empty flow; `create` really is non-idempotent, in the SQL sense the language is +modelled on. What those cases needed was a better error message or a +documentation fix, not a code change — and telling the two apart is what the +measurement is for. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — each rule, its + CE number, and the mxbuild run that established it +- [[check-mxbuild-drift]] — what happens when one of these rules is wrong +- [[mdl-as-sql]] — why the language is permissive by design diff --git a/docs-wiki/bug-patterns/rewrite-drops-unauthored-state.md b/docs-wiki/bug-patterns/rewrite-drops-unauthored-state.md new file mode 100644 index 000000000..0844658fe --- /dev/null +++ b/docs-wiki/bug-patterns/rewrite-drops-unauthored-state.md @@ -0,0 +1,67 @@ +--- +title: Rewrites That Drop What They Did Not Author +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-executor.jsonl + - docs/13-decisions/0005-semantic-model-interface-currency.md + - docs/13-decisions/0008-identity-and-idempotence.md +--- + +> **Do not duplicate**: the guard-don't-drop decision is canonical in ADR-0005, +> identity and idempotence in ADR-0008 and CLAUDE.md, and the per-document +> recipes in the findings. This page describes the recurring failure. + +## What this is + +`CREATE OR REPLACE` and `CREATE OR MODIFY` rebuild a document from a statement +that describes only part of it. Everything the statement does not mention — a +queued call binding, a toolbox entry, translated captions, an index, a folder, an +attribute's identity — has to be carried across, and each property that is not is +lost silently. Thirteen executor findings are this, and they are unusually +expensive because the loss is invisible at every checkpoint: the run reports +success, `mx check` reports 0 errors, and the model is valid. It is simply +smaller than it was. + +## How it fits + +**A model is bigger than its MDL.** Mendix documents carry state MDL has no +spelling for, and a rebuild constructs the document from what the statement says. +Anything else defaults. That is why the safe posture is **guard, don't drop**: a +rewrite that would discard a construct mxcli cannot express should refuse the +statement rather than quietly produce a smaller document. + +**The most expensive variant is identity, not content.** Re-minting an +attribute's ID on every rewrite produced a document that looked identical and +made the runtime's database synchroniser treat every column as new — rows +survived, values gone. Nothing in the model was wrong. This is the same concern +as `GUID` preservation and the reason `canon.Reconcile` exists; a codec that +mints fresh identities on rebuild is a data-loss bug wearing a clean `mx check`. + +**Delete-then-create defeats every protection.** One replace path removed the +stored document before writing the new one, so nothing was left for identity +preservation or elision to reconcile against, and translated captions in every +language reset to the source language. Whatever carrying mechanism exists, a path +that deletes first opts out of it. + +**Stamping every field on both paths is the usual mechanism.** Where a create and +an update share a field-application helper, the update overwrites settings the +user changed by hand with values derived from somewhere else. A field set on the +`OR MODIFY` path is also not evidence it is set on the `CREATE` path — the two +construct the element separately, so both need checking, by grepping the struct +literal rather than the field name. + +**Partial statements are the honest hazard.** `create or modify entity` with a +subset of attributes drops the rest — 36 down to 2 in one report — which is +arguably what "modify to this shape" means. The remedy there was not refusal but +telling the truth loudly: diff the members, print what is being dropped, and +point at the incremental spelling. Where the statement *is* a full replace, the +user asked for it; where the loss is of something MDL cannot express at all, they +did not. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + properties and the carrying mechanism each needed +- [[element-identity]] — `$ID` versus `GUID` versus `StableId` +- [[mpr-read-write]] — where the write choke points are, and what elision assumes