Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f725ce9
docs(lint): document every Starlark builtin and struct field; test th…
claude Sep 26, 2026
a8f136a
fix(alter): write a pluggable property to the field its kind declares…
claude Sep 26, 2026
815f835
Merge pull request #699 from ako/fix/lint-skill-api-drift
ako Sep 26, 2026
22d67b1
fix(widgets): carry an action's <actionVariables> from the .mpk (#1200)
claude Sep 26, 2026
9e9d256
docs: propose MDL beta syntax freeze and brownfield editing plan
ako Sep 26, 2026
52fdfd4
docs: record MDL beta decisions in the syntax proposal
ako Sep 26, 2026
9e3b295
docs: separate element describe from definition describe in MDL proposal
ako Sep 26, 2026
b27a0dc
docs: record argument, alter, beta-scope and header decisions in MDL …
ako Sep 26, 2026
df83781
docs: add weekly release schedule to MDL beta proposal
ako Sep 26, 2026
cd88e58
docs: drift detection as optional optimistic locking via @base annota…
ako Sep 26, 2026
caa6d4a
docs: tie MDL meaning changes to the mdl 1 header; add prior-art section
ako Sep 26, 2026
979be35
docs: ADR-0010..0012 for MDL canonical syntax, versioning and editing…
ako Sep 26, 2026
703d017
skills: design-mdl-syntax checklist follows ADR-0010 rules
ako Sep 26, 2026
306a3ac
docs: header-gated changes need no warning releases; mdl 1 is a previ…
ako Sep 26, 2026
78c43f3
Merge pull request #700 from ako/fix/1201-alter-pluggable-property-kind
ako Sep 26, 2026
a49ecc2
Merge pull request #701 from ako/fix/1200-mpk-action-variables
ako Sep 26, 2026
266e7ad
docs: drop CLAUDE.md pointer to ADR-0010/0011; it lives in design-mdl…
ako Sep 26, 2026
97b07a0
docs: accept ADR-0010..0012; mark @base as provisional pending user a…
ako Sep 26, 2026
6b2809e
Merge pull request #702 from ako/claude/mdl-language-critique-1d4dbb
ako Sep 26, 2026
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
45 changes: 37 additions & 8 deletions .claude/skills/design-mdl-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ Reuse existing patterns. Never create a second syntax for the same concept.
| Remove | `drop <type> Module.Name` | `drop entity Shop.Product` |
| List | `list <type>S [in module]` | `list entities in Shop` |
| Inspect | `describe <type> Module.Name` | `describe entity Shop.Product` |
| Security | `grant/revoke <perm> on <target> to/from <role>` | `grant read on Shop.Product to Shop.User` |
| Security | `grant/revoke <perm> on <kind> <target> to/from <role>` | `grant read on entity Shop.Product to Shop.User` |

Do NOT use alternative verbs: `add` instead of `create`, `remove` instead of `drop`, `show` instead of `list`, `view` instead of `describe`. Note: `show` is the legacy verb — new commands use `list`.
Do NOT use alternative verbs: `add` instead of `create`, `remove` instead of `drop`, `show` instead of `list`, `view` instead of `describe`. `show` is dropped (ADR-0010 R6): plurals and relationship queries are `list`, single things are `describe`. Inside `alter`, children are added and removed with `add`/`drop`.

### 3. Optimize for LLMs

Expand All @@ -56,7 +56,7 @@ Do NOT use alternative verbs: `add` instead of `create`, `remove` instead of `dr
### 5. Token Efficiency (Without Sacrificing Clarity)

- Omit noise words: `create entity` not `create A NEW entity`
- Support `or modify` to avoid check-then-create
- Support `create or modify` (the one idempotent create, ADR-0010 R1) to avoid check-then-create
- Allow type inference for obvious cases: `declare $count = 0`
- Do NOT use symbols to save tokens at the cost of readability

Expand Down Expand Up @@ -115,7 +115,9 @@ Rules:
- Trailing comma allowed
- One property per line (single line acceptable for 1-2 properties)

#### Colon `:` vs `as` — When to Use Each
#### Colon `:`, equals `=` and `as` — When to Use Each

ADR-0010 R3: **`:` sets a model property; `=` binds a runtime value** (call arguments `M.F(Order = $Order)`, `change $o (Attr = v)`, `set $x = …`, text-template parameters `with ({1} = …)`). `alter` uses the same property list as `create`: `set ( Key: value )`.

Use **colon** for property definitions (assigning a value to a named property):

Expand All @@ -134,9 +136,10 @@ CUSTOM NAME map (
'kvkNummer' as 'ChamberOfCommerceNumber', -- old name AS new name
'naam' as 'CompanyName',
)
alter entity Shop.Product rename Code as ProductCode -- old attr AS new attr
```

Renames use `to`, like top-level `rename … to`: `alter entity Shop.Product rename attribute Code to ProductCode`.

**Rule of thumb**: if the left side is a *fixed property key* defined by the syntax, use `:`. If the left side is a *user-provided name* being mapped to another name, use `as`.

### Step 5: Validate
Expand Down Expand Up @@ -181,7 +184,7 @@ create entity Shop.Customer (...);
$items |> filter($.active) |> map($.name)

-- RIGHT: keyword-based
filter $Items where Active = true
$Active = filter $Items by Active = true; -- one statement per Studio Pro list-operation activity
```

### Positional Arguments
Expand All @@ -204,10 +207,35 @@ create rule Shop.ProcessOrder (
-- Don't reuse it to mean property modification elsewhere unless established
```

## Canonical Rules (ADR-0010)

These are the canonical rules. Each PR that adds or changes syntax is checked against them. The rationale is in [ADR-0010](../../docs/13-decisions/0010-mdl-canonical-syntax-rules.md); examples are in `docs/11-proposals/PROPOSAL_mdl_beta_syntax_freeze.md` §3.

| # | Rule |
|---|---|
| R1 | One idempotent create: `create or modify`. It changes only what differs, and writes nothing when nothing differs. `if not exists` is the "leave it alone" operation. Renames go through `alter`/`rename`. |
| R2 | `( Key: value, )` holds properties. `{ }` holds declarative children, each shaped `<kind> [Name] ( props ) [ { … } ]`. `begin … end <keyword>` holds imperative flow. |
| R3 | `:` sets a model property; `=` binds a runtime value. `alter … set ( Key: value )` takes exactly the keys of `create`. |
| R4 | One argument form everywhere: `Param = expression` (no `$` on the parameter name). Text templates always use `with ({1} = …)`. |
| R5 | Expressions are bare; XPath is in `[ … ]`. Neither ever goes in a string literal. Constants are referred to as `@Module.Const`. |
| R6 | Verbs: `list`, `describe`, `create`, `alter`, `drop`. Element `describe` emits runnable MDL; definition `describe` emits a report marked "not executable". |
| R7 | Session commands are REPL commands, not grammar. |
| R8 | Keywords are words, one spelling each: lowercase, no SCREAMING_SNAKE, no optional underscores. Property keys are identifiers, never keywords. |
| R9 | Documentation is a `/** */` doc comment; folder is a `folder '…'` clause; canvas layout uses `@` annotations; everything else is a property. |
| R10 | Document types use Studio Pro's names, with consistent `consumed`/`published` prefixes. |
| R11 | `;` is required; trailing commas are allowed in every list; `''` is the only string escape; unknown property keys are errors. |
| R12 | `describe` emits the canonical form only: no defaults, no derived layout, no names Mendix does not store. |

A change of **meaning** to existing syntax is never made in place. It lands behind a language version (`mdl <n>;`, [ADR-0011](../../docs/13-decisions/0011-mdl-language-versioning.md)). A change of **spelling** keeps the old form as a registered deprecated alias.

## Checklist

Before merging any PR that adds new MDL syntax, verify:

- [ ] Conforms to R1–R12 above (and, where the construct exists in both modes, `alter` accepts the same fragment syntax as `create`, per ADR-0012)
- [ ] No new alias: any second spelling is a registered deprecation with an `fmt --upgrade` rewrite
- [ ] Any change of meaning is gated on the language header (ADR-0011)

- [ ] Follows `create`/`alter`/`drop`/`list`/`describe` pattern
- [ ] Uses `Module.Element` qualified names (no bare names)
- [ ] Property lists use `( key: value, ... )` format
Expand All @@ -224,8 +252,9 @@ Before merging any PR that adds new MDL syntax, verify:

## Related Resources

- Full design rationale: `docs/11-proposals/PROPOSAL_mdl_syntax_design_guidelines.md`
- Decisions: [ADR-0003](../../docs/13-decisions/0003-mdl-is-sql-shaped.md), [ADR-0010](../../docs/13-decisions/0010-mdl-canonical-syntax-rules.md), [ADR-0011](../../docs/13-decisions/0011-mdl-language-versioning.md), [ADR-0012](../../docs/13-decisions/0012-mdl-first-and-data-first-editing.md)
- Beta syntax proposal (examples, plan): `docs/11-proposals/PROPOSAL_mdl_beta_syntax_freeze.md`
- Earlier design rationale: `docs/11-proposals/PROPOSAL_mdl_syntax_design_guidelines.md`
- MDL Quick Reference: `docs/01-project/MDL_QUICK_REFERENCE.md`
- Implementation workflow: `.claude/skills/implement-mdl-feature.md`
- Existing syntax proposals: `docs/11-proposals/PROPOSAL_mdl_syntax_improvements.md`
- Grammar file: `mdl/grammar/MDLParser.g4`
1 change: 1 addition & 0 deletions .claude/skills/fix-issue/findings/cmd-mxcli.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,4 @@
{"area": "cmd/mxcli/test", "date": "2026-09-23", "symptom": "Windows: `mxcli test tests/ -p MyApp.mpr --local` fails with `local runtime: starting mxbuild serve: mxbuild --serve did not become ready` after caching a Linux ELF in %USERPROFILE%\\.mxcli\\mxbuild, and there is \"no flag, environment variable, or mechanism to redirect mxcli to the Windows mxbuild.exe already present in the Studio Pro installation\"", "cause": "Two layers. The platform part (downloading/exec'ing the Linux binary, serving from the cache instead of the resolved binary) was already fixed by #916 and #1122, both after the reporter's v0.21.0. What remained on main: `test` never registered `--mxbuild-path` — `run` gained it in #1125, but `test --local` boots through the same `ResolveMxBuildForLocal` and prints the same 'pass --mxbuild-path' guidance while answering `unknown flag`. `RunOptions` had no field and `localAppOptions` never set `LocalAppOptions.MxBuildPath`, though StartLocalApp honoured it. No env override existed anywhere", "file": "`cmd/mxcli/main.go` + `cmd_test_run.go` (flag), `testrunner/runner.go` + `localapp_options.go` (plumbing), `docker/mxbuild_platform.go` (`MxBuildPathEnv`, read in `resolveMxBuildForLocalOn` after the flag)", "insight": "**The guard for #1125 asserted its invariant against one command** — `TestErrorGuidanceNamesAFlagThatExists` checked only `runCmd`, while the guidance it polices is emitted by a resolver two commands share. When a test pins 'the advertised flag exists', enumerate the callers of the code that ADVERTISES it, not the command the report named; it now iterates `run` and `test`. **Before fixing a platform report, date it against the fixes**: the reporter's first two suggestions ('download platform-correct binary', 'auto-discover Studio Pro') were already on main, and re-implementing them would have been churn — only the override was missing. Put the env var in the resolver, not the CLI, so `run --local` and `test --local` both get it from one line; a flag-level env read would have been one more per-command copy to drift. Control: the env test runs as goos=windows with an unmatched version, so without the override it fails fast with the 'Linux binary cannot run natively on windows' refusal instead of hitting the CDN; removing only the `MxBuildPath:` line in localAppOptions fails the plumbing test for both runners. **Unverified**: no Windows host; code-level with OS-injected tests", "refs": ["mendixlabs/mxcli#1086", "mendixlabs/mxcli#1125", "mendixlabs/mxcli#916", "mendixlabs/mxcli#1122"]}
{"area": "cmd/mxcli", "date": "2026-09-25", "symptom": "Shipped Starlark rules CONV009, CONV010, QUAL001, QUAL003, QUAL004 and CUSTOM002 report nanoflows and rules as microflows: \"Microflow 'NF_Foo' has 30 activities\", and documentType \"Microflow\" in the JSON and report output.", "cause": "microflows() yields all three flow flavours (one catalog table, MicroflowType MICROFLOW/NANOFLOW/RULE), and each rule hardcoded document_type=\"Microflow\" and a \"Microflow '...'\" message. The Go rules had already been fixed with Microflow.DocumentNoun() after MPR002 called a rule a microflow, but that method was never exposed to Starlark, so every Starlark rule had to re-derive it and none did.", "file": "mdl/linter/starlark.go, .claude/lint-rules/{conv009_max_microflow_objects,conv010_act_microflow_content,example_microflow,long_microflows,mccabe_complexity,orphaned_elements}.star, mdl/linter/starlark_flow_noun_test.go", "insight": "When a Go-side fix lives in a method, check whether the Starlark projection can reach it; a fix Starlark cannot call is a fix for half the rules. Exposed as document_noun / document_noun_title on the microflow struct, and the test runs every shipped rule over a fixture with one flow per flavour, requiring a finding on each (else vacuous) plus a guard that any new `for x in microflows()` rule joins the list. Skip the wrong turn of excluding nanoflows/rules from QUAL004: rule calls from decisions ARE emitted as 'call' refs (builder_references.go collectRuleCalls), so only the label was wrong.", "refs": "mendixlabs/mxcli#1178"}
{"area": "cmd/mxcli", "date": "2026-09-25", "symptom": "Starlark rule API docs name microflow_type values the linter never returns: the write-lint-rules skill said \"microflow\"/\"nanoflow\", example_microflow.star (copied into every project by mxcli init) said \"Microflow\"/\"Nanoflow\", only mccabe_complexity.star had \"MICROFLOW\"/\"NANOFLOW\", and none listed \"RULE\". The skill's entity table also omitted has_created_date/has_changed_date/has_owner/has_changed_by.", "cause": "LintContext.Microflows() passes the catalog's MicroflowType through raw (MICROFLOW/NANOFLOW/RULE) while Entities() CASE-normalizes EntityType to TitleCase, so the two adjacent iterators have opposite conventions and each doc author guessed. Nothing tied the documented literals to what the API emits, so two passes over the same skill file (59db6e7b, #1165) fixed instances and left this one.", "file": ".claude/skills/mendix/write-lint-rules/SKILL.md, .claude/lint-rules/example_microflow.star, .claude/lint-rules/mccabe_complexity.star, mdl/linter/starlark_documented_values_test.go", "insight": "Fix the class, not the row: observe the emitted field names and enum values by running a Starlark rule (dir(e), e.entity_type) over a fixture holding every stored kind, then assert the skill tables, the shipped rules' `# .field - ...` headers, and every `.entity_type/.microflow_type ==` comparison in shipped rules against that observed set. Equality, not subset, for the docs -- an undocumented value (RULE) is a flavour a rule silently mistreats. Normalizing MicroflowType instead would have broken every user rule already comparing \"MICROFLOW\" correctly.", "refs": "mendixlabs/mxcli#1178, mendixlabs/mxcli#1164"}
{"area": "cmd/mxcli", "date": "2026-09-26", "symptom": "The write-lint-rules skill, which Starlark rules are written from, omitted seven query functions (java_actions, documents, documentable_elements, navigation_targets, queues, module_cycles, database_connections) plus get_option() and struct(), and fields on scheduled_event (repeat, on_overlap, time_zone) and project_security (anonymous_user_role). Four of the missing functions are used by shipped rules.", "cause": "Each builtin and field was added in code with its own PR and a docs-site paragraph at most; nothing compared the skill to buildPredeclared() or to the struct keys, so every API addition drifted from the skill by default. #1178's test pinned only the entity and microflow tables.", "file": ".claude/skills/mendix/write-lint-rules/SKILL.md, mdl/linter/starlark_skill_coverage_test.go", "insight": "Cover the whole surface at once rather than the table that was reported: an internal test ranges over (&StarlarkRule{}).buildPredeclared() for builtins and go/ast-parses every starlarkstruct.FromStringDict(starlark.String(name), dict) call in the package for struct fields (literal dicts and locally-built ones like ppDict), then requires each struct's skill table to match exactly. Structs documented elsewhere are named in explicit maps (helper params, inline struct{...} in a function row, shared permission table) so an exemption is a visible decision. The quick one-off Python diff over-reported (nested password_policy, permissions_for's entity_name) -- the test encodes those layouts instead of guessing.", "refs": "mendixlabs/mxcli#1178"}
Loading
Loading