Skip to content

Field runs 2026-10-15: #379 PASS, #384 PASS (sync --upgrade-bin not scoped; copy 87-134 s), #374 PASS (.bak in mprcontents) #390

Open

Description

@MendixMau

Field-run report for open PRs #379, #384 and #374. It ran on one real field project (Mendix 11.15.0, mxcli v0.26.0) on macOS arm64 with bash 3.2.

Verdicts:

Findings that need action

  1. sync-project.sh --upgrade-bin X is not a scoped install. It still runs a full sync: 244 tracked files changed, a file was deleted, about 30 files were added and the git hooks were reinstalled. Any instruction or doc that uses it as "install just these scripts" is wrong. A --only-bin mode, or a doc fix, would help (feat(exec): opt-in --on-copy — exec and gate on a scratch copy, promote by rename (#350) #384 field step).
  2. feat(exec): opt-in --on-copy — exec and gate on a scratch copy, promote by rename (#350) #384 --on-copy copy cost. The copy took 86.7 s unloaded and 133.8 s under load, against a 107–138 ms swap, for a model of 549 units. That makes each --on-copy run 4–7 minutes long on this machine.
  3. feat(exec): opt-in --on-copy — exec and gate on a scratch copy, promote by rename (#350) #384: a refusal because Studio Pro holds the model writes no BUILD-LOG row, while the dirty-model refusal does.
  4. feat(exec): opt-in --on-copy — exec and gate on a scratch copy, promote by rename (#350) #384: the dirty-model guard runs before the Studio Pro guard, so a dirty model hides whether the Studio Pro refusal works. This is minor, but it confused the field test.
  5. mxpatch: shared patch core + mxcli fix hashes verify around exec.sh --patch (#294, #295) #374: the patcher leaves <unit>.mxunit.bak inside mprcontents/. It shows up as untracked in the model folder and is easy to commit by accident.
  6. mxpatch: shared patch core + mxcli fix hashes verify around exec.sh --patch (#294, #295) #374: the BUILD-LOG row carries no hash baseline or verify result. In this run the patch also repaired a mismatch that was already in the model, and that is visible only in stdout.
  7. Field confirmation of mxpatch: shared patch core + mxcli fix hashes verify around exec.sh --patch (#294, #295) #374's bug: committed HEAD of the field project had a ContentsHash MISMATCH on exactly the workflow unit that an old-style --patch (2026-10-02) had rewritten.

origin/master 484e7ff
macOS 26.6.2, Darwin 25.6.0 arm64 (Darwin Kernel Version 25.6.0 … RELEASE_ARM64_T6031 arm64)
bash GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)
mxcli v0.26.0 (2026-10-11T18:37:07Z) (field project)
Mendix / Studio Pro 11.15.0, MPR v2 (mprcontents)
Python 3.14.4
Field project 1 real app with 549 units, 1 workflow, 27 pages; the git working tree was clean of tracked changes at the start
PR Verdict
#379 lock race PASS
#384 exec-on-copy PASS for a and b. For c, only the no-op recovery half was run; the Ctrl-C half was NOT RUN (see below)
#374 mxpatch + fix hashes PASS

PR #379: lock reap tickets (fix/lock-reap-tickets @ 81981f2)

Commands, run in a worktree of the branch:

bash tests/wave2/bench-lock-race.sh
bash tests/wave2/bench-lock-race.sh --old
bash tests/wave2/test-lock-reap.sh project-bin/_common.sh

The three ran at the same time on the same machine.

RESULT subject=new rounds=200 contenders=10 overlaps=0 overlap_rounds=0 stolen=0 starved=0 probe_fail=0 kills=558 acquired=1642
exit=0
RESULT subject=old rounds=200 contenders=10 overlaps=21 overlap_rounds=9 stolen=18 starved=0 probe_fail=0 kills=552 acquired=1648
exit=1
PASS=45 FAIL=0   (test-lock-reap.sh, exit=0)

PASS.

  • New code: overlaps=0, stolen=0, starved=0, probe_fail=0, exit 0.
  • --old: overlaps=21 across 9 rounds and 18 stolen locks, so the harness does catch the race on macOS with bash 3.2.
  • Fixture: 45/45.

PR #384: exec-on-copy (feat/exec-on-copy @ fff1cd5)

Install: deviated from the brief

Step 2 said to install with sync-project.sh <project> --upgrade-bin exec.sh --upgrade-bin _common.sh --upgrade-bin test-scratch.sh.

--upgrade-bin does not limit the sync to those scripts. It only allows overwriting a locally modified copy, and a full sync still runs. On the field project it:

  • changed 244 tracked files: all of the skills dirs, CLAUDE.md, PROJECT.md, settings.json, .gitignore, 20 bin/ scripts and the tests/e2e engine
  • deleted .claude/vscode-mdl.vsix
  • added about 30 untracked files
  • reinstalled 3 git hooks
  • reported "2 warning(s) — this project is NOT fully in sync" (property-sets)

The model was not touched. I reverted with git checkout -- . and removed the files the sync had added; the hooks pass install-project-hooks.sh --check. Then I copied only exec.sh, _common.sh and test-scratch.sh from the PR into bin/. Every helper the new exec.sh calls that the project lacks (metrics-ledger.sh, app-restart.sh, doctor.sh, exec-approval.sh) is behind a -x guard and is skipped.

To put things back at the end I used git checkout (the project's bin/ scripts are committed), not the clone's sync-project.sh. The clone is checked out at d16f578, not origin/master, so its sync would have left different bin/ files behind.

a) Studio Pro closed: ./bin/exec.sh --on-copy <4-line ALTER PAGE … SET Visible script>

→ on-copy: the gate passed on the copy — promoting it to the live model...
  ✓ promoted by rename in 138 ms (copy took 133828 ms); scratch copy removed.
  ✓ verification stamp written (pass — exec.sh <script>.mdl)
✓ Script applied to <model>.mpr.
  elapsed: 438s (check + snapshot + exec + mxbuild gate)
  ⚠  slower than a healthy run — run doctor.sh --quick …
exit=0

BUILD-LOG row:

| 2026-10-15T11:10:48+0800 | `<script>.mdl` | pass | ✅ applied | mxbuild clean (no errors file written); lint could not run (lint-gate exit 2) · change: ui-only · approval: auto (PROJECT.md) · on-copy: promoted (copy 133828 ms, swap 138 ms) |

PASS. The copy was built and checked, then promoted, with one BUILD-LOG row and exit 0.

Warts:

b) Studio Pro holds the model

  • b1: Studio Pro was opened, and the run was refused, but for the wrong reason. The uncommitted 66-byte change from (a) tripped the "Uncommitted model changes" guard first, so the Studio Pro check was never reached. The model was untouched, and a 🚫 blocked BUILD-LOG row was written.
  • b2: FORCE_EXEC=1 ./bin/exec.sh --on-copy … promoted (exit 0, copy 86659 ms, swap 107 ms). Investigation: Studio Pro had opened a different clone of the same app, there was no <model>.mpr.lock beside the field model, and Studio Pro had no handle on the field project. So promoting was correct, and this was not a PR defect.
  • b3: To test the guard directly, I wrote <model>.mpr.lock containing {"ProcessId":<pid of a live sleep>} (the format sp_holds_model reads) and ran both forms:
./bin/exec.sh --on-copy <script>
✗ Studio Pro has <model> open (lock PID 21940 alive) — refusing exec (split-brain corruption risk).
  → Close the project in Studio Pro (or quit SP), then re-run.
    --on-copy / --recover-promote: FORCE_EXEC does not override this — a promotion is refused while Studio Pro holds the model.
exit=1

FORCE_EXEC=1 ./bin/exec.sh --on-copy <script>
(same three lines)
exit=1

git status and the model checksums were identical before and after, and no on-copy dir was left behind. PASS. It was refused up front, FORCE_EXEC did not override, and the live model was untouched. The lock was simulated, not written by a real Studio Pro.

Minor: this refusal writes no BUILD-LOG row, while the dirty-model refusal (b1) does.

c) Interrupted promotion

The Ctrl-C half was NOT RUN: the brief said "only if I say yes", and no yes was given. The no-op half was run:

./bin/exec.sh --recover-promote   (twice)
  Nothing to recover: no interrupted --on-copy promotion beside the model.

Both runs printed this.

After step 2

git checkout -- bin/exec.sh bin/_common.sh <model>.mpr mprcontents, plus removing bin/test-scratch.sh. git diff --stat:

 .claude/loop/lint-last.json | 7 +++----
 docs/BUILD-LOG.md           | 3 +++

PR #374: mxpatch + fix hashes (feat/mxpatch-and-fix-hashes @ 01b9b42)

mxcli is v0.26.0, so the step applies. Install: the PR's exec.sh and _common.sh were copied into bin/ (not synced, for the reason above). The patcher ran from the PR tree, beside its mxpatch.py.

Choosing the patcher (both dry runs are read-only):

  • wf-add-path-terminators.py <mpr> Module.Workflow: paths found: 0 … No parallel split in this unit -- nothing to do. exit 0. It would write nothing, so I didn't use it.
  • wf-set-call-captions.py: all 5 call-microflow captions had already been edited by hand. I picked one real write on 1 caption, with --expect-caption giving its current value.

Field confirmation of the bug, before the run. Standalone mxcli fix hashes on committed HEAD:

Checked 549 unit(s) in <model>.mpr against 549 .mxunit file(s).
  MISMATCH  74215fad-…  mprcontents/74/21/74215fad-….mxunit [Workflows$Workflow <the workflow>]
1 mismatch(es), 0 missing file(s), 0 orphan file(s).
exit=1

It's the very workflow unit an old-style --patch (wf-add-outcome-persistent-ids.py, BUILD-LOG 2026-10-02) had rewritten without updating the index, which is exactly what this PR fixes.

Command:

./bin/exec.sh --patch <pr>/project-bin/wf-set-call-captions.py Module.Workflow \
  --captions "Module.ACT_X=<caption> (field test)" --expect-caption "Module.ACT_X=<caption>" --apply

Output, trimmed:

→ ContentsHash baseline: mxcli fix hashes (read-only)...
  ⚠ the index is ALREADY out of step before this patch (1 mismatch(es), 0 missing file(s), 0 orphan file(s).).
    Not this patch's doing, and not held against it. Repair it on its own (rewrites the
    index from the files, never the files): ./mxcli fix hashes -p <model>.mpr --repair
→ Executing …/wf-set-call-captions.py...
captions to set: 1
note: the index was already stale for 74215fad-….mxunit before this patch (stored u2ZSfud+…); this write corrects it
wrote 74215fad-….mxunit (10580 bytes) and its ContentsHash W9sDEt6DRRMRsoFI3BbnEWecFM+1k7zX0+gpOrYPEgM=
backup: <project>/mprcontents/74/21/74215fad-….mxunit.bak
→ ContentsHash verify: mxcli fix hashes (read-only)...
  ✓ index matches every unit file
  ✓ mxbuild: 0 errors — model is clean.
✓ Script applied to <model>.mpr.
  elapsed: 103s
exit=0

BUILD-LOG row:

| 2026-10-15T11:41:45+0800 | `patch: wf-set-call-captions.py` | pass | ✅ applied | mxbuild clean (no errors file written); lint could not run (lint-gate exit 2) · change: logic · approval: auto (PROJECT.md) |

Independent check afterwards:

  • mxcli fix hashes gave 0 mismatch(es), 0 missing file(s), 0 orphan file(s). and exit 0.
  • describe workflow read back the new caption.

PASS. No new hash issue, the pre-existing mismatch was repaired as a side effect, mxbuild reported 0 errors, and exit was 0.

Warts:

  1. The patcher leaves <unit>.mxunit.bak inside mprcontents/. fix hashes doesn't count it, but git shows it as untracked inside the model folder, and a git add mprcontents would commit it.
  2. The BUILD-LOG row says nothing about the hash baseline or verify: not "baseline 1 mismatch, verify clean", and not that the patch repaired a mismatch that was already there. The evidence is only in stdout.
  3. The baseline message advises fix hashes --repair, but the patch then fixed that unit anyway. That's fine, but the message could say the unit the patch touches will be rewritten with a fresh hash.

Restore: git checkout -- bin/exec.sh bin/_common.sh <model>.mpr mprcontents and removing the .bak. The model is back at HEAD, including its pre-existing mismatch.


Final state of the field project

git diff --stat, after both restores:

 .claude/loop/lint-last.json | 7 +++----
 docs/BUILD-LOG.md           | 4 ++++

Model and bin/ are at HEAD. The only untracked files are the 10 that were there at the start. The 4 BUILD-LOG rows were kept as evidence.

All three worktrees were removed. Nothing was changed, committed or pushed in the toolkit clone (only git fetch).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions