From 5f103a053fd210ae556cccb898a538e3919f1925 Mon Sep 17 00:00:00 2001 From: Ako Date: Tue, 1 Sep 2026 08:12:12 +0000 Subject: [PATCH] docs(wiki): digest the mdl/backend findings into three bug-pattern pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 82 of the 83 mdl/backend findings now fall under a named class. Only three pages are new: most of the area reuses classes written for mdl/executor and cmd/mxcli, which is the digest working rather than a shortcut. - engine-divergence (30). The dominant class here. Two implementations behind one interface, with the newer, less complete one as the default — so a gap is the behaviour most users get while tests formed against legacy still pass. A gap on one engine is invisible from inside that engine: everything is self-consistent, and the field that never existed is never missed. The cross-engine DESCRIBE matrix (write with A, read with B, all four) is the only check that sees it. Two failure modes and only one is honest — a refusal naming MXCLI_ENGINE=legacy costs a flag, a `-- Empty action` placeholder makes describe→exec delete the construct. The worst instance was a read that under-reported page access. - mutator-addressing (19). ALTER edits a stored document in place, which means naming a node — and DataGrid2 columns, object-list items and layout regions store no Name at all. Derived names are unstable, ambiguity is refused rather than resolved, and hand-built BSON drifts from codec-built BSON in ways only a dump-diff shows. Getting a typed-array marker wrong turns a silent no-op into an unopenable project. - access-rule-reconciliation (6). Small and graded high: GRANT looks additive and is a read-modify-write, so both widening and narrowing report success. Records two method points — the writer was innocent and a later reconcile removed the rules, and the reported trigger (WHERE) was a red herring that a scoped fix would have satisfied. The ~33 MCP/PED findings are deliberately NOT given a page: that subsystem already has architecture/mcp-backend.md and models/ped-mutation-constraints.md, and restating them would break the never-restate rule. Recorded as its own SYNC_LOG row so the omission reads as a decision rather than an oversight. Co-Authored-By: Claude Opus 5 --- .claude/skills/maintain-wiki.md | 3 + CLAUDE.md | 2 +- docs-wiki/SYNC_LOG.md | 4 + .../access-rule-reconciliation.md | 62 +++++++++++++++ docs-wiki/bug-patterns/engine-divergence.md | 77 +++++++++++++++++++ docs-wiki/bug-patterns/mutator-addressing.md | 74 ++++++++++++++++++ 6 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 docs-wiki/bug-patterns/access-rule-reconciliation.md create mode 100644 docs-wiki/bug-patterns/engine-divergence.md create mode 100644 docs-wiki/bug-patterns/mutator-addressing.md diff --git a/.claude/skills/maintain-wiki.md b/.claude/skills/maintain-wiki.md index 86f092ba7..60fffc093 100644 --- a/.claude/skills/maintain-wiki.md +++ b/.claude/skills/maintain-wiki.md @@ -157,6 +157,9 @@ isn't better served by an existing page or a different doc artifact. | `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 | ## Adding a new page diff --git a/CLAUDE.md b/CLAUDE.md index 6efa2829e..5bb0ca61f 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 covers `mdl/executor` (83% of its findings) and `cmd/mxcli` (92%) across 17 pattern pages, 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/.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%) and `mdl/backend` (99%) across 20 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). diff --git a/docs-wiki/SYNC_LOG.md b/docs-wiki/SYNC_LOG.md index 17f92daef..55d25d60a 100644 --- a/docs-wiki/SYNC_LOG.md +++ b/docs-wiki/SYNC_LOG.md @@ -45,3 +45,7 @@ not capture, because sources are upstream of the commit. | 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 | diff --git a/docs-wiki/bug-patterns/access-rule-reconciliation.md b/docs-wiki/bug-patterns/access-rule-reconciliation.md new file mode 100644 index 000000000..34181c2bb --- /dev/null +++ b/docs-wiki/bug-patterns/access-rule-reconciliation.md @@ -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 diff --git a/docs-wiki/bug-patterns/engine-divergence.md b/docs-wiki/bug-patterns/engine-divergence.md new file mode 100644 index 000000000..567adbfa8 --- /dev/null +++ b/docs-wiki/bug-patterns/engine-divergence.md @@ -0,0 +1,77 @@ +--- +title: Two Engines, One Interface +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-backend.jsonl + - mdl/backend/modelsdk/microflow.go + - docs/13-decisions/0004-full-codec-engine.md +--- + +> **Do not duplicate**: the abstraction's rationale is canonical in ADR-0002 and +> ADR-0004 and framed in [[backend-abstraction]]; the per-field fixes live in the +> findings. This page describes what goes wrong while two implementations exist. + +## What this is + +The backend interface has two implementations — the legacy `sdk/mpr` writer and +the codec-based `modelsdk` engine — and roughly a quarter of the `mdl/backend` +findings are one of them doing something the other does not. The default is the +newer engine, so a gap in it is the behaviour most users get, while the tests and +habits formed against legacy still pass. + +A gap on one engine is **invisible from inside that engine**. Everything is +self-consistent: the write stores what the read returns, the round trip is +stable, and the field that never existed is never missed. + +## How it fits + +**Two failure modes, and only one of them is honest.** A refusal — +`… not yet supported by the modelsdk engine — rerun with MXCLI_ENGINE=legacy` — +costs the user a flag and tells the truth. The alternative is a placeholder: +`-- Empty action` from a describer that does not recognise an activity type, a +sort clause silently absent, an argument list quietly dropped. The placeholder is +worse than the refusal in every way, because `describe → edit → exec` then +deletes the construct and everything reports success. + +**The worst instance was a read that under-reported access.** A restricted page +came back as "no roles" on the default engine, so `SHOW ACCESS` and the security +matrix both understated who could reach it. A missing field is a cosmetic bug +almost everywhere and a security-relevant one here. + +**The check that finds these is the cross-engine matrix.** Write with engine A, +read with engine B, all four combinations. Every other test shape is +engine-local and therefore blind to exactly this class. + +**Read against the keys the writer builds, not against gen's accessors.** The +generated bindings and the stored document disagree in places — one action's +result is bound as `VariableName` where the model stores `OutputVariableName` — +so an accessor-based reader silently returns nothing. Round-trip tests +(`toGen → encode → decode → fromGen`) catch it; a reader test over hand-written +BSON does not, because the hand-written BSON was written from the same wrong +assumption. These have to be Go tests rather than MDL ones: `create or modify` +rebuilds the document from MDL, so it cannot reproduce a read-back defect at all. + +**Count the gap before fixing the instance.** Decoding every `$Type` through the +real reader turned one reported unsupported action into twenty-seven, one of +which was a shipped feature that could not be described back. Related: several +activity types live in **their own sub-metamodel** — `DatabaseConnector$…`, not +`Microflows$…` — so a probe or a grep under one prefix systematically +under-counts what is covered. + +**Fixing one field and not its sibling is the characteristic mistake.** A reader +restored for `TableMappings` and not `Parameters`, a source type ported without +its key parts. Enumerate every child of the element rather than the one the +report named. + +**Guard the write side with a round trip, not with `mx check`.** Several of these +produced models mxbuild accepts: a box size of zero renders every activity as a +one-pixel sliver in Studio Pro and validates at 0 errors, because `mx check` does +not look at geometry. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + fields, sub-metamodels and round-trip guards +- [[backend-abstraction]] — why the seam exists +- [[describe-round-trip-gaps]] — the same read-side failure, engine-independent diff --git a/docs-wiki/bug-patterns/mutator-addressing.md b/docs-wiki/bug-patterns/mutator-addressing.md new file mode 100644 index 000000000..be6e76517 --- /dev/null +++ b/docs-wiki/bug-patterns/mutator-addressing.md @@ -0,0 +1,74 @@ +--- +title: Addressing Things the Model Does Not Name +category: bug-pattern +last-synced: ced830e0 +sources: + - .claude/skills/fix-issue/findings/mdl-backend.jsonl + - mdl/backend/mpr/page_mutator.go + - mdl/backend/modelsdk/page_write.go +--- + +> **Do not duplicate**: the ALTER PAGE / ALTER LAYOUT syntax lives in the skills +> and `MDL_QUICK_REFERENCE.md`; the per-property fixes live in the findings. This +> page is about the addressing problem underneath them. + +## What this is + +`ALTER PAGE`, `ALTER LAYOUT` and `ALTER WORKFLOW` edit a stored document in +place, which means naming a node inside it. Roughly thirteen `mdl/backend` +findings come from the fact that **many of the interesting nodes have no name**. +A DataGrid 2 column, an Accordion group, a pop-up menu item and a layout region +are all addressable in Studio Pro and store no `Name` at all. + +The resulting failures have a distinctive signature: the command reports +success and nothing changes. Or it reports `widget "X" not found` for something +`DESCRIBE` plainly shows. + +## How it fits + +**A derived name is the only option, and it is not stable.** A column's +addressable name comes from its attribute or caption, so it moves when the +caption is edited. Persisting the name the author wrote is not available either: +there is no slot for it, and inventing one is the write-a-property-Studio-Pro- +does-not-declare hazard. What is left is to make the derivation *visible* — warn +at authoring time which name will be addressable, and on a lookup miss list the +addressable names rather than reporting a bare not-found. + +**Ambiguity must be refused, not resolved.** Two columns deriving the same name +is a real state, and taking the first is a data hazard rather than a wart. The +same applies to a bare name that could mean either an object-list item or a +widget: discriminate on the resolved node's `$Type`, name the qualified form in +the error, and do not guess which grid was meant — guessing is what produced an +invalid document. + +**A slot is not a name.** A layout region has neither, so it is addressed +positionally (`layoutContainer.top`), reusing the dotted reference that also +serves columns and disambiguated the same way. Only `INSERT INTO` accepts one: +BEFORE and AFTER position a widget among siblings, and treating them as INTO +would silently put widgets somewhere the script did not ask for. + +**Property lookup is per-shape, and the shapes differ.** A button's text is a +`CaptionTemplate`, not a `Caption`. Page-level property names are case-sensitive +while widget ones are matched lowercase. A column's value kind comes from the +schema — expression, primitive or text template — and writing a string where a +reference belongs is accepted by everything and visible to nothing. + +**Hand-built BSON drifts from codec-built BSON.** The mutator constructs +documents directly while CREATE goes through the codec, so the two encodings of +"the same" widget diverge — an empty-string value where the codec writes an +explicit key, a typed-array marker present in one and not the other. The reliable +method is to **diff the two encodings** with a BSON dump rather than eyeball the +hand-built one. Getting the marker wrong is not a smaller version of the same +bug: it turns a silent no-op into a project Studio Pro cannot open. + +**Check whether the symptom reproduces outside the reported context.** One +"widget not found" turned out to be two independent defects, and the fix for the +reported nesting alone would not have made the reporter's command work. + +## See also + +- [fix-issue findings](../../.claude/skills/fix-issue/findings/) — the individual + properties, shapes and refusals +- [[unloadable-model-writes]] — where a wrong typed-array marker ends up +- [[silent-property-drop]] — the executor-side twin of "reports success, writes + nothing"