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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are.
- learn(skills/lint-that-actually-runs.md): **three more lint vocabulary facts, verified on v0.24.0, with a before/after rule.** `microflow_type` is `MICROFLOW`/`NANOFLOW`/`RULE`, and `microflows()` yields all three. SQL `PERSISTENT` is Starlark `Persistent`. `activity_count` counts top-level objects (splits, loops, annotations), not actions, and `activities_for()` does not see loop bodies. — field report from a colleague's naming-conventions project (mxcli v0.23.0, Mendix 11.12.4)
- learn(skills/learned-microflow-patterns.md): **expression functions take positional arguments only — `toString(from: $X)` is not a syntax error, it's a silent misparse.** MDL's expression grammar has no named-argument form for function calls; `label: value` parses as `label` (a bare unresolved identifier) COLON-divided by `value` — COLON being the OQL division operator. Verified on mxcli v0.24.0 / Mendix 11.12.1, scratch copy of a PoC model: `toString(from: $Temperature)` and `formatDateTime($D, pattern: 'yyyy-MM-dd')` both pass `mxcli check --references` silently (exit 0) because the collapsed argument count still matches the function's arity; `substring(from: $S, index: 0, length: 3)` fails, but with a misleading `E006` ("expects 2 to 3 argument(s), got 1") that never names labelled arguments as the cause. The likely source: labelled-colon syntax is genuine MDL elsewhere (page `Attribute:`/`Action: MICROFLOW Name(Param: val)`, `@anchor(from: bottom, to: top)`) — just never inside an expression — workshop feedback, Sep 2026
- fix(bin/doctor.sh): **the gate self-test's scratch copy now carries the whole model directory, not just `.mpr` + `mprcontents/`.** A `--target=deploy` build (the same target the real gate runs, in place, against the project as it sits on disk) resolves widget/theme/design-property references out of `theme/`, `resources/`, `widgets/` and `javasource/` sitting beside the `.mpr`; a thin copy reported hundreds to over a thousand `Could not find widget` / design-property errors as a "dirty baseline" while the real gate passed with 0 errors on the identical model. Now copies the directory holding the `.mpr` (single-tree root or `app/` on a two-tree checkout) minus `.git/`, `deployment/`, `node_modules/` and `.mpr-snapshots/`, and prints the copied size (`du -sh`) so the cost is visible. Keeps the existing real-basename fix. A sibling `.mpr` in the project's own directory was ruled out (collides with the project's own `mprcontents/`); `mxbuild --target=check` was ruled out (no evidence it exists anywhere in this toolkit's usage) — field reports, issue #127 and a macOS custom-theme project
- fix(project-bin/exec.sh): **the mxbuild delta gate now keeps a write when the post-exec error set is a strict subset of the pre-flight baseline, not just when it is identical.** A script that took 34 pre-existing errors down to 1 was being rolled back and logged `blocked: PRE-EXISTING CE1613`, because the gate could only recognise "unchanged," never "reduced." The comparison key is unchanged (`err_set`'s per-message string); a new `is_subset_of` helper checks membership, and a set containing anything NEW still restores the snapshot exactly as before. The kept-write BUILD-LOG row now reads `⚠️ applied (dirty model, reduced)` with detail `errors 34→1 (pre-existing, reduced); remaining [...] still blocks deploy` — field report from a colleague's naming-conventions project (mxcli v0.23.0)
- learn(skills/learned-file-upload-widget.md): **a file upload mxcli can author, with proof that it uploads.** The Mendix File Uploader 2.5.0 bound to a `System.FileDocument` specialisation: the MDL shape (entities, grants, create/delete microflows, advanced formats), which upload widgets mxcli cannot author (classic FileManager, PDS uploader), the two traps with workarounds (a simple-mode `allowedfileformat` passes exec and fails `mx check` with CE0463; an uploader DESCRIBE will not re-exec, `exposes 2 datasources`), and the six-step upload instrument. Field run on stock v0.24.0: the section-4 MDL taken verbatim from the skill gave `mx check` 0 errors, 2/2 files stored, 2/2 downloads sha256-equal, `.csv` rejected with 0 rows; both traps reproduce unchanged on v0.24.0 — a Mendix app-rebuild project
- new(skills/mendix-best-practices-index.md): **one row per Mendix best-practice area: the Mendix docs page, the bundled `assess-quality` section, the toolkit skill that applies it before the write, and the `mxcli lint` rule that catches it after exec.** An index, not a copy — the practice text stays on the Mendix pages (17 URLs verified HTTP 200 on 2026-09-25) and in the mxcli-bundled skill; Mendix's own Best Practice Recommender rules (MXP001–016) anchor the performance rows, and four rows say out loud that no lint rule exists and the preflight checklist is the only check. Routed `all` agents, stages 3/5/6, group reference — Maurits Visser
- learn(skills/microflow-preflight.md, agents/mdl-agent.md): **a microflow now gets a tier before any MDL — Simple, Guided or Split-first — and Split-first means a posted split plan (thin orchestrator + one `SUB_` per responsibility, with signatures) that the user confirms first.** Prompted by a colleague's session refusing a long microflow as "too difficult" while the same task went through on a stronger model: the piece was too big, not the task. mdl-agent gains two rules — never hand back "too difficult", hand back the split plan; escalate one failing `SUB_` by name after one retry, never the whole script. Also records the mxcli team's answer on positioning: a standalone `mxcli layout` command — which on v0.24.0 and upstream main (2026-09-25) arranges domain models only (`--dry-run` on a scratch copy of a PoC model: 10 entity moves, no microflow), so the no-`@position` rule and the if-branch workaround stand until a microflow mode ships; noted in the bug ledger and `learned-microflow-patterns.md` — Maurits Visser
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ Use this before every write. Full per-rule detail (root causes, bug IDs, retest
| Drop an attribute that has security grants | Studio Pro GUI | N/A |
| After any MPR corruption or load error | `bin/restore-mpr.sh` | Closed |

**The crash net.** An MPR is two parts: `Project.mpr` (SQLite index) and `mprcontents/` (BSON units). `bin/exec.sh` snapshots both before every batch; 5 rotate; `bin/restore-mpr.sh` rolls back both together (either alone is useless). Git commits at phase gates are the real history. Ad-hoc `.mpr.backup` copies are banned.
**The crash net.** An MPR is two parts: `Project.mpr` (SQLite index) and `mprcontents/` (BSON units). `bin/exec.sh` snapshots both before every batch; 5 rotate; `bin/restore-mpr.sh` rolls back both together (either alone is useless). Git commits at phase gates are the real history. Ad-hoc `.mpr.backup` copies are banned. By design, `exec.sh` refuses to run at all while the model has uncommitted changes — its snapshot would not cover them, so a later auto-restore could silently lose that work; commit the model first (`FORCE_EXEC=1` overrides, at your own risk).

### Something went wrong? Don't panic.

Expand Down
75 changes: 70 additions & 5 deletions project-bin/exec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
# exec.sh — the guard chain around a model write.
#
# concurrent-writer guard → module-brief advisory → mxcli check → snapshot → baseline → exec
# → mxbuild gate → auto-restore on regression → lint ratchet → SP reopen
# → mxbuild delta gate → auto-restore only on a NEW error → lint ratchet → SP reopen
#
# The mxbuild gate is a DELTA gate, not an absolute "0 errors" one: a write is kept
# when the post-exec error set is the pre-flight baseline exactly, or a strict
# SUBSET of it (every post-exec error message already existed before this script
# ran — nothing new, even if some were cleared). Only a set containing something
# new triggers the snapshot restore. See "Delta gate" below.
# Known limit: errors are compared by message text only, so a new error whose
# message matches one already in the baseline is not seen as new.
#
# Usage: ./bin/exec.sh <script.mdl>
# ./bin/exec.sh --patch <script> [args...]
Expand All @@ -23,6 +31,10 @@
# the project's allow-list (install-claude-permissions.sh), so in auto mode Claude Code's
# classifier does not review the script. Read what a patch does outside the .mpr before running it.
#
# Refuses to run while the model has UNCOMMITTED changes (by design: the snapshot
# taken below would not cover them, so a later auto-restore could silently lose
# that work) — commit the model first, or FORCE_EXEC=1 to override at that risk.
#
# Overrides: FORCE_EXEC=1 (skip refusals), SKIP_CHECK=1 (skip the pre-exec
# mxcli check), SKIP_BASELINE=1 (skip pre-flight mxbuild),
# MXBUILD_PATH=..., MENDIX_APP=..., MPR_FILE=...,
Expand Down Expand Up @@ -547,6 +559,34 @@ err_set() {
"$PY" -c "import json;d=json.load(open('$(native_path "$1")'));print('|'.join(sorted(x.get('message','') for x in d.get('problems',[]) if x.get('severity')=='Error')))" 2>/dev/null || echo ""
}

# is_subset_of <candidate> <superset> — both are err_set's own "|"-joined sorted
# message strings (the SAME comparison key the identical-baseline check already
# used; this does not introduce a new one). True (exit 0) when every message in
# <candidate> also appears in <superset> — i.e. nothing NEW. An empty <candidate>
# (0 post-exec errors) is trivially a subset. Bash-native, no extra Python call:
# both strings are already in hand as plain variables by the time this runs.
# Pure POSIX word-splitting on IFS='|', no arrays/`read -a` — bash 3.2 / Git Bash
# safe. Messages containing a literal "|" would break the membership test the
# same way they already break the exact-equality test above; not new exposure.
is_subset_of() {
_cand="$1"
[ -z "$_cand" ] && return 0
_super="|$2|"
_oldIFS="$IFS"
IFS='|'
for _e in $_cand; do
IFS="$_oldIFS"
[ -z "$_e" ] && continue
case "$_super" in
*"|$_e|"*) : ;;
*) return 1 ;;
esac
IFS='|'
done
IFS="$_oldIFS"
return 0
}

# ── Doctor freshness (warn-only) ─────────────────────────────────────────────
# doctor.sh used to run once, at install, and never again. A machine that drifts mid-project
# (new mxcli, a wrong-arch mxbuild, an endpoint agent slowing every fork) then reads as "the
Expand Down Expand Up @@ -724,14 +764,39 @@ if [ -x "$MXBUILD" ] && [ -x "$JAVA_EXE" ]; then
# ── Delta gate ─────────────────────────────────────────────────────────
# A shared model means another workstream can leave it non-building; an
# absolute "zero errors" gate would then block every good script forever.
# Keeps the write when the POST-exec error set is the baseline exactly
# (nothing changed), OR a STRICT SUBSET of it (every post-exec message
# already existed pre-exec — no new message, even though some
# pre-existing ones may have been cleared). Messages only: a new error
# with the same text as a baseline one passes (known limit, see header).
# Only a set with something NEW in it restores the snapshot.
# Real incident: a script that took 34
# pre-existing errors down to 1 was rolled back and logged
# "blocked: PRE-EXISTING CE1613" because the gate could only recognise
# "unchanged," never "reduced." Comparison key is unchanged — err_set's
# per-message string (see err_set above) — subset-checked by
# is_subset_of, not switched to a different key.
POST_SET=$(err_set "$ERRORS_FILE")
DELTA_KIND=""
if [ -n "$BASELINE_SET" ] && [ "$BASELINE_SET" = "$POST_SET" ]; then
DELTA_KIND="identical"
elif [ -n "$BASELINE_SET" ] && is_subset_of "$POST_SET" "$BASELINE_SET"; then
DELTA_KIND="subset"
fi
if [ -n "$DELTA_KIND" ]; then
GATE_STATE="pass"
KEEP_CODES=$(err_codes "$ERRORS_FILE")
echo " ⚠ mxbuild: $CE_COUNT error(s) — IDENTICAL to the pre-flight baseline."
echo " Pre-existing [$KEEP_CODES], not introduced by this script. KEEPING the changes."
echo " The model still will not deploy until those are cleared in Studio Pro."
[ "$EXEC_STATUS" -eq 0 ] && log_build "⚠️ applied (dirty model)" "no new errors; pre-existing $KEEP_CODES still blocks deploy"
if [ "$DELTA_KIND" = "subset" ]; then
echo " ⚠ mxbuild: $CE_COUNT error(s) — a STRICT SUBSET of the $_BC pre-flight baseline error(s), no new ones."
echo " Pre-existing [$KEEP_CODES] remain; this script reduced the error count. KEEPING the changes."
echo " The model still will not deploy until those are cleared in Studio Pro."
[ "$EXEC_STATUS" -eq 0 ] && log_build "⚠️ applied (dirty model, reduced)" "errors ${_BC}→${CE_COUNT} (pre-existing, reduced); remaining [$KEEP_CODES] still blocks deploy"
else
echo " ⚠ mxbuild: $CE_COUNT error(s) — IDENTICAL to the pre-flight baseline."
echo " Pre-existing [$KEEP_CODES], not introduced by this script. KEEPING the changes."
echo " The model still will not deploy until those are cleared in Studio Pro."
[ "$EXEC_STATUS" -eq 0 ] && log_build "⚠️ applied (dirty model)" "no new errors; pre-existing $KEEP_CODES still blocks deploy"
fi
cp "$ERRORS_FILE" "$LAST_ERRS"
rm -f "$ERRORS_FILE"
else
Expand Down
Loading