diff --git a/CHANGELOG.md b/CHANGELOG.md index af9972de..2d5ebb2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- fix(exec): **a script restated from a stale DESCRIBE now WARNs instead of silently reverting an earlier exec** (#261). A script drafted from a DESCRIBE taken before an earlier script of the batch ran put four old messages back, with check, mxbuild and lint green. New `project-bin/describe-baseline.sh stamp` writes `-- baseline: sha256=` (plus `-- baseline-mxcli: `) from a DESCRIBE at draft time; `exec.sh` runs `verify` only for a script that carries the header, so other scripts cost one grep and no mxcli call. It reports DRIFT with the first lines of the diff, STALEHEADER, and RESTAMP after an mxcli upgrade (DESCRIBE text may change, so that is not called drift). A re-run by the script's own last recorded write (the #269 ledger) and a fresh model are not drift. Nothing is refused and the exit codes do not change. Preflight STOP 21 gains one clause (`-- supersedes:`, `stamp`, exec.sh only warns) and its trigger names `create or modify` (+24 words). Not-found DESCRIBE and `--version` output are captured from mxcli v0.26.0. Lands after the #269 PR; with it, supersedes draft #311. **Behaviour change to check in your project:** `exec.sh` output can carry `WARN: stale draft: …` lines, and the BUILD-LOG detail cell `WARN stale baseline: `; `MXTK_RESTATE_GUARD=off` skips the check. — AlvinnYeo (#261) +- fix(exec): **re-running an older `mdlsource/` script over a newer owner's live work now WARNs instead of passing silently** (#269). Three field apps had 99/579, 130/357 and 38/159 documents created by more than one script, and a replay of an older one reverted the newer with exit 0 and every gate green. New `project-bin/script-owners.sh` maps each microflow, nanoflow, page and snippet to the scripts that create or restate it, in run order (digit runs compared as numbers, so `2-core/` runs before `11-fixes/`; `done-` ignored), and reads `-- supersedes:` / `-- superseded-by:` headers. `exec.sh` records every kept write in a machine-local last-writer ledger (`.claude/loop/script-owners/`, self-gitignored, compacted per write); `restore-mpr.sh` marks it on a manual restore. A replay WARNs only when the newer owner's write is live: ledger first, else one DESCRIBE per document, at most 5 per exec (the rest print `CONFLICT (not checked)`). A new owner without a supersedes header gets one `NOTE:` line on its first run, kept out of BUILD-LOG. Nothing is refused and the exit codes do not change. With no co-owner, `exec.sh` runs one grep over `mdlsource/` and no mxcli call. `gate-check.sh` prints the co-owned count when it is above 0. Part of the rebuilt draft #311 (with the #261 PR). **Behaviour change to check in your project:** `exec.sh` output can carry `WARN: restatement: …` and `NOTE: new owner of …` lines; the BUILD-LOG detail cell can carry `WARN reverts: `. Set `MXTK_RESTATE_GUARD=off` to skip the check. — MendixMau (#269) +- fix(exec): **re-running an older `mdlsource/` script over a newer owner's live work now WARNs instead of passing silently** (#269). Three field apps had 99/579, 130/357 and 38/159 documents created by more than one script, and a replay of an older one reverted the newer with exit 0 and every gate green. New `project-bin/script-owners.sh` maps each microflow, nanoflow, page and snippet to the scripts that create or restate it, in run order (digit runs compared as numbers, so `2-core/` runs before `11-fixes/`; `done-` ignored), and reads `-- supersedes:` / `-- superseded-by:` headers. `exec.sh` records every kept write in a machine-local last-writer ledger (`.claude/loop/script-owners/`, self-gitignored, compacted per write); `restore-mpr.sh` marks it on a manual restore. A script counts as run when it has a ledger row, a `done-` name, or an applied `docs/BUILD-LOG.md` row, so a fresh clone or cloud session (empty ledger) still warns on a replay over committed newer work; a script nobody ran yet (draft-ahead) stays quiet. The ledger cannot see Studio Pro edits or a `git pull`/`checkout` of the `.mpr`. A replay WARNs only when the newer owner's write is live: ledger first, else one DESCRIBE per document, at most 5 per exec (the rest print `CONFLICT (not checked)`). A new owner without a supersedes header gets one `NOTE:` line on its first run, kept out of BUILD-LOG. Nothing is refused and the exit codes do not change. With no co-owner, `exec.sh` runs one grep over `mdlsource/` and no mxcli call. `gate-check.sh` prints the co-owned count when it is above 0. Part of the rebuilt draft #311 (with the #261 PR). **Behaviour change to check in your project:** `exec.sh` output can carry `WARN: restatement: …` and `NOTE: new owner of …` lines; the BUILD-LOG detail cell can carry `WARN reverts: `. Set `MXTK_RESTATE_GUARD=off` to skip the check. — MendixMau (#269) - fix(exec): **re-running an older `mdlsource/` script over a newer owner's live work now WARNs instead of passing silently** (#269). Three field apps had 99/579, 130/357 and 38/159 documents created by more than one script, and a replay of an older one reverted the newer with exit 0 and every gate green. New `project-bin/script-owners.sh` maps each microflow, nanoflow, page and snippet to the scripts that create or restate it, in run order (digit runs compared as numbers, so `2-core/` runs before `11-fixes/`; `done-` ignored), and reads `-- supersedes:` / `-- superseded-by:` headers. `exec.sh` records every kept write in a machine-local last-writer ledger (`.claude/loop/script-owners/`, self-gitignored, compacted per write); `restore-mpr.sh` marks it on a manual restore. A script counts as run when it has a ledger row, a `done-` name, or an applied `docs/BUILD-LOG.md` row, so a fresh clone or cloud session (empty ledger) still warns on a replay over committed newer work; a script nobody ran yet (draft-ahead) stays quiet. The ledger cannot see Studio Pro edits or a `git pull`/`checkout` of the `.mpr`. A replay WARNs only when the newer owner's write is live: ledger first, else one DESCRIBE per document, at most 5 per exec (the rest print `CONFLICT (not checked)`). A new owner without a supersedes header gets one `NOTE:` line on its first run, kept out of BUILD-LOG. Nothing is refused and the exit codes do not change. With no co-owner, `exec.sh` runs one grep over `mdlsource/` and no mxcli call. `gate-check.sh` prints the co-owned count when it is above 0. Part of the rebuilt draft #311 (with the #261 PR). **Behaviour change to check in your project:** `exec.sh` output can carry `WARN: restatement: …` and `NOTE: new owner of …` lines; the BUILD-LOG detail cell can carry `WARN reverts: `. Set `MXTK_RESTATE_GUARD=off` to skip the check. — MendixMau (#269) - fix(root): **the installed and toolkit copies of `_common.sh` now resolve the same project root from inside an app folder** (#489). With the cwd in `app/`, `app/javasource` or `apps//`, the installed copy (`/bin/_common.sh`, tier 2) answered the project while the toolkit copy (tier 3) stopped at the folder holding the `.mpr`, so the same script wrote its look ledger, metrics ledger and BUILD-LOG to two roots depending on where it was run from. `mxtk_walk_root` now returns the project folder when the folder it stops at is `

/app` or `

/apps/` and `

` is a marked project folder (an installed `bin/_common.sh`, or `.claude/` beside `PROJECT.md`); an unmarked app folder is still returned as itself, and so is an app folder that is a project of its own (its own install, or `.claude/` beside `PROJECT.md`); the folder names match in any letter case, and single-tree resolution is unchanged. Builtin tests only, no extra process. `find_mpr` now always prints an absolute path (a cwd-relative `MPR_FILE` broke once a caller did `cd "$PROJECT_ROOT"`), and with several apps and no `MPR_FILE`, sourcing `_common.sh` from inside one app's folder exports `MPR_FILE` as that app's model, so the pick survives exec.sh's `cd "$PROJECT_ROOT"` and reaches `snapshot-mpr.sh` (outside any one app nothing is picked and `find_mpr` still refuses; an explicit `MPR_FILE` or a root/`app/` model always wins; the pick is marked `MXTK_MPR_PICKED`, so a child script whose cwd is inside a different `apps//` re-derives instead of inheriting it, and `model-stamp.sh` says "(picked from your cwd)" when it refuses one). `lint-gate.sh` now honours `MPR_FILE` when `MPR` is unset and it resolves under the project root, so exec.sh run from inside one app no longer reaches its "several apps" refusal; an `MPR_FILE` that does not resolve there (cwd-relative, or absolute through a symlinked path) falls back to discovery as before, never to exit 2. An inherited pick from another project is dropped. **Behaviour change to check in your project:** a toolkit-copy script run from inside `app/` or `apps//` of a wired project now reads and writes state at the project root, as the installed copy already did; if `app/` (or `apps//`) holds `.mpr-snapshots/`, `docs/BUILD-LOG.md` or `.claude/loop/` from a toolkit-copy run, merge them into the project root. `find_mpr` with `MPR_FILE` set now prints an absolute path where it echoed a relative one. A project whose `bin/_common.sh` is hand-edited needs `sync-project.sh --only-bin _common.sh --upgrade-bin _common.sh`, as for #420. `test-common-root-multiapp.sh` R8a, R9c-f, R13g, R17a-j and R18a-h (cwd in `app/`, `app/javasource`, `apps/A`, `apps/A/javasource`, a nested or wired app folder, `App/`, both copies; the cwd's app surviving a `cd` to the root and a child `snapshot-mpr.sh`; a child in another app re-deriving, R18h a child in another project): 67/67 with the fix; `test-lint-gate-accept.sh` T-MA (lint-gate with `MPR_FILE` in a multi-app folder) and T-MF (an unresolvable `MPR_FILE` falls back to discovery). — MendixMau (review 2026-10-20) - fix(portability): **`bug-lookup.sh --index` and `test-routing-tier.sh` no longer depend on sed expanding `\t`, and `check-portability.sh` flags `\t` in a sed pattern or replacement outside a bracket** (#488). BSD sed (macOS) reads `\t` there as the letter `t`, so `sed -E 's/^([0-9]+):## /\1\t/'` fed `awk -F'\t'` no tab and `--index` printed 222 empty lines; the line-count check passed anyway. The index is now one awk (same 222 lines, byte-identical on GNU), the fixture builds its sed script with a real tab (`TAB=$'\t'`), and `test-bug-lookup.sh` runs `--index` under a sed shim that reads `\t` as `t` (fails on master: 0 non-empty lines). The lint also had a blind spot: its comment filter dropped any line holding `:#`, so the bug-lookup line was never checked; it is now anchored on the line number. New rule over the current tree: the two sites on master, 0 elsewhere. `test-sed-tab-bracket.sh` T4. Simulated on Linux only; one macOS run is still owed. — MendixMau (review 2026-10-20) diff --git a/bin/lib/install-manifest.sh b/bin/lib/install-manifest.sh index 33ab0bc7..f674fcba 100644 --- a/bin/lib/install-manifest.sh +++ b/bin/lib/install-manifest.sh @@ -121,7 +121,9 @@ MXTK_AGENTS_STAGE_BUILD="mdl-agent.md gate-agent.md test-agent.md review-agent.m # anchors, a wireframe or a BRD, and gate-check.sh check_stage_3 fails a stale render. # script-owners.sh joined on 2026-10-21 (#269): exec.sh runs it as its sibling before every MDL # write (co-owned documents) and records each kept write in its ledger; restore-mpr.sh marks it. -MXTK_PROJECT_BIN="process-map.js process-map.template.html app-restart.sh test-scratch.sh script-owners.sh assemble-prototype.js prototype-route.js check-prototype-links.js wf-add-path-terminators.py wf-set-call-captions.py mxunit_bson.py mxpatch.py _common.sh app-facts.sh _claims.sh snapshot-mpr.sh restore-mpr.sh exec.sh affected-tests.sh look-ledger.sh save-sp.sh restart-sp.sh check-sp-health.sh verify-module.sh test-stack-up.sh fixture-manifest.sh check-root-clean.sh lint-gate.sh lint-trend.sh metrics-ledger.sh metrics-trend.sh close-task.sh conformance-check.sh coverage-preflight.sh graph-sweep.sh review-module.sh coherence-cadence.sh build-plan-status.sh progress-report.py done-drift-check.sh page-scope.sh render-improvement-register.sh check-design-portability.sh check-design-reaches-app.sh check-page-shell.sh page-fidelity.js model-stamp.sh verify-model.sh install-project-hooks.sh mx-metadata-note.py session-check.sh constants-audit.sh context-pack.sh check-staged-secrets.sh" +# describe-baseline.sh joined the same day (#261): exec.sh runs its verify for a script with a +# `-- baseline:` header, and it reads script-owners.sh's ledger as ITS sibling. +MXTK_PROJECT_BIN="process-map.js process-map.template.html app-restart.sh test-scratch.sh script-owners.sh describe-baseline.sh assemble-prototype.js prototype-route.js check-prototype-links.js wf-add-path-terminators.py wf-set-call-captions.py mxunit_bson.py mxpatch.py _common.sh app-facts.sh _claims.sh snapshot-mpr.sh restore-mpr.sh exec.sh affected-tests.sh look-ledger.sh save-sp.sh restart-sp.sh check-sp-health.sh verify-module.sh test-stack-up.sh fixture-manifest.sh check-root-clean.sh lint-gate.sh lint-trend.sh metrics-ledger.sh metrics-trend.sh close-task.sh conformance-check.sh coverage-preflight.sh graph-sweep.sh review-module.sh coherence-cadence.sh build-plan-status.sh progress-report.py done-drift-check.sh page-scope.sh render-improvement-register.sh check-design-portability.sh check-design-reaches-app.sh check-page-shell.sh page-fidelity.js model-stamp.sh verify-model.sh install-project-hooks.sh mx-metadata-note.py session-check.sh constants-audit.sh context-pack.sh check-staged-secrets.sh" # Files in project-bin/ that are deliberately NOT installed into projects. The reverse check # below flags anything named by NEITHER list, so a new file in project-bin/ has to be either diff --git a/project-bin/describe-baseline.sh b/project-bin/describe-baseline.sh new file mode 100755 index 00000000..3154da80 --- /dev/null +++ b/project-bin/describe-baseline.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# describe-baseline.sh — record, at draft time, what each document a script restates looked like; +# say, at exec time, whether it still looks that way. +# +# bin/describe-baseline.sh stamp DESCRIBE every document the script restates and +# write one header line per document at the top: +# -- baseline: Module.Doc sha256= +# -- baseline: Module.Doc absent +# -- baseline-mxcli: v0.26.0 +# Re-stamping replaces the old lines. Run it right +# after the DESCRIBE you drafted from. +# bin/describe-baseline.sh verify re-DESCRIBE each baselined document and compare. +# exit 0 all match · 1 a row to warn on: DRIFT, +# RESTAMP (mxcli upgraded since the stamp: DESCRIBE +# text may differ, so not called drift) or +# STALEHEADER · 2 no baseline header · 3 no DESCRIBE +# bin/describe-baseline.sh live prints sha256= or absent +# +# verify reads SAME, not DRIFT, in the two cases that are not a stale draft: +# the last recorded writer of the document is this script (a re-run after a clean exec; +# `script-owners.sh last-writer`, the ledger exec.sh writes after every kept write), and +# a baselined document is absent (a rebuild onto a fresh model: the script creates it). +# +# COST. Opt-in: only a script that carries a `-- baseline:` header pays anything, one DESCRIBE +# per baselined document (one `mxcli -c` each; 0.25 s on a small public test model, v0.26.0). +# A script with no header costs exec.sh one sed over the script and no mxcli call. +# +# "Restates" = `create or modify|create or replace microflow|nanoflow|page|snippet`, the +# statements whose body replaces whatever is live (script-owners.sh docs, one parser). A plain +# `create` fails on an existing name, so it needs no baseline. +# +# WHY (#261, field 2026-10-10, Mendix 11.14.0, mxcli v0.24.0). Script A changed the messages of +# three microflows. Script B, drafted in the same batch from a DESCRIBE taken BEFORE A ran, +# restated them whole; executing B put four old messages back. check --references, mxbuild +# (0 errors) and lint all passed: a whole restatement is valid MDL whatever it replaces. Caught +# later by a module review and a failing end-to-end journey. The draft-ahead pattern (agents +# drafting later rows while earlier rows execute) makes this the default outcome, not a fluke. +# +# The hash is of the DESCRIBE text (CR stripped). Any change (another script, Studio Pro, a hand +# fix) moves it. A moved hash is not proof of loss, only that the drafter's picture is stale: +# re-DESCRIBE, merge, re-stamp. The DESCRIBE text is kept in .claude/loop/describe-baseline/ +# .mdl so verify can print the first lines of the diff. +# +# What a baseline cannot see: anything DESCRIBE does not print. +# +# DESCRIBE runs through the project's own ./mxcli: `mxcli -p .mpr -c "DESCRIBE "`. +# A missing document: exit 1, nothing on stdout, "Error: not found: " on stderr after +# mxcli's WARNING banner, recorded as `absent`. Captured from mxcli v0.26.0 for a missing +# microflow, nanoflow, page and snippet (tests/wave2/fixtures/restatement-guard/). The match is +# non-zero exit, no DESCRIBE body, and "not found: " in either stream, never a parse +# error or "no describable document named" (verbatim v0.24.0, conformance-check.sh): those say +# nothing about the model and fail the call (exit 3). +# Golden DESCRIBE output: mxcli v0.23.0 captures in tests/wave2/fixtures/context-pack/capture/, +# v0.26.0 in tests/wave2/fixtures/restatement-guard/. +# Consumer: exec.sh runs `verify` before every MDL write that carries a baseline header and +# prints a WARN line on drift, never a refusal. Producer: this script's `stamp`, run by whoever +# drafts the script (learned-mdl-preflight.md STOP 21). Bash 3.2 / Git Bash; no Python. +set -u + +usage() { + sed -n '3,18p' "$0" | sed 's/^# \{0,1\}//' >&2 + exit 2 +} + +HERE="$(cd "$(dirname "$0")" && pwd)" +. "$HERE/_common.sh" + +CMD="${1:-}"; SCRIPT="${2:-}" +case "$CMD" in + stamp|verify) [ -f "$SCRIPT" ] || usage ;; + live) [ $# -eq 3 ] || usage ;; + *) usage ;; +esac +OWNERS="$HERE/script-owners.sh" +[ -f "$OWNERS" ] || { echo "describe-baseline: $OWNERS is not installed (run sync-project.sh)" >&2; exit 3; } + +CACHE="$PROJECT_ROOT/.claude/loop/describe-baseline" + +# A script's key: its path relative to the project root with a leading done- taken off the file +# name (iterative-build-loop.md renames an executed script done-). script-owners.sh's +# ledger records the same key. +script_key() { # $1 = script path + local abs rel b root + root="$(cd "$PROJECT_ROOT" 2>/dev/null && pwd)" + abs="$(cd "$(dirname "$1")" 2>/dev/null && pwd)/$(basename "$1")" + rel="${abs#"$root"/}" + b="$(basename "$rel" | sed 's/^done-//')" + printf '%s/%s\n' "$(dirname "$rel")" "$b" +} + +# KIND Doc for every restated document, first occurrence order, unique. +restated() { + bash "$OWNERS" docs "$SCRIPT" | awk -F'\t' '$4 == "modify" || $4 == "replace" { k = $1 "\t" $2; if (!(k in s)) { s[k] = 1; print k } }' +} + +MXCLI="" +MPR="" +need_mxcli() { + [ -n "$MXCLI" ] && return 0 + MXCLI="$(find_project_mxcli)" || { echo "describe-baseline: no ./mxcli in $PROJECT_ROOT — cannot DESCRIBE" >&2; return 1; } + MPR="$(find_mpr)" || return 1 +} + +# Prints the hash (or "absent") of DESCRIBE ; keeps the text in the cache. +# Returns 1 when DESCRIBE failed for a reason other than "not found". +describe_hash() { # $1 = KIND, $2 = Doc + local out err rc h all body + err="$(mktemp "${TMPDIR:-/tmp}/dbl-err.XXXXXX")" + rc=0 + out="$("$MXCLI" -p "$MPR" -c "DESCRIBE $1 $2" 2>"$err")" || rc=$? + # The "Connected to: " line seen in REPL-style output is not part of the body. + body="$(printf '%s\n' "$out" | tr -d '\r' | grep -v '^Connected to:' || true)" + if [ "$rc" -ne 0 ]; then + all="$(printf '%s\n' "$out"; cat "$err")" + if [ -z "$(printf '%s' "$body" | grep -v '^Error:' | tr -d '[:space:]')" ] \ + && ! printf '%s\n' "$all" | grep -qE 'Parse error|no describable document' \ + && printf '%s\n' "$all" | grep -qiF "not found: $2"; then + rm -f "$err"; echo absent; return 0 + fi + printf '%s\n' "$all" | sed 's/^/ mxcli: /' >&2; rm -f "$err"; return 1 + fi + rm -f "$err" + h="$(printf '%s\n' "$body" | mxtk_sha256)" + mkdir -p "$CACHE" 2>/dev/null && printf '%s\n' "$body" > "$CACHE/$h.mdl" 2>/dev/null + echo "sha256=$h" +} +# "v0.26.0" from `mxcli --version` (captured: "mxcli version v0.26.0 (2026-10-11T18:37:07Z)"). +mxcli_version() { "$MXCLI" --version 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1; } + +if [ "$CMD" = live ]; then + need_mxcli || exit 3 + describe_hash "$2" "$3" || exit 3 + exit 0 +fi + +if [ "$CMD" = stamp ]; then + need_mxcli || exit 3 + DOCS="$(restated)" + if [ -z "$DOCS" ]; then echo "describe-baseline: $SCRIPT restates no document — nothing to stamp"; exit 0; fi + HDR="" + while IFS=' ' read -r kind doc; do + [ -n "$doc" ] || continue + v="$(describe_hash "$kind" "$doc")" || { echo "describe-baseline: DESCRIBE $kind $doc failed — nothing stamped" >&2; exit 3; } + HDR="$HDR-- baseline: $doc $v +" + echo " $doc $v" + done < "$TMP" + cat "$TMP" > "$SCRIPT"; rm -f "$TMP" + echo "describe-baseline: stamped $(printf '%s' "$HDR" | grep -c ' baseline: ') document(s) in $SCRIPT" + exit 0 +fi + +# ── verify ─────────────────────────────────────────────────────────────────── +LINES="$(sed -nE 's/^[[:space:]]*--[[:space:]]*baseline:[[:space:]]*([A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*)[[:space:]]+(sha256=[0-9a-f]{64}|absent).*$/\1 \2/p' "$SCRIPT")" +if [ -z "$LINES" ]; then + echo "NOBASELINE $(restated | wc -l | tr -d ' ')" + exit 2 +fi +need_mxcli || exit 3 +KINDS="$(restated)" +KEY="$(script_key "$SCRIPT")" +WAS_V="$(sed -nE 's/^[[:space:]]*--[[:space:]]*baseline-mxcli:[[:space:]]*(v[0-9.]+).*$/\1/p' "$SCRIPT" | head -1)" +NOW_V=""; [ -n "$WAS_V" ] && NOW_V="$(mxcli_version)" +RC=0 +while read -r doc want; do + [ -n "$doc" ] || continue + kind="$(printf '%s\n' "$KINDS" | awk -F'\t' -v d="$doc" '$2 == d { print $1; exit }')" + if [ -z "$kind" ]; then echo "STALEHEADER $doc the script no longer restates it — re-stamp"; RC=1; continue; fi + have="$(describe_hash "$kind" "$doc")" || { echo "UNKNOWN $doc DESCRIBE failed"; [ "$RC" -eq 0 ] && RC=3; continue; } + if [ "$have" = "$want" ]; then echo "SAME $doc"; continue; fi + if [ "$have" = absent ]; then echo "SAME $doc absent: this script creates it (a rebuild)"; continue; fi + if [ "$(PROJECT_ROOT="$PROJECT_ROOT" bash "$OWNERS" last-writer "$kind $doc")" = "$KEY" ]; then + echo "SAME $doc this script is its last recorded writer (a re-run)"; continue + fi + if [ -n "$NOW_V" ] && [ "$NOW_V" != "$WAS_V" ]; then + echo "RESTAMP $doc re-stamp after mxcli upgrade ($WAS_V → $NOW_V): DESCRIBE text may differ"; RC=1; continue + fi + case "$want" in + absent) why="created since the script was drafted" ;; + *) why="changed since the script was drafted" ;; + esac + echo "DRIFT $doc $why" + old="$CACHE/${want#sha256=}.mdl"; new="$CACHE/${have#sha256=}.mdl" + if [ -f "$old" ] && [ -f "$new" ] && command -v diff >/dev/null 2>&1; then + diff -u "$old" "$new" | sed -n '3,14p' | sed 's/^/ /' + fi + RC=1 +done <&1)" || _db_rc=$? + case "$_db_rc" in + 1) + echo "WARN: stale draft: '-- baseline:' header no longer matches the model:" + printf '%s\n' "$_db_out" | grep -v '^SAME' | sed -E 's/^(DRIFT|STALEHEADER|RESTAMP|UNKNOWN) ([^ ]*) / \2 — /' + echo " → DESCRIBE it again, merge the live body into the script, re-stamp: bin/describe-baseline.sh stamp $SCRIPT" + _db_drift="$(printf '%s\n' "$_db_out" | awk -F'\t' '$1 == "DRIFT" { print $2 }' | head -3 | tr '\n' ' ' | sed 's/ $//')" + [ -n "$_db_drift" ] && RESTATE_NOTE="${RESTATE_NOTE:+$RESTATE_NOTE; }WARN stale baseline: $_db_drift" + ;; + 3) echo "WARN: '-- baseline:' header present but DESCRIBE could not run — drift not checked" ;; + esac +fi + # ── Gate must be able to run ───────────────────────────────────────────────── # Discovery happens BEFORE the snapshot and the write, because the answer decides whether # there is a write at all. Until 2026-09-17 a missing mxbuild produced a warning and the diff --git a/skills/learned-mdl-preflight.md b/skills/learned-mdl-preflight.md index 9f98e0a7..348a7313 100644 --- a/skills/learned-mdl-preflight.md +++ b/skills/learned-mdl-preflight.md @@ -55,7 +55,7 @@ Re-probed on mxcli v0.24.0 / Mendix 11.13.0 (2026-09-30); rules that now work or | 17 | *(retired 2026-09-30 — uppercase `AND`/`OR` with `!=` builds at 0 errors)* | | | | 18 | Write **any workflow MDL** — `CREATE`/`ALTER WORKFLOW`, a `CALL MICROFLOW` step, a `JUMP TO`, user-task outcomes | **STOP → read [[learned-workflow-patterns]] first, then script. Create every callee microflow in an EARLIER script than the workflow that calls it.** Read the BRD lifecycle + wireframe before writing a single outcome. | v0.24.0: with the callee already in the model, `check` and `exec` refuse a call-microflow with an unmapped parameter, a missing callee, and a `JUMP TO` that is not a real activity name (MDL-WF05). With the callee created **in the same script**, `check --references` skips the call and `exec` auto-fills the `WITH` mapping — a non-context parameter gets `(Name = '$WorkflowContext')` — and `mx check` fails with CE0117 at the call. A context-typed parameter auto-maps fine. `mxcli lint` does not flag it. Earlier incidents: 2026-07-24 ×2, 2026-08-21, 2026-08-31 (`learned-workflow-patterns.md` §7, §13, §14, §21). | | 16 🔎 | Write an access-rule XPath constraint with a **path expression inside the `[%CurrentUser/...%]` substitution token** — `[Assoc = '[%CurrentUser/Module.OtherAssoc%]']`, especially crossing module boundaries | **Never put a path inside the CurrentUser substitution. Keep it bare (`'[%CurrentUser%]'`) and do the ENTIRE multi-hop/cross-module traversal on the left-hand side instead**, qualifying the entity name at every module crossing (same rule as the "cross-module association expression paths" MDL gotcha, extended here to access-rule XPath constraints, not just microflow expressions). Was: `[Decision_Partner = '[%CurrentUser/PLM.Account_Partner%]']`. **Fix (confirmed working):** `[PLM.Decision_Partner/PLM.Partner/Administration.Account_Partner = '[%CurrentUser%]']` — walk Partner→Account fully on the left, compare directly to the (already-an-Account) CurrentUser token. | This is genuinely invalid Mendix XPath, not an mxcli-specific bug — `CE0161` "Error(s) in XPath constraint" is Mendix correctly rejecting it every time it's checked, which is why it kept resurfacing across three separate incidents on a PLM parts-flow project (2026-07-22/23, `bug-logs/mxcli-bugs.md`) that were originally misdiagnosed as mxcli corrupting the domain model on unrelated grant rewrites. In fact the "unrelated entity broke too" symptom was just two independently-broken rules (both using the same invalid CurrentUser-path form) surfacing together once a full `mx check` ran — not one rule corrupting the other. Once rewritten to the correct bare-CurrentUser/full-LHS-chain form, the earlier "Studio Pro GUI only, module-wide" restriction no longer applies — **confirmed mxcli-safe 2026-07-23** via `test-05-currentuser-xpath-chain.mdl` (rebuilt all 4 role grants on the affected entity through mxcli exec, reproducing the corrected XPath verbatim, `mx check: 0 errors`). Use mxcli directly for this pattern; no GUI fallback needed. | -| 21 | `DROP` + `CREATE` (or `CREATE OR REPLACE`) any document **another script also edits** — an aggregator microflow, an after-startup chain, a navigation profile | **STOP → `describe` the live document first, diff your restatement against that dump, and name in the script header which call sites you preserved.** mxcli has no `ALTER MICROFLOW` for an existing body, and `ALTER NAVIGATION` **does not exist at all** (the parser rejects it: `no viable alternative at input 'alternavigation'`). Whole-document restatement is therefore the only write mxcli offers, which makes losing a sibling script's edit the *default* outcome, not an unlikely one. | Confirmed 2026-08-26 on a field run: two scripts each did DROP+CREATE on one shared seed aggregator. The second restated it as it had been *before* the first ran, and a demo-data seed call silently vanished. Every instrument was green — `mx check` reported **0 errors** because an uncalled microflow is perfectly legal, and `mxcli lint`'s orphan rule (QUAL004, `orphaned_elements.star`) exempts the `ACT_` prefix as a presumed UI entry point, so an `ACT_` microflow with no callers at all reports clean. It was found by opening a page and reading "0 to 0 of 0". | +| 21 | `DROP` + `CREATE`, `create or modify` or `create or replace` any document **another script also edits** — an aggregator microflow, an after-startup chain, a navigation profile | **STOP → `describe` the live document first, diff your restatement against that dump, and name in the script header which call sites you preserved. A new owner adds `-- supersedes: