Skip to content

Commit 4346b39

Browse files
authored
Merge pull request #220 from yscraft/docs/mxcli-dev-contributor-commands
docs: add mxcli-dev contributor command namespace and /mxcli-dev:review
2 parents 0813ba9 + 8b750ac commit 4346b39

File tree

9 files changed

+509
-17
lines changed

9 files changed

+509
-17
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# /mxcli-dev:review — PR Review
2+
3+
Run a structured review of the current branch's changes against the CLAUDE.md
4+
checklist, then check the recurring findings table below for patterns that have
5+
burned us before.
6+
7+
## Steps
8+
9+
1. Run `gh pr view` and `gh pr diff` (or `git diff main...HEAD`) to read the change.
10+
2. Work through the CLAUDE.md "PR / Commit Review Checklist" in full.
11+
3. Then check every row in the Recurring Findings table below — flag any match.
12+
4. Report: blockers first, then moderate issues, then minor. Include a concrete fix
13+
option for every blocker (not just "this is wrong").
14+
5. After the review: **add a row** to the Recurring Findings table for any new
15+
pattern not already covered.
16+
17+
---
18+
19+
## Recurring Findings
20+
21+
Patterns caught in real reviews. Each row is a class of mistake worth checking
22+
proactively. Add a row after every review that surfaces something new.
23+
24+
| # | Finding | Category | Canonical fix |
25+
|---|---------|----------|---------------|
26+
| 1 | Formatter emits a keyword not present in `MDLParser.g4` → DESCRIBE output won't re-parse (e.g. `RANGE(...)`) | DESCRIBE roundtrip | Grep grammar before assuming a keyword is valid; if construct can't be expressed yet, emit `-- TypeName(field=value) — not yet expressible in MDL` |
27+
| 2 | Output uses `$currentObject/Attr` prefix — non-idiomatic; Studio Pro uses bare attribute names | Idiomatic output | Verify against a real Studio Pro BSON sample before choosing a prefix convention |
28+
| 3 | Malformed BSON field (missing key, wrong type) produces silent garbage output (e.g. `RANGE($x, , )`) | Error handling | Default missing numeric fields to `"0"`; or emit `-- malformed <TypeName>` rather than broken MDL |
29+
| 4 | No DESCRIBE roundtrip test — grammar gap went undetected until human review | Test coverage | Add roundtrip test: format struct → MDL string → parse → confirm no error |
30+
| 5 | Hardcoded personal path in committed file (e.g. `/c/Users/Ylber.Sadiku/...`) | Docs quality | Use bare commands (`go test ./...`) without absolute paths in any committed doc or skill |
31+
| 6 | Docs-only PR cites an unmerged PR as a "model example" — cited PR had blockers | Docs quality | Only cite merged, verified PRs; or annotate with known gaps if citing in-flight work |
32+
| 7 | Skill/doc table references a function that doesn't exist (e.g. `formatActionStatement()` vs `formatAction()`) | Docs quality | Grep function names before writing: `grep -r "func formatA" mdl/executor/` |
33+
| 8 | "Always X" rule is too absolute for trivial edge cases (e.g. "always write failing test first" for one-char typos) | Docs quality | Soften to "prefer X" or add an exception clause; include the reasoning so readers can judge edge cases |
34+
35+
---
36+
37+
## After Every Review
38+
39+
- [ ] All blockers have a concrete fix option stated.
40+
- [ ] Recurring Findings table updated with any new pattern.
41+
- [ ] If docs-only PR: every function name, path, and PR reference verified against
42+
live code before approving.

.claude/skills/fix-issue.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Fix Issue Skill
2+
3+
A fast-path workflow for diagnosing and fixing bugs in mxcli. Each fix appends
4+
to the symptom table below, so the next similar issue costs fewer reads.
5+
6+
## How to Use
7+
8+
1. Match the issue symptom to a row in the table — go straight to that file.
9+
2. Follow the fix pattern for that row.
10+
3. Write a failing test first, then implement.
11+
4. After the fix: **add a new row** to the table if the symptom is not already covered.
12+
13+
---
14+
15+
## Symptom → Layer → File Table
16+
17+
| Symptom | Root cause layer | First file to open | Fix pattern |
18+
|---------|-----------------|-------------------|-------------|
19+
| `DESCRIBE` shows `$var = LIST OPERATION ...;` | Missing parser case | `sdk/mpr/parser_microflow.go``parseListOperation()` | Add `case "Microflows$XxxType":` returning the correct struct |
20+
| `DESCRIBE` shows `$var = ACTION ...;` | Missing formatter case | `mdl/executor/cmd_microflows_format_action.go``formatActionStatement()` | Add `case *microflows.XxxAction:` with `fmt.Sprintf` output |
21+
| `DESCRIBE` shows `$var = LIST OPERATION %T;` (with type name) | Missing formatter case | `mdl/executor/cmd_microflows_format_action.go``formatListOperation()` | Add `case *microflows.XxxOperation:` before the `default` |
22+
| Compile error: `undefined: microflows.XxxOperation` | Missing SDK struct | `sdk/microflows/microflows_actions.go` | Add struct + `func (XxxOperation) isListOperation() {}` marker |
23+
| `TypeCacheUnknownTypeException` in Studio Pro | Wrong `$Type` storage name in BSON write | `sdk/mpr/writer_microflow.go` | Check the storage name table in CLAUDE.md; verify against `reference/mendixmodellib/reflection-data/` |
24+
| CE0066 "Entity access is out of date" | MemberAccess added to wrong entity | `sdk/mpr/writer_domainmodel.go` | MemberAccess must only be on the FROM entity (`ParentPointer`), not the TO entity — see CLAUDE.md association semantics |
25+
| CE0463 "widget definition changed" | Object property structure doesn't match Type PropertyTypes | `sdk/widgets/templates/` | Re-extract template from Studio Pro; see `sdk/widgets/templates/README.md` |
26+
| Parser returns `nil` for a known BSON type | Unhandled `default` in a `parseXxx()` switch | `sdk/mpr/parser_microflow.go` or `parser_page.go` | Find the switch by grepping for `default: return nil`; add the missing case |
27+
| MDL check gives "unexpected token" on valid-looking syntax | Grammar missing rule or token | `mdl/grammar/MDLParser.g4` + `MDLLexer.g4` | Add rule/token, run `make grammar` |
28+
| CE7054 "parameters updated" / CE7067 "does not support body entity" after `SEND REST REQUEST` | `addSendRestRequestAction` emitted wrong BSON: all params as query params, BodyVariable set for JSON bodies | `mdl/executor/cmd_microflows_builder_calls.go``addSendRestRequestAction` | Look up operation via `fb.restServices`; route path/query params with `buildRestParameterMappings`; suppress BodyVariable for JSON/TEMPLATE/FILE via `shouldSetBodyVariable` |
29+
30+
---
31+
32+
## TDD Protocol
33+
34+
Always follow this order — never implement before the test exists:
35+
36+
```
37+
Step 1: Write a failing unit test (parser test or formatter test)
38+
Step 2: Confirm it fails to compile or fails at runtime
39+
Step 3: Implement the minimum code to make it pass
40+
Step 4: Run: /c/Users/Ylber.Sadiku/go/go/bin/go test ./mdl/executor/... ./sdk/mpr/...
41+
Step 5: Add the symptom row to the table above if not already present
42+
```
43+
44+
Parser tests go in `sdk/mpr/parser_<domain>_test.go`.
45+
Formatter tests go in `mdl/executor/cmd_<domain>_format_<area>_test.go`.
46+
47+
---
48+
49+
## Issue #212 — Reference Fix (seeding example)
50+
51+
**Symptom:** `DESCRIBE MICROFLOW` showed `$var = LIST OPERATION ...;` for
52+
`Microflows$Find`, `Microflows$Filter`, `Microflows$ListRange`.
53+
54+
**Root cause:** `parseListOperation()` in `sdk/mpr/parser_microflow.go` had no
55+
cases for these three BSON types — they fell to `default: return nil`.
56+
57+
**Files changed:**
58+
| File | Change |
59+
|------|--------|
60+
| `sdk/microflows/microflows_actions.go` | Added `FindByAttributeOperation`, `FilterByAttributeOperation`, `RangeOperation` |
61+
| `sdk/mpr/parser_microflow.go` | Added 3 parser cases |
62+
| `mdl/executor/cmd_microflows_format_action.go` | Added 3 formatter cases |
63+
| `mdl/executor/cmd_microflows_format_listop_test.go` | Added 4 formatter tests |
64+
| `sdk/mpr/parser_listoperation_test.go` | New file, 4 parser tests |
65+
66+
**Key insight:** `Microflows$ListRange` stores offset/limit inside a nested
67+
`CustomRange` map — must cast `raw["CustomRange"].(map[string]any)` before
68+
extracting `OffsetExpression`/`LimitExpression`.
69+
70+
---
71+
72+
## After Every Fix — Checklist
73+
74+
- [ ] Failing test written before implementation
75+
- [ ] `go test ./mdl/executor/... ./sdk/mpr/...` passes
76+
- [ ] New symptom row added to the table above (if not already covered)
77+
- [ ] PR title: `fix: <one-line description matching the symptom>`

CLAUDE.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ Available namespaces: `DomainModels`, `Enumerations`, `Microflows`, `Pages`, `Mo
224224

225225
When reviewing pull requests or validating work before commit, verify these items:
226226

227+
### Bug fixes
228+
- [ ] **Fix-issue skill consulted** — read `.claude/skills/fix-issue.md` before diagnosing; match symptom to table before opening files
229+
- [ ] **Symptom table updated** — new symptom/layer/file mapping added to `.claude/skills/fix-issue.md` if not already covered
230+
- [ ] **Test written first** — failing test exists before implementation (parser test in `sdk/mpr/`, formatter test in `mdl/executor/`)
231+
227232
### Overlap & duplication
228233
- [ ] Check `docs/11-proposals/` for existing proposals covering the same functionality
229234
- [ ] Search the codebase for existing implementations (grep for key function names, command names, types)
@@ -356,11 +361,22 @@ mxcli new MyApp --version 10.24.0 --output-dir ./projects/my-app
356361

357362
Steps performed: downloads MxBuild → `mx create-project``mxcli init` → downloads correct Linux mxcli binary for devcontainer. The result is a ready-to-open project with `.devcontainer/`, AI tooling, and a working `./mxcli` binary.
358363

364+
### Slash Command Namespaces
365+
366+
Commands in `.claude/commands/` are organised by audience:
367+
368+
| Namespace | Folder | Invoked as | Purpose |
369+
|-----------|--------|------------|---------|
370+
| `mendix:` | `.claude/commands/mendix/` | `/mendix:lint` | mxcli **user** commands — synced to Mendix projects via `mxcli init` |
371+
| `mxcli-dev:` | `.claude/commands/mxcli-dev/` | `/mxcli-dev:review` | **Contributor** commands — this repo only, never synced to user projects |
372+
373+
Both namespaces are discoverable by typing `/mxcli` in Claude Code. Add new contributor tooling (review workflows, debugging helpers, etc.) under `mxcli-dev/`. Add commands intended for Mendix project users under `mendix/`.
374+
359375
### mxcli init
360376

361377
`mxcli init` creates a `.claude/` folder with skills, commands, CLAUDE.md, and VS Code MDL extension in a target Mendix project. Source of truth for synced assets:
362378
- Skills: `reference/mendix-repl/templates/.claude/skills/`
363-
- Commands: `.claude/commands/mendix/`
379+
- Commands: `.claude/commands/mendix/` (the `mxcli-dev/` folder is **not** synced)
364380
- VS Code extension: `vscode-mdl/vscode-mdl-*.vsix`
365381

366382
Build-time sync: `make build` syncs everything automatically. Individual targets: `make sync-skills`, `make sync-commands`, `make sync-vsix`.

mdl/executor/cmd_microflows_builder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type flowBuilder struct {
3333
reader *mpr.Reader // For looking up page/microflow references
3434
hierarchy *ContainerHierarchy // For resolving container IDs to module names
3535
pendingAnnotations *ast.ActivityAnnotations // Pending annotations to attach to next activity
36+
restServices []*model.ConsumedRestService // Cached REST services for parameter classification
3637
}
3738

3839
// addError records a validation error during flow building.

mdl/executor/cmd_microflows_builder_calls.go

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,16 @@ func (fb *flowBuilder) addSendRestRequestAction(s *ast.SendRestRequestStmt) mode
769769
// Build operation reference: Module.Service.Operation
770770
operationQN := s.Operation.String()
771771

772+
// Look up the operation definition to classify parameters and body kind.
773+
// s.Operation.Module = "MfTest", s.Operation.Name = "RC_TestApi.PostJsonTemplate"
774+
var opDef *model.RestClientOperation
775+
if fb.restServices != nil && s.Operation.Module != "" && strings.Contains(s.Operation.Name, ".") {
776+
dotIdx := strings.Index(s.Operation.Name, ".")
777+
serviceName := s.Operation.Name[:dotIdx]
778+
opName := s.Operation.Name[dotIdx+1:]
779+
opDef = lookupRestOperation(fb.restServices, serviceName, opName)
780+
}
781+
772782
// Build OutputVariable
773783
var outputVar *microflows.RestOutputVar
774784
if s.OutputVariable != "" {
@@ -778,29 +788,20 @@ func (fb *flowBuilder) addSendRestRequestAction(s *ast.SendRestRequestStmt) mode
778788
}
779789
}
780790

781-
// Build BodyVariable
791+
// Build BodyVariable only for EXPORT_MAPPING body kind.
792+
// For JSON / TEMPLATE / FILE bodies, the body expression lives on the
793+
// operation definition itself and must NOT be set here (CE7067).
782794
var bodyVar *microflows.RestBodyVar
783-
if s.BodyVariable != "" {
795+
if s.BodyVariable != "" && shouldSetBodyVariable(opDef) {
784796
bodyVar = &microflows.RestBodyVar{
785797
BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())},
786798
VariableName: s.BodyVariable,
787799
}
788800
}
789801

790-
// Build parameter mappings from WITH clause
791-
var paramMappings []*microflows.RestParameterMapping
792-
var queryParamMappings []*microflows.RestQueryParameterMapping
793-
for _, p := range s.Parameters {
794-
// Determine if path or query param by convention:
795-
// the executor can't distinguish at this level, so we emit both
796-
// and let the BSON field names sort it out. For now, emit as
797-
// query parameter mappings (most common use case).
798-
queryParamMappings = append(queryParamMappings, &microflows.RestQueryParameterMapping{
799-
Parameter: operationQN + "." + p.Name,
800-
Value: p.Expression,
801-
Included: "Yes",
802-
})
803-
}
802+
// Build parameter mappings, routing to ParameterMappings (path) or
803+
// QueryParameterMappings (query) based on the operation definition.
804+
paramMappings, queryParamMappings := buildRestParameterMappings(s.Parameters, opDef, operationQN)
804805

805806
// RestOperationCallAction does not support custom error handling (CE6035).
806807
// ON ERROR clauses in the MDL are silently ignored for this action type.
@@ -831,6 +832,84 @@ func (fb *flowBuilder) addSendRestRequestAction(s *ast.SendRestRequestStmt) mode
831832
return activity.ID
832833
}
833834

835+
// lookupRestOperation finds a specific operation in a consumed REST service list.
836+
func lookupRestOperation(services []*model.ConsumedRestService, serviceName, opName string) *model.RestClientOperation {
837+
for _, svc := range services {
838+
if svc.Name != serviceName {
839+
continue
840+
}
841+
for _, op := range svc.Operations {
842+
if op.Name == opName {
843+
return op
844+
}
845+
}
846+
}
847+
return nil
848+
}
849+
850+
// shouldSetBodyVariable returns true if a BodyVariable BSON field should be
851+
// emitted for a call to the given operation.
852+
// For JSON, TEMPLATE, and FILE body kinds, the body expression lives on the
853+
// operation definition and must not be overridden by a BodyVariable (CE7067).
854+
// For EXPORT_MAPPING, the caller provides an entity to export via BodyVariable.
855+
// When the operation definition is unknown (nil), we preserve old behaviour and
856+
// set BodyVariable so the caller's intent is not silently dropped.
857+
func shouldSetBodyVariable(op *model.RestClientOperation) bool {
858+
if op == nil {
859+
return true // unknown operation — preserve caller intent
860+
}
861+
switch op.BodyType {
862+
case "JSON", "TEMPLATE", "FILE":
863+
return false
864+
default:
865+
// EXPORT_MAPPING or empty (no body) — only set if EXPORT_MAPPING
866+
return op.BodyType == "EXPORT_MAPPING"
867+
}
868+
}
869+
870+
// buildRestParameterMappings splits parameter bindings from a SEND REST REQUEST
871+
// WITH clause into path parameter mappings and query parameter mappings,
872+
// using the operation definition to determine which is which.
873+
// When op is nil (operation not found), all parameters fall back to query
874+
// parameter mappings (preserves old behaviour).
875+
func buildRestParameterMappings(
876+
params []ast.SendRestParamDef,
877+
op *model.RestClientOperation,
878+
operationQN string,
879+
) ([]*microflows.RestParameterMapping, []*microflows.RestQueryParameterMapping) {
880+
if len(params) == 0 {
881+
return nil, nil
882+
}
883+
884+
// Build lookup sets from the operation definition.
885+
pathParamSet := map[string]bool{}
886+
if op != nil {
887+
for _, p := range op.Parameters {
888+
pathParamSet[p.Name] = true
889+
}
890+
}
891+
892+
var pathMappings []*microflows.RestParameterMapping
893+
var queryMappings []*microflows.RestQueryParameterMapping
894+
895+
for _, p := range params {
896+
if pathParamSet[p.Name] {
897+
pathMappings = append(pathMappings, &microflows.RestParameterMapping{
898+
Parameter: operationQN + "." + p.Name,
899+
Value: p.Expression,
900+
})
901+
} else {
902+
queryMappings = append(queryMappings, &microflows.RestQueryParameterMapping{
903+
Parameter: operationQN + "." + p.Name,
904+
Value: p.Expression,
905+
Included: "Yes",
906+
})
907+
}
908+
}
909+
910+
return pathMappings, queryMappings
911+
}
912+
834913
// addExecuteDatabaseQueryAction creates an EXECUTE DATABASE QUERY statement.
835914
func (fb *flowBuilder) addExecuteDatabaseQueryAction(s *ast.ExecuteDatabaseQueryStmt) model.ID {
836915
// DynamicQuery is a Mendix expression — string literals need single quotes

mdl/executor/cmd_microflows_builder_control.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ func (fb *flowBuilder) addLoopStatement(s *ast.LoopStmt) model.ID {
287287
measurer: fb.measurer, // Share measurer
288288
reader: fb.reader, // Share reader
289289
hierarchy: fb.hierarchy, // Share hierarchy
290+
restServices: fb.restServices, // Share REST services for parameter classification
290291
}
291292

292293
// Process loop body statements and connect them with flows
@@ -360,6 +361,7 @@ func (fb *flowBuilder) addWhileStatement(s *ast.WhileStmt) model.ID {
360361
measurer: fb.measurer,
361362
reader: fb.reader,
362363
hierarchy: fb.hierarchy,
364+
restServices: fb.restServices,
363365
}
364366

365367
var lastBodyID model.ID

mdl/executor/cmd_microflows_builder_flows.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func (fb *flowBuilder) addErrorHandlerFlow(sourceActivityID model.ID, sourceX in
6666
measurer: fb.measurer,
6767
reader: fb.reader,
6868
hierarchy: fb.hierarchy,
69+
restServices: fb.restServices,
6970
}
7071

7172
var lastErrID model.ID

0 commit comments

Comments
 (0)