From 8ad0d77097de018b82349f5678a07f72edc09059 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 05:20:06 +0000 Subject: [PATCH 1/4] fix(exec): warn when a restatement reverts newer work (#261, #269) A script restated from a stale DESCRIBE (#261), or a re-run of an older script that owns the same document (#269), silently reverted newer work: exit 0, every gate green, no warning. - project-bin/script-owners.sh: which mdlsource/ scripts create or restate each microflow/nanoflow/page/snippet; -- supersedes: / -- superseded-by: headers; a last-writer ledger exec.sh appends after every kept write, so an in-order rebuild stays quiet without asking mxcli. - project-bin/describe-baseline.sh: stamp -- baseline: sha256=... at draft time, verify at exec (opt-in: only stamped scripts DESCRIBE). - exec.sh: WARN lines + BUILD-LOG detail; never a refusal, exit codes unchanged. Happy path: one grep over mdlsource/, zero mxcli calls. - gate-check.sh: co-owned count (info only). - learned-mdl-preflight.md: STOP 33 and 34. - Fixture tests/wave2/test-restatement-guard.sh; not-found DESCRIBE output captured from mxcli v0.26.0. Rebuilt from master; supersedes draft #311 without its merged #315 and crash-rollback (#424) hunks. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + bin/gate-check.sh | 12 + bin/lib/install-manifest.sh | 6 +- project-bin/describe-baseline.sh | 187 +++++++++++ project-bin/exec.sh | 55 +++- project-bin/script-owners.sh | 281 ++++++++++++++++ skills/learned-mdl-preflight.md | 2 + ...OFLOW__Administration.ShowPasswordForm.out | 19 ++ ...AGE__Administration.ChangePasswordForm.out | 40 +++ .../restatement-guard/notfound-MICROFLOW.rc | 1 + .../notfound-MICROFLOW.stderr | 2 + .../restatement-guard/notfound-NANOFLOW.rc | 1 + .../notfound-NANOFLOW.stderr | 2 + .../restatement-guard/notfound-PAGE.rc | 1 + .../restatement-guard/notfound-PAGE.stderr | 2 + .../restatement-guard/notfound-SNIPPET.rc | 1 + .../restatement-guard/notfound-SNIPPET.stderr | 2 + tests/wave2/test-restatement-guard.sh | 307 ++++++++++++++++++ 18 files changed, 920 insertions(+), 2 deletions(-) create mode 100755 project-bin/describe-baseline.sh create mode 100755 project-bin/script-owners.sh create mode 100644 tests/wave2/fixtures/restatement-guard/MICROFLOW__Administration.ShowPasswordForm.out create mode 100644 tests/wave2/fixtures/restatement-guard/PAGE__Administration.ChangePasswordForm.out create mode 100644 tests/wave2/fixtures/restatement-guard/notfound-MICROFLOW.rc create mode 100644 tests/wave2/fixtures/restatement-guard/notfound-MICROFLOW.stderr create mode 100644 tests/wave2/fixtures/restatement-guard/notfound-NANOFLOW.rc create mode 100644 tests/wave2/fixtures/restatement-guard/notfound-NANOFLOW.stderr create mode 100644 tests/wave2/fixtures/restatement-guard/notfound-PAGE.rc create mode 100644 tests/wave2/fixtures/restatement-guard/notfound-PAGE.stderr create mode 100644 tests/wave2/fixtures/restatement-guard/notfound-SNIPPET.rc create mode 100644 tests/wave2/fixtures/restatement-guard/notfound-SNIPPET.stderr create mode 100755 tests/wave2/test-restatement-guard.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 485ccb15..4d74fb1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- fix(exec): **a restatement that reverts newer work now WARNs instead of passing silently: a script restated from a stale DESCRIBE (#261) and a re-run of an older script that owns the same document (#269).** Both exited 0 with every gate green, because a whole `create or modify` is valid MDL whatever it replaces. New `project-bin/script-owners.sh` lists the `mdlsource/` scripts that create or restate each microflow, nanoflow, page and snippet, reads `-- supersedes:` / `-- superseded-by:` headers, and keeps a last-writer ledger that `exec.sh` appends after every kept write, so a rebuild in run order stays quiet. New `project-bin/describe-baseline.sh` stamps `-- baseline: sha256=` from a DESCRIBE at draft time and compares at exec. `exec.sh` prints `WARN:` lines and adds them to the BUILD-LOG detail; it never refuses, and the exit codes do not change. Cost: with no co-owner and no baseline header it runs one awk over `mdlsource/` and no mxcli call (fixture-asserted). DESCRIBE runs only for a stamped script, or once per conflicting document the ledger cannot settle. `gate-check.sh` prints the co-owned count when it is above 0 (information only). Preflight STOP 33 (owner/supersedes/stamp) and STOP 34 (DESCRIBE omits role and layout-row visibility). Not-found DESCRIBE output is captured from mxcli v0.26.0. Supersedes draft #311 without its unrelated hunks. **Behaviour change to check in your project:** `exec.sh` output gains `WARN: restatement:` / `WARN: stale draft:` lines, and the BUILD-LOG detail cell can carry `WARN co-owned: …` / `WARN stale baseline: …`; set `MXTK_RESTATE_GUARD=off` to skip both. β€” AlvinnYeo (#261), MendixMau (#269) - fix(exec): **a pre-exec `mxcli check` refusal now records its reason in the BUILD-LOG row, and the preflight STOP table gains rows 29-32** (#445). The 🚫 row said only "mxcli check failed pre-exec", so the reason for 3 field refusals was lost (the issue suspects MDL-PAGEARG01, in mxcli since about v0.19); exec.sh now keeps the output of the one check run it already makes, still printed live, and appends `; check: ` (rule id first, ANSI and CR stripped, pipes escaped, cut at 160 bytes on a UTF-8 boundary). New STOP rows: a constant in a nanoflow (CE0117), `dynamicimage` on the React client (CE0582), a data-view attribute binding across 2+ associations (CE1613), a widget `show page` argument with no context object or besides it (MDL-PAGEARG01). Golden input: two real mxcli v0.26.0 check captures (mxcli's own PAGEARG01 test and a parse error), not the field output. BUG-70 notes that v0.26.0 `check` now refuses its shape. Test: `tests/wave2/test-exec-check-refusal.sh` β€” MendixMau (field 2026-10-17) - fix(exec): **`exec.sh` exit codes now mean what they say (#443, #448).** A clean build with a lint rise exited 1 like a failed gate, and an UNVERIFIED gate exited 0 like a pass: in one field build 57 of 58 clean builds exited 1, agents learned to ignore 1, and 10 UNVERIFIED runs read as success. Now `0` applied and gate passed Β· `1` not applied or rolled back (and PARTIAL, whatever mxcli exited) Β· `2` usage, nothing touched Β· `4` applied, gate passed, lint only (LINT ROSE/BLIND; last line `gate passed, lint only`) Β· `5` applied, UNVERIFIED (also `ALLOW_UNVERIFIED=1`). An mxbuild exit 1/3 whose errors file holds only a generic deploy-stage message (`Compilation of the app bundle failed.`, `An error occurred while ...`) and no model problem is retried once, like an empty one, but not again by the gate when the baseline's retry already failed with the same text; a version mismatch still is not. No extra mxbuild on the happy path (2); worst case 3. Defined in the `exec.sh` header, mirrored in `iterative-build-loop.md`; `doctor.sh --fix-property-sets` reads 4 as kept, `sync-project.sh` names the fix for a hand-edited copy; `test-exec-gate-retry.sh` F–L pin it. **Behaviour change to check in your project:** a wrapper that tests exec.sh's exit code now sees 4 for a lint-only rise (was 1) and 5 for UNVERIFIED (was 0); a PARTIAL run never exits 4 or 5. β€” MendixMau (field 2026-10-17) - fix(status): **`bin/status.sh` (full and `--brief`) prints one `CWD` line when the session runs outside the project folder, and the README quick start says to launch the lead session from the project folder** (#447). Claude Code finds `.claude/agents/` by walking up from the launch folder, so a lead started in a sibling folder ran a whole Stage 5 build (110 execs) on general-purpose agents and nothing said why. The check is two `pwd -P` calls; only the warning path asks `_common.sh`'s `mxtk_walk_root` which project the cwd belongs to. `CONVERSION-RUNBOOK.md` now says `` instead of "the workspace". Test: `tests/wave2/test-status-session-cwd.sh`; `test-status-existing-app.sh` now runs status.sh from the project root, as a session does β€” MendixMau (field 2026-10-17) diff --git a/bin/gate-check.sh b/bin/gate-check.sh index 40159c2a..ab1dc61c 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -2827,6 +2827,18 @@ else fi printf "Report disposition (findings filed?): %s β€” %s\n" "$DISP_STATUS" "$DISP_NOTE" +# Script ownership (#269): INFORMATION, never a verdict and never a block. A document created by +# more than one mdlsource/ script reverts whenever an older script is replayed; field apps carried +# 99/579, 130/357 and 38/159. exec.sh warns per write; this prints the project-wide count so a +# slice close can see whether it is falling. One awk over mdlsource/, no mxcli; silent at 0. +if [ -f "$TOOLKIT_DIR/project-bin/script-owners.sh" ]; then + OWN_CNT="$(bash "$TOOLKIT_DIR/project-bin/script-owners.sh" count "$PROJECT_DIR" 2>/dev/null || true)" + case "$OWN_CNT" in + 0\ *|"") ;; + *) printf "Script ownership (info): %s of %s document(s) are created by more than one mdlsource/ script β€” bin/script-owners.sh report lists them\n" "${OWN_CNT%% *}" "${OWN_CNT#* }" ;; + esac +fi + # Stage P is checked outside the numeric loop (bash 3.2 arrays need integer indices). N_PASS=0; N_PENDING=0; N_WAIVED=0; N_FAIL=0; N_MANUAL=0; NEXT_UP=""; NEXT_UP_STATUS=""; ATTENTION="" APPROVED_OVER_FAIL="" diff --git a/bin/lib/install-manifest.sh b/bin/lib/install-manifest.sh index 5eb0928f..6c02121f 100644 --- a/bin/lib/install-manifest.sh +++ b/bin/lib/install-manifest.sh @@ -119,7 +119,11 @@ MXTK_AGENTS_STAGE_BUILD="mdl-agent.md gate-agent.md test-agent.md review-agent.m # the generator reads the template from its own directory. The project runs it at Stage 3 # (skills/process-map.md Step 4) after every change to design/process-model.json, the # anchors, a wireframe or a BRD, and gate-check.sh check_stage_3 fails a stale render. -MXTK_PROJECT_BIN="process-map.js process-map.template.html app-restart.sh test-scratch.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" +# script-owners.sh + describe-baseline.sh joined on 2026-10-21 (#269, #261): exec.sh runs both +# as its siblings before every MDL write (co-owned documents, stale-DESCRIBE restatements) and +# records each kept write in script-owners.sh's ledger; describe-baseline.sh calls script-owners.sh +# as ITS sibling, so the three install together. +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..e8df77de --- /dev/null +++ b/project-bin/describe-baseline.sh @@ -0,0 +1,187 @@ +#!/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 +# 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 drift (one DRIFT row each) +# Β· 2 no baseline header Β· 3 could not 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. Module-role visibility and +# layout-grid row visibility are absent from DESCRIBE (v0.25.0), so a restatement drops them with +# the hash unchanged; learned-mdl-preflight.md STOP 34 is the row for that. +# +# 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 33). Bash 3.2 / Git Bash; no Python. +set -u + +usage() { + sed -n '3,14p' "$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" +} + +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 .) 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")" +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"; 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 + 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 <> "$BUILD_LOG" @@ -565,6 +565,7 @@ take_score() { # Not found, or the scan itself errors (exit 2) β†’ one warning, and the exec goes on: mxcli # check and the mxbuild gate still stand behind it. SCAN_NOTE=""; HV_NOTE="" # HV_NOTE: the ContentsHash bracket's cell text, set below +RESTATE_NOTE="" # the restatement warnings' cell text (#261/#269), set after the check if [ "$PATCH_MODE" != "1" ]; then _SCAN="" for _s in "${MXTK_ROOT:-}/bin/mdl-scan.sh" \ @@ -1121,6 +1122,52 @@ elif [ "${SKIP_CHECK:-0}" != "1" ] && [ -n "$PMXCLI" ]; then echo " βœ“ check clean" fi +# ── Restatement warnings (#261, #269; WARN only, never a refusal) ──────────── +# A whole restatement (`create or modify|replace microflow|nanoflow|page|snippet`) is valid MDL +# whatever it replaces, so check, mxbuild and lint stay green while it reverts newer work. Two +# field shapes, one sibling instrument each: +# co-owned (#269) three apps had 99/579, 130/357 and 38/159 documents created by more than one +# mdlsource/ script; re-running an older one reverts the newer. script-owners.sh +# check names every other owner no `-- supersedes:` header settles. +# stale (#261) a script drafted from a DESCRIBE taken BEFORE an earlier script of the batch +# ran put four old messages back. describe-baseline.sh verify compares the +# script's `-- baseline:` header with a fresh DESCRIBE. Opt-in: no header, no call. +# Each prints WARN lines and lands in the BUILD-LOG detail; the exec goes on and the exit code is +# whatever the rest of the chain decides. MXTK_RESTATE_GUARD=off skips both. Cost without a +# co-owner or a header: one grep over mdlsource/ for the restated names, no mxcli call. +if [ "$PATCH_MODE" != "1" ] && [ "${MXTK_RESTATE_GUARD:-}" != "off" ] && [ -f "$SCRIPT" ]; then + _rs_bin="$(dirname "$0")" + if [ -f "$_rs_bin/script-owners.sh" ]; then + _so_rc=0 + _so_out="$(PROJECT_ROOT="$PROJECT_ROOT" bash "$_rs_bin/script-owners.sh" check "$SCRIPT" "$PROJECT_ROOT" 2>/dev/null)" || _so_rc=$? + if [ "$_so_rc" -eq 1 ] && [ -n "$_so_out" ]; then + _so_docs="$(printf '%s\n' "$_so_out" | cut -f2 | sed 's/^[A-Z]* //' | LC_ALL=C sort -u)" + _so_n="$(printf '%s\n' "$_so_docs" | grep -c . || true)" + echo "WARN: restatement: $_so_n document(s) this script restates are also owned by another mdlsource/ script:" + printf '%s\n' "$_so_out" | awk -F'\t' '{ printf " %s β€” %s\n", $2, $4 }' + echo " β†’ edit the owning script or use alter; a new owner says '-- supersedes: '" + echo " (bin/script-owners.sh report; skills/learned-mdl-preflight.md STOP 33)" + grep -qE '^[[:space:]]*--[[:space:]]*baseline:' "$SCRIPT" || \ + echo " no '-- baseline:' header, so a stale draft cannot be detected: bin/describe-baseline.sh stamp