Skip to content

exec.sh: warn on stale-DESCRIBE and co-owned restatements; roll back an mxbuild crash on the written model - #311

Draft
MendixMau wants to merge 10 commits into
masterfrom
fix/stale-restatement-guard
Draft

MendixMau wants to merge 10 commits into
masterfrom
fix/stale-restatement-guard

Conversation

@MendixMau

@MendixMau MendixMau commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Status: draft, on hold for a field run, which waits for the maintainer. Depends on #315 (chore/baseline-word-trim): it is merged into this branch because that PR frees the baseline words this one uses. Merge #315 first. Apart from the first commit, this branch does not touch project-bin/exec.sh; the one call still needed there is listed as a pending follow-up below.

#261: a script restated from a stale DESCRIBE reverts earlier work

Problem. Script B was drafted from a DESCRIBE taken before script A ran. Running B put four old messages back, and every gate stayed green.

Fix. New project-bin/describe-baseline.sh:

  • stamp: writes -- baseline: <Doc> sha256=<hash> (or absent) for each restated document.
  • verify: runs DESCRIBE again and reports drift with a short diff.
  • record-applied: run after a gate pass; records each document's hash after the write in .claude/loop/describe-baseline/applied.tsv.
  • live and applied-by: helpers used by script-owners.sh.

verify reports SAME in two cases: the live body is this script's own recorded write (a re-run after a clean exec), or a baselined document is absent (a rebuild onto a fresh model).

#269: one document, several owning scripts

Problem. On three field apps, 99 of 579, 130 of 357 and 38 of 159 documents were created by more than one script. Replaying an older script reverts the newer one.

Fix. New project-bin/script-owners.sh with docs, report, count and check, plus -- supersedes: / -- superseded-by: headers.

  • Run order and header names ignore a leading done-, the rename iterative-build-loop.md gives an executed script.
  • The two "replaying this reverts O" verdicts ask the model first. If the document is absent, or its live body was applied by this script or an older one, there is no warning. So a rebuild that replays scripts in run order with supersedes headers stays quiet. With no ./mxcli to ask, it stays a conflict.
  • gate-check.sh prints the co-owned count when it is above 0. Information only.
  • exec.sh warns by default, and the warning lands in the BUILD-LOG row. It refuses under MXTK_RESTATE_GUARD=refuse or exec approval ask.

From #309 (refs)

  • mxbuild crash after the write. When mxbuild ran clean on the baseline and then crashes on the written model, the snapshot is restored and a BUILD-LOG row is written. If the baseline was not measured, the write is kept as before. The message now says STOP and names restore-mpr.sh.

  • STOP 29. DESCRIBE omits role and layout-row visibility, so the row gives a command with no | to run before restating:

    grep -la -e ModuleRoles -e ConditionalVisibilitySettings /dev/null $(grep -rlF '<Doc name>' mprcontents/)

    A hit may be an empty setting, or a unit that only names the document. A hit means "check it in Studio Pro".

"Not found" detection

No capture of DESCRIBE on a missing microflow or page exists in this repo. The match follows the not-found output recorded for other kinds:

  • Error: entity not found: ZKT35.Widget (duration: …), EXIT 1 (a 2026-08-07 retest log under bug-logs/)
  • Error: constant not found: Nope.X, rc 1 (v0.24.0, project-bin/conformance-check.sh)

So it matches defensively: non-zero exit, no DESCRIBE body, and not found: <Doc> in either stream. "Parse error" and "no describable document named" fail the call instead of reading as absent. A real missing-microflow capture should be added during the field run.

Pending follow-up: one line in exec.sh (another PR owns exec.sh tonight)

Insert right after the LOOK-owed block (the look-ledger.sh owe call):

# ── Applied record (describe-baseline.sh, #261) ──────────────────────────────
if [ "$PATCH_MODE" = 0 ] && [ "$GATE_STATE" = "pass" ] && [ "$EXEC_STATUS" -eq 0 ] \
   && [ -f "$(dirname "$0")/describe-baseline.sh" ]; then
  PROJECT_ROOT="$PROJECT_ROOT" bash "$(dirname "$0")/describe-baseline.sh" record-applied "$SCRIPT" >/dev/null 2>&1 || true
