Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/skills/maintain-wiki.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ 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 |
| `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

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<area>.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, all synthesised on 2026-05-24, against 631 findings of which **607 arrived afterwards** (`make digest-status`) — 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/<area>.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).

Expand Down
9 changes: 9 additions & 0 deletions docs-wiki/SYNC_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,12 @@ 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 |
| 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 |
88 changes: 88 additions & 0 deletions docs-wiki/bug-patterns/check-mxbuild-drift.md
Original file line number Diff line number Diff line change
@@ -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
100 changes: 100 additions & 0 deletions docs-wiki/bug-patterns/describe-round-trip-gaps.md
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading