Skip to content
Merged
500 changes: 250 additions & 250 deletions .claude/skills/fix-issue/findings/mdl-executor.jsonl

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions .claude/skills/maintain-wiki.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,19 @@ isn't better served by an existing page or a different doc artifact.
| `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 |
| `bug-patterns/test-runner-cannot-fail.md` | bug-pattern | `mxcli test` reporting PASS for what did not hold, run or get evaluated |
| `bug-patterns/local-loop-silence.md` | bug-pattern | the warm loop failing quietly across processes mxcli does not own |
| `bug-patterns/styling-compiles-to-nothing.md` | bug-pattern | SCSS and tokens written correctly, compiled nowhere, checked by nothing |
| `bug-patterns/package-operations-damage.md` | bug-pattern | marketplace / `mx` operations that change the project and report success |
| `bug-patterns/cli-contract-defects.md` | bug-pattern | flags, paths, stdout and help — the class with no Mendix document in it |
| `bug-patterns/engine-divergence.md` | bug-pattern | two backend implementations, and a gap in one that is invisible from inside it |
| `bug-patterns/mutator-addressing.md` | bug-pattern | in-place edits to nodes the model does not name |
| `bug-patterns/access-rule-reconciliation.md` | bug-pattern | GRANT is a read-modify-write, and both directions of loss report success |
| `bug-patterns/capability-gap-as-parse-error.md` | bug-pattern | an unspelled capability presenting as `no viable alternative`, and the workarounds that follow |
| `bug-patterns/keyword-collisions.md` | bug-pattern | MDL's keyword set occupying positions where user data lives |
| `bug-patterns/scripts-that-cannot-rerun.md` | bug-pattern | statement-level idempotence, and why it is not write-level idempotence |
| `bug-patterns/expression-translation-drift.md` | bug-pattern | MDL expression to Mendix expression, where the translation changes the meaning |
| `bug-patterns/misleading-diagnostics.md` | bug-pattern | hints that fire on the wrong thing and send the reader somewhere the problem is not |

## 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 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".
**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` (83% of its findings), `cmd/mxcli` (92%), `mdl/backend` (99%), `mdl/grammar` (92%) and `mdl/visitor` (96%) across 25 pattern pages; `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
15 changes: 15 additions & 0 deletions docs-wiki/SYNC_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,18 @@ not capture, because sources are upstream of the commit.
| 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 |
| 2026-08-31 | bug-patterns/test-runner-cannot-fail.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (14 records), cmd/mxcli/testrunner/parser.go, cmd/mxcli/testrunner/generator_endpoint.go | New page (added to seed table). First pass over cmd/mxcli. The one class whose cost is confidence rather than debugging time: silent absence (an annotation parsed and read by nothing), two result-assembly paths, and why the remedy is fail-closed plus a single call site |
| 2026-08-31 | bug-patterns/local-loop-silence.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (16 records), cmd/mxcli/docker/localboot.go, cmd/mxcli/docker/runlocal.go | New page (added to seed table). The warm loop orchestrates processes mxcli did not write, so the thing that breaks is not the thing that reports — a missing bundle answers 200, a dead child does not stop the parent, and every resolved-for-another-tool value is a silent disagreement |
| 2026-08-31 | bug-patterns/styling-compiles-to-nothing.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (11 records), cmd/mxcli/theme/block.go | New page (added to seed table). No validator exists for CSS, so location decides whether anything compiles, an unread token is indistinguishable from a design not applied, and contrast is a correctness property nothing measures |
| 2026-08-31 | bug-patterns/package-operations-damage.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (11 records), cmd/mxcli/marketplace/update.go | New page (added to seed table). Damage from handing the project to tools mxcli does not control, reported as success — the v2 to v1 collapse at 0 errors being the worst of it |
| 2026-08-31 | bug-patterns/cli-contract-defects.md | .claude/skills/fix-issue/findings/cmd-mxcli.jsonl (14 records), cmd/mxcli/syntax/ | New page (added to seed table). The class with no Mendix document in it: help that teaches unparseable syntax, unqualified success messages, accepted-but-inert flags, stdout as a data channel. Matters disproportionately because agents take the tool's word |
| 2026-08-31 | bug-patterns/engine-divergence.md | .claude/skills/fix-issue/findings/mdl-backend.jsonl (30 records), mdl/backend/modelsdk/microflow.go, docs/13-decisions/0004-full-codec-engine.md | New page (added to seed table). First pass over mdl/backend. A gap on one engine is invisible from inside that engine, so the cross-engine DESCRIBE matrix is the only check that finds them; a refusal is honest and a `-- Empty action` placeholder is not; read against the WRITER's keys rather than gen's accessors |
| 2026-08-31 | bug-patterns/mutator-addressing.md | .claude/skills/fix-issue/findings/mdl-backend.jsonl (19 records), mdl/backend/mpr/page_mutator.go, mdl/backend/modelsdk/page_write.go | New page (added to seed table). Many addressable nodes — DataGrid2 columns, object-list items, layout regions — store no Name, so addressing is derived or positional; ambiguity is refused rather than resolved, and hand-built BSON drifts from codec-built BSON in ways only a dump-diff shows |
| 2026-08-31 | bug-patterns/access-rule-reconciliation.md | .claude/skills/fix-issue/findings/mdl-backend.jsonl (6 records), mdl/backend/modelsdk/domainmodel_security_write.go | New page (added to seed table). Small class graded high: GRANT is a read-modify-write and both directions of loss report success. Records that the writer was innocent in the instructive case — a later reconcile removed the rules — and that the reported trigger (WHERE) was a red herring |
| 2026-08-31 | (no page) mdl/backend MCP findings | .claude/skills/fix-issue/findings/mdl-backend.jsonl | Deliberately NOT given a page: ~33 records touch the MCP/PED backend, which already has architecture/mcp-backend.md and models/ped-mutation-constraints.md. Restating them here would violate the never-restate rule; the findings link to those pages instead |
| 2026-08-31 | bug-patterns/capability-gap-as-parse-error.md | .claude/skills/fix-issue/findings/mdl-grammar.jsonl (20 records), mdl/grammar/MDLParser.g4, docs/13-decisions/0003-mdl-is-sql-shaped.md | New page (added to seed table). First pass over mdl/grammar. A parse error is indistinguishable from a mistake the user made, so the reports in this class are workarounds rather than feature requests; the gap is bidirectional (DESCRIBE must do something too, and rebuilding a DIFFERENT document is worse than a parse error); a narrow/wide statement pair is a whitelist |
| 2026-08-31 | bug-patterns/keyword-collisions.md | .claude/skills/fix-issue/findings/mdl-grammar.jsonl (8 records), mdl/grammar/MDLLexer.g4, mdl/executor/identifier_quoting.go | New page (added to seed table). The distinguishing question is how a collision fails, not whether one happens: a parse error is recoverable, a different valid parse is not. Records that a grammar alternative and its visitor case are one change, and the control-binary sweep for proving a relaxation |
| 2026-08-31 | bug-patterns/scripts-that-cannot-rerun.md | .claude/skills/fix-issue/findings/mdl-grammar.jsonl (5 records), mdl/grammar/domains/MDLDomainModel.g4, docs/13-decisions/0003-mdl-is-sql-shaped.md | New page (added to seed table). Statement-level idempotence, kept explicitly apart from ADR-0008 write-level idempotence, which is what people usually mean when they say mxcli is idempotent. The silent variant (a duplicated index) is worse than the error |
| 2026-08-31 | bug-patterns/expression-translation-drift.md | .claude/skills/fix-issue/findings/mdl-visitor.jsonl (13 records), mdl/visitor/visitor_microflow_expression.go, mdl/visitor/visitor_helpers.go | New page (added to seed table). First pass over mdl/visitor. Distinct from platform-semantics-gaps: there the MDL is illegal Mendix, here the MDL is correct and the TRANSLATION says something else — the worst case a microflow computing a different number with every check green. Records the ANTLR hidden-token trap and why the same expression text means different things in different slots |
| 2026-08-31 | bug-patterns/misleading-diagnostics.md | .claude/skills/fix-issue/findings/mdl-visitor.jsonl (6 records), mdl/visitor/visitor.go | New page (added to seed table). Graded above "the message could be clearer": a wrong hint costs however long the reader spends acting on it, and in the reported cases they blamed their quoting, renamed an attribute that was fine, or concluded a construct was unsupported. A hint's precision matters more than its coverage |
| 2026-08-31 | bug-patterns/visitor-wiring-gaps.md | .claude/skills/fix-issue/findings/mdl-visitor.jsonl, mdl/visitor/visitor_enumeration.go, mdl/visitor/visitor_helpers.go | **Re-sync** (first since the 2026-05-24 initial synthesis). Broadened from one size of gap to three — a field, a structure (ELSIF arms lowered into nested ifs), and a whole statement that parses and dispatches to nothing — and added the neighbouring failure where a field is wired to the WRONG thing, which reports success and changes meaning. sources: updated from the findings directory to the specific shard; wiki-links normalised to bare slugs |
62 changes: 62 additions & 0 deletions docs-wiki/bug-patterns/access-rule-reconciliation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: Access Rules Are Reconciled, Not Appended
category: bug-pattern
last-synced: ced830e0
sources:
- .claude/skills/fix-issue/findings/mdl-backend.jsonl
- mdl/backend/modelsdk/domainmodel_security_write.go
---

> **Do not duplicate**: the GRANT syntax and the security workflow live in
> `.claude/skills/manage-security.md`; the CE numbers and per-member rules live
> in the findings. This page describes why this small class is graded high.

## What this is

`GRANT` looks additive and is not. A grant is stored as an *entity access rule*
carrying a complete picture of what a role may do, so writing one is a
read-modify-write over an existing rule — and five `mdl/backend` findings are
that reconciliation losing rights the statement never mentioned.

The class is small and worth its own page because of what it costs when wrong.
Every other silent loss in this wiki costs a feature; this one **quietly widens
or narrows who can read data**, and both directions are reported as success.

## How it fits

**A grant that mentions one attribute must not revoke the others.** The reported
symptom was attributes granted by an earlier statement coming back as `None`.
Structural rights — create, delete, the default member access — went with them,
which the report did not mention: **re-derive the blast radius rather than
inheriting it from the reporter**.

**The reported trigger is often not the trigger.** A constrained (`WHERE`) grant
was blamed, and the same loss reproduced with no `WHERE` and with `READ *`. A fix
scoped to the reported path would have passed the reporter's reproduction and
left most of the defect in place.

**When a value is written and then absent, instrument the later pass.** In the
most instructive of these the writer was innocent — it stored all three rules —
and a reconciliation running afterwards removed them. Starting at the writer is
the natural instinct and the wrong end.

**Preserve what cannot be checked.** Membership questions do not all resolve
locally: an association is qualified by the module that *declares* it, so a
specialization inheriting from a generalization in another module has members the
local walk cannot see. Dropping what the walk cannot confirm produced
`CE0066 "Entity access is out of date"` — the model claiming rights over members
it no longer lists. The safe default is to carry an unconfirmable member through
rather than to prune it.

**Under-reporting access is the read-side twin.** A restricted page reported as
having "no roles" is the same class seen from the query side, and it is the shape
most likely to be believed, because "no roles" reads like a finding rather than a
gap.

## See also

- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the member
walks, the CE numbers and the controls
- [[association-pointers]] — why a member belongs to the FROM entity, which
decides where a MemberAccess may appear
- [[engine-divergence]] — where the "no roles" read gap came from
74 changes: 74 additions & 0 deletions docs-wiki/bug-patterns/capability-gap-as-parse-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: A Missing Capability Looks Like a Syntax Error
category: bug-pattern
last-synced: ced830e0
sources:
- .claude/skills/fix-issue/findings/mdl-grammar.jsonl
- mdl/grammar/MDLParser.g4
- docs/13-decisions/0003-mdl-is-sql-shaped.md
---

> **Do not duplicate**: the syntax itself lives in `MDL_QUICK_REFERENCE.md` and
> the skills; each construct's grammar rule and BSON shape live in the findings.
> This page is about what a parse error costs when it means "not implemented".

## What this is

Roughly twenty of the `mdl/grammar` findings are the same event: someone tries to
express something Mendix supports, the parser says `no viable alternative`, and
they conclude **the feature is impossible**. It is not — the capability is simply
unspelled, and a parse error is indistinguishable from a mistake the user made.

That confusion is the expensive part. The reports in this class are rarely
"MDL should support X"; they are workarounds. An admin screen hand-rolled as five
pages with bespoke SCSS because a tab container was believed not to exist. "Use a
Java action" for a binary upload. "Go to Studio Pro" as the standing answer to
translating an app. A headless pipeline that ends with a manual step because
anonymous access has no statement.

## How it fits

**The gap is bidirectional.** MDL not being able to *write* a construct is only
half of it: `DESCRIBE` of a document that has one has to do something, and
dropping it silently is the common outcome. Worse, in the mapping findings,
DESCRIBE emitted MDL that **parsed and rebuilt a different document** — output
that looks like a successful round trip and is not. When adding a spelling, the
read side is part of the same change, not a follow-up.

**A narrow/wide pair is a whitelist.** Where two statements can set the same
property and one accepts less than the other — `SET` versus `REPLACE` here — the
narrow one gets extended a bug report at a time, and the documented workaround
routes users through the wide one, which rebuilds more than they asked. The fix
that holds is to reuse the wide rule and its builder, not to add another case to
the narrow one.

**"Not in the metamodel" is not a conclusion until the namespace is right.** A
search under one prefix "disproved" a capability that lives under another —
`Microflows$…RequestHandling` rather than `Rest$…Body`. Asking for a Studio Pro
example settled in minutes what a metamodel grep had ruled out, and CLAUDE.md's
rule applies: when the shape is unknown, get a reference document rather than
reason from an absence.

**With no reference available, derive from something already proven.** Where no
example could be obtained, the shapes came from the generated metamodel and from
sibling writers that already ship the same by-name reference form — not from
guessing and not from stopping. Where a reference *is* available, pin against it:
a guess got four things wrong at once about one element — the type's namespace, a
property that does not exist, append-versus-sort ordering, and a flag's default.

**A reference is a reference, not a string.** An icon, an entity, a microflow —
anything that names a model element is spelled as a qualified name, which is what
makes the language consistent (ADR-0003) and what forces the quoting question
into the describe emitter as well.

**The documentation is part of the surface.** Several of these were found by
running the statements quoted in mxcli's own skills through `mxcli check` rather
than by reading them. Advice that cannot parse is the same defect as a missing
rule, and it is the version an agent will follow confidently.

## See also

- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — each construct,
its rule, and the reference document that settled its shape
- [[mdl-as-sql]] — why the language is shaped the way it is
- [[describe-round-trip-gaps]] — the read half of every gap here
Loading
Loading