Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m

## Unreleased

- 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: <docs>`. 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/<X>/`, the installed copy (`<project>/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 `<p>/app` or `<p>/apps/<X>` and `<p>` 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/<X>/` 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/<X>/` of a wired project now reads and writes state at the project root, as the installed copy already did; if `app/` (or `apps/<X>/`) 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 <project> --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)
- fix(look hook, verify-module, coverage): **the look hook records from any cwd, and `verify-module.sh` loses four instrument faults** (#446). (a) The PostToolUse(Read) hook was `bash bin/look-ledger.sh seen || true`: from any cwd but the project root (a subagent in a private copy, a shell in `app/javasource`) it exited 127 and `|| true` hid it (field: 30 pages owed, 1 seen). `install-claude-permissions.sh` now writes it to `cd "$CLAUDE_PROJECT_DIR"` first; a missing script or failed run goes to stderr, and is logged once to `.claude/loop/look/hook.log` when that can be written. The installer (and `sync-project.sh`'s hook repair) rewrites the old form in place and removes duplicate look hooks, so one remains; `doctor.sh` flags any look hook not in the root-resolving form. (b) A module with 0 pages in the page scope reports journeys N/A instead of a FAULT (`page-scope.sh` now runs before the runtime rungs; same mxcli calls) — jopterhorst (comment on #446); a missing journey JSON names any `tests/<module>*.test.sh` that exist in another shape; missing e2e dependencies (playwright, axe-core, probed from `JOURNEY_RUNNER`/`MONKEY_JS`/`DESIGN_AUDIT_JS`'s folders) are one FAULT row with the one fix (`npm install playwright axe-core && npx playwright install chromium`), journeys and monkey SKIPPED as dependents of playwright, the design audit static-only; the coverage rung's budget is 1200 s (was 300); `summary.tsv` ends with a `verdict` row (CLEAN / FINDINGS / INCOMPLETE), absent when a run was cut short (`report-normalize.js` skips it); and `coverage-preflight.sh`'s PHANTOM CLAIM pass gives each BRD only the claims it can resolve (12-BRD bench: 16.1 s to 4.2 s), with identical verdicts except that false PHANTOMs for BRDs in another source folder or named only by `.id` are gone (exit can drop 1→0). Found while proving it: every rung's watchdog `sleep` kept the script's stdout open, so `verify-module.sh | tee` waited up to 30 minutes after the verdict; the watchdogs now drop fds 3/4 and take their sleep with them. **Behaviour change to check in your project:** a page-less module's `summary.tsv` has `journeys SKIPPED (N/A: ...)` and the run can exit 0; missing e2e dependencies add an `e2e harness dependency` FAULT row and SKIPPED journeys/monkey rows; `summary.tsv` gains a last `verdict` row; `coverage-preflight.sh` can exit 0 where it exited 1 (the false PHANTOMs above); `.claude/settings.json`'s look hook command changes. Fixtures: `test-look-hook-cwd.sh`, `test-verify-module-446.sh`, `test-coverage-phantom-scope.sh`. — MendixMau (field 2026-10-17), jopterhorst (page-less modules)
Expand Down
12 changes: 12 additions & 0 deletions bin/gate-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand Down
4 changes: 3 additions & 1 deletion bin/lib/install-manifest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ 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 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"

# 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
Expand Down
41 changes: 39 additions & 2 deletions project-bin/exec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ HDR2
# The gate cell is read from GATE_STATE rather than passed in, so no caller
# can omit it — a blank cell in this table reads as "fine", which is the exact
# false-green the gate exists to prevent.
_detail="$2${CHANGE_CLASS:+ · change: $CHANGE_CLASS}${CR_ID:+ · cr: $CR_ID}${SCAN_NOTE:+ · $SCAN_NOTE}${GATE_NOTE:+ · $GATE_NOTE}${MXB_LOG_NOTE:+ · $MXB_LOG_NOTE}${HV_NOTE:+ · $HV_NOTE}$_EXEC_APPROVAL_SUFFIX${STEP_TIMES:+ · steps: $STEP_TIMES}"
_detail="$2${CHANGE_CLASS:+ · change: $CHANGE_CLASS}${CR_ID:+ · cr: $CR_ID}${SCAN_NOTE:+ · $SCAN_NOTE}${RESTATE_NOTE:+ · $RESTATE_NOTE}${GATE_NOTE:+ · $GATE_NOTE}${MXB_LOG_NOTE:+ · $MXB_LOG_NOTE}${HV_NOTE:+ · $HV_NOTE}$_EXEC_APPROVAL_SUFFIX${STEP_TIMES:+ · steps: $STEP_TIMES}"
printf '| %s | `%s` | %s | %s | %s |\n' \
"$(date '+%Y-%m-%dT%H:%M:%S%z')" "$LOG_NAME" \
"${GATE_STATE:-not-run}" "$1" "$_detail" >> "$BUILD_LOG"
Expand Down Expand Up @@ -568,6 +568,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 warning's cell text (#269), set after the check
if [ "$PATCH_MODE" != "1" ]; then
_SCAN=""
for _s in "${MXTK_ROOT:-}/bin/mdl-scan.sh" \
Expand Down Expand Up @@ -1124,6 +1125,34 @@ elif [ "${SKIP_CHECK:-0}" != "1" ] && [ -n "$PMXCLI" ]; then
echo " ✓ check clean"
fi

# ── Restatement warnings (#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. Three
# field 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: REVERT/UNCHECKED rows print
# as WARN lines and land in the BUILD-LOG detail; NOTE rows (a new owner without a supersedes
# header, first run only) print as one NOTE line and stay out of the log. The exec goes on and the
# exit code is whatever the rest of the chain decides. MXTK_RESTATE_GUARD=off skips it. Cost
# without a co-owner: one grep over mdlsource/ for the restated names, no mxcli call.
if [ "$PATCH_MODE" != "1" ] && [ "${MXTK_RESTATE_GUARD:-}" != "off" ] && [ -f "$SCRIPT" ] \
&& [ -f "$(dirname "$0")/script-owners.sh" ]; then
_so_out="$(PROJECT_ROOT="$PROJECT_ROOT" bash "$(dirname "$0")/script-owners.sh" check "$SCRIPT" "$PROJECT_ROOT" 2>/dev/null || true)"
_so_warn="$(printf '%s\n' "$_so_out" | grep -E '^(REVERT|UNCHECKED) ' || true)"
_so_note="$(printf '%s\n' "$_so_out" | grep '^NOTE ' || true)"
if [ -n "$_so_note" ]; then
echo "NOTE: new owner of $(printf '%s\n' "$_so_note" | cut -f2 | sed 's/^[A-Z]* //' | LC_ALL=C sort -u | tr '\n' ' ')— older owner(s): $(printf '%s\n' "$_so_note" | cut -f3 | LC_ALL=C sort -u | tr '\n' ' '); add '-- supersedes: <older script>' (STOP 21)"
fi
if [ -n "$_so_warn" ]; then
_so_docs="$(printf '%s\n' "$_so_warn" | 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 hold a newer owner's live work:"
printf '%s\n' "$_so_warn" | awk -F'\t' '{ printf " %s — %s\n", $2, $4 }'
echo " → edit the owning script or use alter (bin/script-owners.sh report; skills/learned-mdl-preflight.md STOP 21)"
RESTATE_NOTE="WARN reverts: $(printf '%s\n' "$_so_docs" | head -3 | tr '\n' ' ' | sed 's/ $//')"
if [ "$_so_n" -gt 3 ]; then RESTATE_NOTE="$RESTATE_NOTE +$((_so_n - 3))"; fi
fi
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
Expand Down Expand Up @@ -1202,7 +1231,9 @@ restore_snapshot() {
echo " ⚠ No usable snapshot to restore from — the model is left as the script wrote it."
elif [ ! -f "$RESTORER" ]; then
echo " ⚠ bin/restore-mpr.sh is not installed — cannot auto-restore (run sync-project.sh)."
elif R_OUT=$(bash "$RESTORER" "$NEWEST_SNAP" 2>&1); then
# The snapshot was taken just before this exec, so script-owners.sh's ledger still matches it:
# no RESTORE marker (restore-mpr.sh writes one for every other restore).
elif R_OUT=$(MXTK_RESTORE_SAME_LEDGER=1 bash "$RESTORER" "$NEWEST_SNAP" 2>&1); then
RESTORED=1
printf '%s\n' "$R_OUT" | sed 's/^/ → /'
echo " → Auto-restored from: $NEWEST_SNAP"
Expand Down Expand Up @@ -2070,6 +2101,12 @@ fi
if [ "$PATCH_MODE" = 0 ] && [ -f "$(dirname "$0")/look-ledger.sh" ]; then
bash "$(dirname "$0")/look-ledger.sh" owe "$SCRIPT" || true
fi
# Last writer of each document this script names (script-owners.sh ledger), so a later check can
# tell a replay that reverts nothing from one that does. Same place and reason as LOOK owed: a
# restored or partial run exited above. No mxcli call: one awk over this script.
if [ "$PATCH_MODE" = 0 ] && [ -f "$(dirname "$0")/script-owners.sh" ]; then
PROJECT_ROOT="$PROJECT_ROOT" bash "$(dirname "$0")/script-owners.sh" record "$SCRIPT" 2>/dev/null || true
fi

# ── Build plan page ──────────────────────────────────────────────────────────
# Refresh architecture/progress-report.html so it tracks the build without anyone remembering to
Expand Down
8 changes: 8 additions & 0 deletions project-bin/restore-mpr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,12 @@ else
exit 1
fi

# script-owners.sh's last-writer ledger described the model before this restore: mark it, so rows
# above the marker stop counting and its next replay check asks the model (DESCRIBE) instead.
# exec.sh's own rollback sets MXTK_RESTORE_SAME_LEDGER=1: that snapshot predates nothing recorded.
_SO_LEDGER="$PROJECT_ROOT/.claude/loop/script-owners/written.tsv"
if [ -f "$_SO_LEDGER" ] && [ "${MXTK_RESTORE_SAME_LEDGER:-0}" != 1 ]; then
printf '%s\tRESTORE\t%s\n' "$(date +%s)" "$(basename "$SNAP")" >> "$_SO_LEDGER" || true
fi

echo "Restore complete. Verify with: ./mxcli docker check -p $MPR --no-update-widgets"
Loading
Loading