From 842ce40d9bb24bfb9ab46f56a6024d157e28690d Mon Sep 17 00:00:00 2001 From: r Date: Fri, 25 Sep 2026 14:57:38 +0000 Subject: [PATCH 1/2] fix(project-bin/exec.sh): delta gate accepts a strict-subset error reduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mxbuild delta gate previously kept a write only when the post-exec error set matched the pre-flight baseline exactly. A script that took 34 pre-existing errors down to 1 was rolled back and logged "blocked: PRE-EXISTING CE1613", because the gate had no way to recognise "reduced" as distinct from "unchanged but different." Adds a bash-native is_subset_of() helper (no new Python call, no new comparison key — reuses err_set()'s existing per-message string) and classifies the post-exec set as identical / subset / reject. Only a set containing something genuinely NEW still triggers the snapshot restore. The kept-write BUILD-LOG row for the reduced case now reads "applied (dirty model, reduced)" with the before->after counts and remaining codes in the detail column. Also documents (header comment + README's crash-net paragraph) that exec.sh refuses to run while the model has uncommitted changes, since the delta-gate change touches the same header block. Verified against a scratch harness (not committed) using the real extracted err_set/err_codes/is_subset_of functions fed two synthetic mxbuild error-JSON fixtures: 34->1 subset accepted, 34->35 (one new code) rejected, plus guard cases for the identical-set and empty-set paths. No real .mpr/mxcli/mxbuild was exercised. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + README.md | 2 +- project-bin/exec.sh | 71 +++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30ebae9..2a2d4a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ three commits past it), and a bug report can name a release instead of a sha nob Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased +- 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 diff --git a/README.md b/README.md index 35d27d6..a3cdd5b 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/project-bin/exec.sh b/project-bin/exec.sh index 66d6b1d..d4a4e9c 100755 --- a/project-bin/exec.sh +++ b/project-bin/exec.sh @@ -2,10 +2,20 @@ # 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 already existed before this script ran — no +# new error code+location, even if some were cleared). Only a set containing +# something new triggers the snapshot restore. See "Delta gate" below. # # Usage: ./bin/exec.sh # +# 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=..., @@ -470,6 +480,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 — 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 +# also appears in — i.e. nothing NEW. An empty +# (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 @@ -601,14 +639,37 @@ 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 error code+location, even though some + # pre-existing ones may have been cleared). 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 From 6c29a8862bc8da6bfe67a2d3882794ce121b35dd Mon Sep 17 00:00:00 2001 From: r Date: Tue, 29 Sep 2026 08:43:28 +0000 Subject: [PATCH 2/2] exec.sh: delta-gate comments say messages are compared, not code+location The comparison key is err_set's message text. The header and the delta-gate comment claimed code+location; state what the code does and name the known limit (a new error with the same text as a baseline one is not seen as new). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- project-bin/exec.sh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/project-bin/exec.sh b/project-bin/exec.sh index e3ec881..4f091c3 100755 --- a/project-bin/exec.sh +++ b/project-bin/exec.sh @@ -6,9 +6,11 @@ # # 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 already existed before this script ran — no -# new error code+location, even if some were cleared). Only a set containing -# something new triggers the snapshot restore. See "Delta gate" below. +# 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 # ./bin/exec.sh --patch