fi
  • Without it, re-running a stamped script after a clean exec reads as drift, and under ask it is refused.
  • The fixture's exec-level case for this prints NOT RUN until exec.sh carries the line.
  • With the line applied to a copy of exec.sh, the fixture passes 67/67.

Skills

learned-mdl-preflight.md changes:

Checks (after merging #315 and master)

  • LC_ALL=C bin/render-routing.sh --check: in sync, baseline 76,467 of 80,000 words, exit 0.
  • check-scripts, check-portability, check-no-private-citations, the leak guard (LEAKGUARD_ALLOW_NO_DENYLIST=1) and check-pr-discipline: all clean.
  • tests/wave2/test-restatement-guard.sh: 65 ok plus 1 NOT RUN (the pending exec.sh case); 67/67 with the follow-up line. Against master's exec.sh, the guard and crash cases fail.
  • Run earlier on this branch: test-bug07-08.sh 43/0, test-exec-lock-watch.sh 45/0, test-exec-change-class.sh 20/0, test-model-stamp.sh 33/0, test-bug03-gates.sh 31/0.

Field run

Still owed. Nothing has run against a real project yet.

Refs #261
Refs #269
Refs #309

🤖 Generated with Claude Code

https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw

…an mxbuild crash on the written model

- project-bin/script-owners.sh (new): lists the documents each mdlsource/ script creates or
  restates; report/count of co-owned documents; check <script> names every other owner not
  settled by a `-- supersedes:` / `-- superseded-by:` header (#269).
- project-bin/describe-baseline.sh (new): stamp writes `-- baseline: <Doc> sha256=<hash of
  DESCRIBE>` per restated document; verify re-DESCRIBEs and reports drift with a diff (#261).
- exec.sh runs both before the snapshot. Warn by default, the warning lands in the BUILD-LOG
  row; refuse (nothing written) under MXTK_RESTATE_GUARD=refuse or exec approval `ask`.
- exec.sh: mxbuild exits non-zero with no errors file after the write while the baseline was
  measured -> snapshot restore + BUILD-LOG row, so the next exec does not write on top of an
  unloadable model (#309 inbox finding). Unmeasured baseline keeps today's behaviour.
- gate-check.sh prints the co-owned count (information only, only when > 0).
- learned-mdl-preflight.md: STOP 21 covers draft-ahead batches and ownership; STOP 29
  (DESCRIBE omits role and layout-row visibility, #309); Step 0 "edit the owning script or alter".
- install-manifest registers both scripts.

Fixture tests/wave2/test-restatement-guard.sh: 48 ok on this branch, 38 of 50 fail against
master's exec.sh. Golden input: mxcli v0.23.0 DESCRIBE captures already in the repo, the
StyleGallery mdlsource tree, skills/agents-examples. No field run on a project yet.

Refs #309
Refs #261 (field run owed)
Refs #269 (field run and trend owed)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
The always-on baseline tier counted by bin/render-routing.sh --check was
at 79,735 of its 80,000-word cap. This frees 3,772 words (now 75,964)
by moving narrative and setup-only text out of baseline files, keeping
every rule, STOP, denominator and gate cell in place, each rule still
paired with a one-line failure example plus a pointer to the full text.

- conversion-runbook.md, source-triage.md, interview-protocol.md: long
  incident narratives -> one line + pointer; originals verbatim in new
  on-demand process/baseline-incident-history.md (R1-R20, AR1, ST1-ST4,
  IP1-IP3). Runbook's duplicate open-questions status table now defers
  to interview-protocol.md section 4.
- agent-roles.md: the six template shapes (setup-only) moved to
  agents/README.md, which installers never copy (explicit MXTK_AGENTS).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
…low", ST1 id

Independent review of #315: obligations.tsv still pointed at agent-roles.md for
step 8a, which moved to agents/README.md; agent-roles.md said the example bodies
are "below"; the history header cited a non-existent section id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
Measured with LC_ALL=C on the branch after master (#313) was merged in:
79,997 -> 76,259 (-3,738); the old line used pre-#313 figures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
…er replay, STOP 29 command)

Review of #311 (held):
1. script-owners.sh orders and matches scripts with a leading done- removed: a raw sort put an
   executed done-05-x.mdl after 09-y.mdl and called the NEW 09 a replay; `-- supersedes: 05-x.mdl`
   stopped matching after the rename.
2. describe-baseline.sh record-applied <script> records each document's live hash after a write;
   verify reads SAME when the live body is the script's own recorded write (a re-run) or when a
   baselined document is absent (a rebuild onto a fresh model). exec.sh is not changed here: the
   one call it needs after a gate pass is proposed in the PR; the fixture runs that case only
   when exec.sh carries it (NOT RUN otherwise) and passes 67/67 with the patch applied.
3. Replaying older co-owned scripts in run order is quiet: the "replaying reverts O" verdicts
   ask the model first (document absent, or live body applied by this script or an older one).
   No ./mxcli to ask stays a conflict.
4. "Not found" matching follows recorded mxcli output (entity/constant not-found, rc 1) by shape:
   non-zero exit, no body, "not found: <Doc>"; "no describable document" and parse errors fail
   the call instead. No capture of a missing MICROFLOW/PAGE exists in the repo.
5. STOP 29 gives a pipe-free grep over mprcontents/ instead of the codec library.
6. Cut the Step 0 line that repeated STOP 22.

Fixture tests/wave2/test-restatement-guard.sh: 65 ok + 1 NOT RUN on this branch.

Refs #261
Refs #269
Refs #309

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
…e baseline budget after merging master

Master sits at 79997 of 80000 baseline words; this branch's STOP 21/29 text needs room.
Instruction cells are unchanged; only incident history is compressed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw
…o fit the baseline budget after merging master"

This reverts commit 3d3e789.
MendixMau added a commit that referenced this pull request Oct 16, 2026
Independent review: APPROVE.

# Review: PR #424, exec.sh rolls the write back when mxbuild crashes (crash-rollback split from #311)

- Head: `aade99b7` (`fix/exec-crash-rollback`), one commit on master `2def0679`. 3 files: `project-bin/exec.sh` (+24/-3, one hunk), new `tests/wave2/test-exec-crash-rollback.sh` (118), one CHANGELOG line.
- Read-only detached worktrees under /tmp, removed afterwards. Synthetic fixtures only. macOS 26.6, bash 3.2 via `bash`.
- Verdict: **APPROVE** (items below are small; 1 and 2 are worth doing before merge, 3 to 5 are notes).

## Claims checked

| Claim | Result |
|---|---|
| New test 13/0 on the branch | **Reproduced**: 13 passed, 0 failed |
| 9/4 on master's exec.sh | **Reproduced**: 9 passed, 4 failed, all four in case B (write kept, model differs, no message, no row) |
| Extracted from #311 only, no guard | **Yes.** The hunk is the `elif [ "$MXBUILD_EXIT" -ne 0 ]` branch only. No refusal path, no new script, no routed text, no `applied.tsv`, no baseline words. |

## Behaviour against the T3 intent

| Question | Finding |
|---|---|
| Restores exactly the pre-exec model? | Yes. `restore_snapshot` -> `restore-mpr.sh` replaces `mprcontents/` wholesale (copy to temp, `rm -rf`, `mv`), so units the write ADDED are removed too, then copies the `.mpr`, then checks the unit count. Case B asserts a byte-identical checksum of `.mpr` + unit. |
| Only on a crash, not on a normal build error? | Yes. A written errors file goes down the existing CE branch (case D: ordinary `gate failed` row, crash branch not taken). The new code sits only in "non-zero exit and no/empty errors file". Matches T3-S2. |
| Only when the baseline is known? | Yes. `BASE_KNOWN=1` comes from a measured baseline (clean or with errors) or the clean verification stamp. Case C (baseline crash too) keeps the old "kept, flagged" behaviour. I also ran T3-S4 by hand (baseline with a CE error list, then crash): restored, model identical, rc 1, one honest row. |
| `--patch` | Unchanged: `PATCH_MODE=1` takes the old `else`, and the later `--patch` block restores once (`RESTORE_TRIED=0`). No double restore. Read only, not run (no test case, see item 1). |
| Backup step fails | No data loss. `./bin/snapshot-mpr.sh` runs bare under `set -e` (exec.sh:79), and the snapshot script is `set -euo pipefail`, so a failed snapshot aborts the exec **before** the write. If the restore itself fails, the branch prints the STOP line and logs "NOT rolled back (restore did not run)". `restore-mpr.sh` leaves the working tree untouched when its copy fails. |
| Exit code and stamp | `GATE_STATE=fail`, so the stamp is cleared and exec exits 1 (verified: rc 1 in B and S4). The next exec re-measures the baseline. |
| `--on-copy` | Not on master (#384 is open), so no interaction yet. Whoever lands second has to look at this branch again: under `--on-copy`, rollback means "discard the copy", not "restore the snapshot". T3 order (this first, #384 rebases over one hunk) still holds. |
| Ctrl-C | **Unchanged from master, not made worse.** I tested it: SIGINT to the process group while the post-write mxbuild runs. exec.sh dies (rc 130) in its `kill -0` / `sleep 1` poll, so the crash branch never runs. The write is kept, there is no BUILD-LOG row, and the background mxbuild keeps running (async children ignore SIGINT in a non-interactive shell). A Ctrl-C does not trigger a rollback. The only INT exposure is a Ctrl-C during `restore-mpr.sh`, between its `rm -rf mprcontents` and `mv`. That window already exists on the failed-gate path. This belongs to T3-S5 / E5, not to this PR. |

## Items

1. **Test gaps (small).** (a) Case B does not assert the exit code. Add `[ "$RC" -eq 1 ]`. (b) There is no case for a baseline that is known from the **verification stamp** rather than measured. That is the second source of `BASE_KNOWN=1`, and the one in item 2. (c) There is no `--patch` crash case (T3-S3), which would guard against a double restore. (d) The new file is mode 100644, while the other exec tests are 755. `run-all.sh` runs it with `bash`, so this is consistency only.
2. **The stamp case: wording and a possible false rollback.** The new message says "mxbuild ran on this model before the write". When the baseline was skipped because of the stamp, mxbuild did not run in this exec. The stamp fingerprint covers the model only (`model-stamp.sh`: `.mpr` + units), not the mxbuild or JDK version. So a Studio Pro or mxbuild change between the stamp and this exec can make mxbuild die from a machine problem, and that gets read as a write problem. The write is then rolled back. This is recoverable (re-run the script, snapshot kept), but the row then says something untrue. Either reword the message and row to "baseline known (measured, or verification stamp)", or keep the rollback only when the baseline was measured in this run, and treat a stamp-only baseline like case C. My recommendation is the rewording: T3 explicitly counts the stamp as known.
3. **CHANGELOG: two consequences after a pull.** (a) With `MXTK_GATE_TIMEOUT` set, a timed-out gate (exit 124, no errors file) now **rolls back** a write that may be fine. `_common.sh:608-611` already warns about exactly this. The default is unbounded, so only opt-in users see it. (b) T3 asked for the run output to say where the crash output is kept. The output prints the first 20 mxbuild lines and the snapshot path, which is enough, but one clause in the CHANGELOG line would help a project that relied on "kept, open Studio Pro".
4. Note, not this PR: `restore_snapshot` picks the newest snapshot by mtime (`ls -dt`), not `$SNAP_NOW`. This is pre-existing and shared with the failed-gate path. Under the exec lock it is the right directory, but passing `$SNAP_NOW` would make it exact.
5. Note: the V1-to-V2 repack case (the snapshot has no `mprcontents/` but the live model does) makes `restore-mpr.sh` refuse. The branch then logs "NOT rolled back" honestly. The failed-gate path behaves the same way. Fine.

## Verdict

**APPROVE.** Before merge, preferably: 1 (exit-code assert, stamp case, patch case) and 2 (honest wording for a stamp-only baseline). 3 is one CHANGELOG clause. 4 and 5 are notes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants