From 05fefc82e2a8b7716960051b83099ac363ce4341 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 20:35:41 +0000 Subject: [PATCH 01/57] fix(page-shell): read `create page if not exists` declarations; report unparsed ones The page-name sed printed the whole declaration line when it did not match, so `create page if not exists "M"."P"` went on as the page name into a false "has no wireframe" STOP. Accept the optional IF NOT EXISTS, print only on a match, report an unparsed declaration as such, and skip a placeholder that the same file declares again for real. Fixes #517 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- CHANGELOG.md | 1 + project-bin/check-page-shell.sh | 19 +++++++++++++++++-- tests/wave2/test-page-shell-body.sh | 26 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eabec440..fba1f975 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(page-shell): **`check-page-shell.sh` reads `create page if not exists "M"."P"` as page `P`, skips that placeholder when the same file declares the page again for real, and reports a declaration it cannot parse as "page declaration not read" instead of a missing wireframe** (#517). The name `sed` printed the whole line on a non-match, so the declaration text went on as the page name into a false "has no wireframe" STOP. Fixture `test-page-shell-body.sh` cases 9–10 (4 asserts fail on master). — AlvinnYeo (#517) - fix(lint): **the toolkit's CONV020 (page actions give user feedback) is now `MXTK-UX003` (`mxtk_ux003_action_user_feedback.star`), because mxcli v0.28.0 ships a built-in CONV020 ("Created object never persisted", mendixlabs/mxcli#1518) and has no duplicate-ID guard.** Captured on a public model (`tests/wave2/fixtures/lint-conv020-v028/`): v0.28.0 reported two CONV020 findings, one per rule, `-l` listed CONV020 twice, and `CONV020: enabled: false` switched both off. `bin/sync-project.sh` migrates projects: an unedited old copy (md5 of any shipped version, CR-insensitive, or the receipt row) is replaced, `lint-baseline.json` findings whose message is the toolkit rule's are rekeyed to MXTK-UX003 (the built-in's stay CONV020), and a `lint-rules-disabled.txt` line follows the file. A lint-config opt-out of CONV020 is carried over too: a `lint-rules-disabled.txt` line for the new file keeps MXTK-UX003 off (the key stays; on v0.28.0 it switches off only the built-in). It is read the way mxcli v0.28.0 reads it (yaml.v3, captured per spelling): `enabled:` `false`/`False`/`FALSE` or `n`/`no`/`off` in any of yaml.v3's casings (`No`, `NO`, `Off`, `OFF`…, plain or quoted), as a direct child of a bare, single- or double-quoted `CONV020` key or in its flow map, from the first of `.claude/lint-config.yaml`, `lint-config.yaml`, `.lint-config.yaml`; an `enabled` nested under `options:` and a quoted `"false"` (mxcli fails to load that config) are not opt-outs. An edited copy is left in place with a warning and manual steps (with the `lint-rules-disabled.txt` line to keep such an opt-out), and the new rule is not installed beside it. A counts-only baseline, `lint-config.yaml` keys and `PROJECT.md` lines that name CONV020 get a NOTE, not a guess, printed once. New toolkit rules take an `MXTK-` id (`lint-rules/README.md` "Rule IDs"). After any mxcli upgrade, `lint-gate.sh --update-baseline` now records `mxcli_version`, and `bin/doctor.sh` prints the re-baseline hint when the installed mxcli differs from the recorded one (a baseline with no recorded version gets a note, no command; `skills/lint-that-actually-runs.md`). Decision #12 — mxcli v0.28.0 test pass - process(routing, mdl-agent): **the mdl helper no longer preloads the kind-specific MDL skills at Stage 5; it loads them per script through `bin/mdl-rules.sh`** — lazy-load PR 2b. `learned-microflow-patterns.md`, `microflow-preflight.md` and `ui-preflight-pages.md` move to on-demand; `learned-mdl-preflight.md`, `design-spacing.md`, `learned-detection-gaps.md`, `security-is-not-a-later-script.md` and `retesting-learned-rules.md` become on-demand for mdl only (`mdl?`), unchanged for every other role; a new baseline `mdl-rules` row (Stages 5 and 6, so a Stage 6 mdl pack keeps the detection-gaps and retesting rules; mdl Stage 6 preload 9,476 words) replaces them. Baseline 72,891 → 63,877 words; mdl Stage 5 preload 29,388 → 14,600 (per script, after reading the `mdl-rules` output, the helper reads ≈21.4k words for page only, ≈22.5k for microflow+access, ≈28.1k for page+microflow, ≈29.5k for all kinds: ≈0–8k saved by kind; the A/B covered rule sufficiency with kinds pre-selected, not step 4a) (the 3,347-word core of `bin/mdl-rules.sh` counted, like the runbook slice). The mdl stub's workflow step 4a and report line ask for "STOP rows for kinds K: n/n checked". `context-pack.sh`'s "Rules" list names `/bin/mdl-rules.sh ` with the step's kinds (page / microflow / entity) — after this change the only route `microflow-preflight`, `security-is-not-a-later-script`, `design-spacing`, `learned-detection-gaps` and `retesting-learned-rules` have to a pack-fed helper (fixture: a page step and a logic step). `gate-check.sh` build-ready item 2 accepts `bin/mdl-rules.sh` as well as `ui-preflight-pages`, so a project synced after this change does not flip to FAIL (fixture: both layouts). No skill text changed — lazy-load plan, option C (maintainer) - new(mdl-rules): **`bin/mdl-rules.sh | --for ` prints the MDL rules one script needs, by the kinds of statement it writes** — lazy-load PR 2a. `bin/lib/mdl-rule-kinds.tsv` maps page / microflow / access / workflow / entity / any to sections located by file and heading text (never line numbers): the STOP rows by their `Kind` column, the kind-specific gotchas, `learned-microflow-patterns.md`, `microflow-preflight.md`, `ui-preflight-pages.md`, `design-spacing.md` §1–3, the detection-gaps register rows by construct, `security-is-not-a-later-script.md` and `retesting-learned-rules.md`. Output is the core (Step 0, the `any` STOP rows, gotchas, final self-check: 3,347 words) plus the kind sections, closed by "Rules for kinds K: STOP rows n/n listed" (page 10,223 words, microflow 10,186, access 4,730, entity 5,183, workflow 3,732), against the 29,388 words the mdl helper preloads at Stage 5 today. `--for` reuses exec.sh's statement classifier (`exec.sh --subjects`; Java actions and mappings count as microflow, demo users as access) and exits 1 if it fails; a heading that is gone fails loudly, never prints empty. `skills/generated/rules-.md` are the same output for harnesses that read files (Copilot, Cursor, Windsurf); `--check` (CI) fails on drift, on any STOP row, gotcha, register row or `##` section that reaches no kind (a section left out on purpose needs an `exclude` row with its reason), and on any register ERE that matches no row by itself, and on an unclosed code fence in a file it reads. No routing change yet: PR 2b demotes the preloads. Behaviour suite: `build-packs.sh --rules ""` expands the script in a pack. Fixture `tests/wave2/test-mdl-rules.sh` — lazy-load plan, option C (maintainer) diff --git a/project-bin/check-page-shell.sh b/project-bin/check-page-shell.sh index 54c416b9..01bfef5c 100755 --- a/project-bin/check-page-shell.sh +++ b/project-bin/check-page-shell.sh @@ -189,8 +189,23 @@ for f in $TARGETS; do case "$txt" in *page-shell-ok:*) continue ;; esac - page="$(printf '%s' "$txt" | sed -E 's/.*[Pp][Aa][Gg][Ee][[:space:]]+"?[A-Za-z0-9_]+"?\."?([A-Za-z0-9_]+)"?.*/\1/')" - [ -n "$page" ] || continue + # `-n ... p`: print only on a match, so an unreadable declaration yields "" and is + # reported as such. Without `-n`, `create page if not exists "M"."P"` (a placeholder that + # mxcli accepts) did not match, sed printed the whole line, and that line went on as the + # "page name" into a false "has no wireframe" STOP (#517, field-found 2026-10-18). + page="$(printf '%s' "$txt" | sed -nE 's/.*[Pp][Aa][Gg][Ee][[:space:]]+([Ii][Ff][[:space:]]+[Nn][Oo][Tt][[:space:]]+[Ee][Xx][Ii][Ss][Tt][Ss][[:space:]]+)?"?[A-Za-z0-9_]+"?\."?([A-Za-z0-9_]+)"?.*/\2/p')" + if [ -z "$page" ]; then + report "$f:$ln" "page declaration not read: could not parse a Module.Page name from this line" \ + 'Write the declaration as `create [or modify|or replace] page [if not exists] "Module"."Page"` on one line, or mark it `-- page-shell-ok: `.' + continue + fi + # A placeholder (`if not exists`) that the same file declares again for real is not the + # page that ships: the later create/modify/replace is, and that one is checked. + if printf '%s' "$txt" | grep -qiE 'PAGE[[:space:]]+IF[[:space:]]+NOT[[:space:]]+EXISTS' \ + && grep -viE 'IF[[:space:]]+NOT[[:space:]]+EXISTS' "$f" \ + | grep -iE "^[[:space:]]*CREATE([[:space:]]+OR[[:space:]]+(MODIFY|REPLACE))?[[:space:]]+PAGE[[:space:]]+\"?[A-Za-z0-9_]+\"?\.\"?$page\"?([^A-Za-z0-9_]|\$)" >/dev/null; then + continue + fi wf_for "$page" if [ ! -f "$wf" ]; then diff --git a/tests/wave2/test-page-shell-body.sh b/tests/wave2/test-page-shell-body.sh index 666f5a11..f9e75ba8 100755 --- a/tests/wave2/test-page-shell-body.sh +++ b/tests/wave2/test-page-shell-body.sh @@ -194,5 +194,31 @@ check "an opted-out page is skipped entirely" "$(code "$d")" "2" out="$(run "$d")" has "and inspecting nothing is NOT a pass" "$out" "NOT a pass" +# ── 9. a placeholder `if not exists` declaration is read, and skipped when redeclared (#517) +d="$(proj placeholder)" +wireframe "$d" "Some_Page.html" +cat > "$d/mdlsource/pages.mdl" <<'EOF' +create page if not exists "MOC"."Some_Page" folder 'F' ( Title: 'x', Layout: "MOC"."App_Default" ) { container c { } }; +create or modify page "MOC"."Some_Page" folder 'F' + (Title: 'Some', Layout: "MOC"."App_Default") + { HEADER hdr { DYNAMICTEXT h1 (Content: 'Some', RenderMode: H1) } }; +EOF +out="$(run "$d")" +check "placeholder + real page exit 0" "$(code "$d")" "0" +hasnt "the declaration line is never a page name" "$out" "page create page" +has "one page checked: the real one" "$out" "1 page(s)" + +# ── 10. an unparseable declaration is reported as unread, not as a missing wireframe ──── +d="$(proj unparsed)" +wireframe "$d" "Other.html" +cat > "$d/mdlsource/pages.mdl" <<'EOF' +create page if not exists ( + Title: 'x' +) { container c { } }; +EOF +out="$(run "$d")" +has "says the name was not parsed" "$out" "page declaration not read" +hasnt "does not claim a missing wireframe" "$out" "has no wireframe" + printf '\n%s passed, %s failed\n' "$PASS" "$FAIL" [ "$FAIL" -eq 0 ] || exit 1 From cff3154efcbae2d4bea1a898bbb9dd829d272464 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 20:36:25 +0000 Subject: [PATCH 02/57] fix(doctor): gate self-test leaves Studio Pro's .mpr.lock out of the scratch copy gate_selftest copied every sibling of the .mpr, including .mpr.lock, so mxcli refused the known-bad injection into the throwaway copy whenever Studio Pro had the project open, and the self-test proved only its baseline half. Case 11 added to test-doctor-gate-selftest.sh (fails on master, 32/32 with the fix). Fixes #577 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- CHANGELOG.md | 1 + bin/doctor.sh | 6 +++++- tests/wave2/test-doctor-gate-selftest.sh | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eabec440..21f2d023 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(doctor): **the gate self-test no longer copies Studio Pro's `.mpr.lock` into its scratch copy**, so with the project open in Studio Pro the known-bad injection is no longer refused and the self-test proves both halves instead of reporting NOT RUN (#577). Case 11 in `tests/wave2/test-doctor-gate-selftest.sh`. Field report, macOS, Studio Pro 11.15.0, mxcli v0.28.0 — MendixMau - fix(lint): **the toolkit's CONV020 (page actions give user feedback) is now `MXTK-UX003` (`mxtk_ux003_action_user_feedback.star`), because mxcli v0.28.0 ships a built-in CONV020 ("Created object never persisted", mendixlabs/mxcli#1518) and has no duplicate-ID guard.** Captured on a public model (`tests/wave2/fixtures/lint-conv020-v028/`): v0.28.0 reported two CONV020 findings, one per rule, `-l` listed CONV020 twice, and `CONV020: enabled: false` switched both off. `bin/sync-project.sh` migrates projects: an unedited old copy (md5 of any shipped version, CR-insensitive, or the receipt row) is replaced, `lint-baseline.json` findings whose message is the toolkit rule's are rekeyed to MXTK-UX003 (the built-in's stay CONV020), and a `lint-rules-disabled.txt` line follows the file. A lint-config opt-out of CONV020 is carried over too: a `lint-rules-disabled.txt` line for the new file keeps MXTK-UX003 off (the key stays; on v0.28.0 it switches off only the built-in). It is read the way mxcli v0.28.0 reads it (yaml.v3, captured per spelling): `enabled:` `false`/`False`/`FALSE` or `n`/`no`/`off` in any of yaml.v3's casings (`No`, `NO`, `Off`, `OFF`…, plain or quoted), as a direct child of a bare, single- or double-quoted `CONV020` key or in its flow map, from the first of `.claude/lint-config.yaml`, `lint-config.yaml`, `.lint-config.yaml`; an `enabled` nested under `options:` and a quoted `"false"` (mxcli fails to load that config) are not opt-outs. An edited copy is left in place with a warning and manual steps (with the `lint-rules-disabled.txt` line to keep such an opt-out), and the new rule is not installed beside it. A counts-only baseline, `lint-config.yaml` keys and `PROJECT.md` lines that name CONV020 get a NOTE, not a guess, printed once. New toolkit rules take an `MXTK-` id (`lint-rules/README.md` "Rule IDs"). After any mxcli upgrade, `lint-gate.sh --update-baseline` now records `mxcli_version`, and `bin/doctor.sh` prints the re-baseline hint when the installed mxcli differs from the recorded one (a baseline with no recorded version gets a note, no command; `skills/lint-that-actually-runs.md`). Decision #12 — mxcli v0.28.0 test pass - process(routing, mdl-agent): **the mdl helper no longer preloads the kind-specific MDL skills at Stage 5; it loads them per script through `bin/mdl-rules.sh`** — lazy-load PR 2b. `learned-microflow-patterns.md`, `microflow-preflight.md` and `ui-preflight-pages.md` move to on-demand; `learned-mdl-preflight.md`, `design-spacing.md`, `learned-detection-gaps.md`, `security-is-not-a-later-script.md` and `retesting-learned-rules.md` become on-demand for mdl only (`mdl?`), unchanged for every other role; a new baseline `mdl-rules` row (Stages 5 and 6, so a Stage 6 mdl pack keeps the detection-gaps and retesting rules; mdl Stage 6 preload 9,476 words) replaces them. Baseline 72,891 → 63,877 words; mdl Stage 5 preload 29,388 → 14,600 (per script, after reading the `mdl-rules` output, the helper reads ≈21.4k words for page only, ≈22.5k for microflow+access, ≈28.1k for page+microflow, ≈29.5k for all kinds: ≈0–8k saved by kind; the A/B covered rule sufficiency with kinds pre-selected, not step 4a) (the 3,347-word core of `bin/mdl-rules.sh` counted, like the runbook slice). The mdl stub's workflow step 4a and report line ask for "STOP rows for kinds K: n/n checked". `context-pack.sh`'s "Rules" list names `/bin/mdl-rules.sh ` with the step's kinds (page / microflow / entity) — after this change the only route `microflow-preflight`, `security-is-not-a-later-script`, `design-spacing`, `learned-detection-gaps` and `retesting-learned-rules` have to a pack-fed helper (fixture: a page step and a logic step). `gate-check.sh` build-ready item 2 accepts `bin/mdl-rules.sh` as well as `ui-preflight-pages`, so a project synced after this change does not flip to FAIL (fixture: both layouts). No skill text changed — lazy-load plan, option C (maintainer) - new(mdl-rules): **`bin/mdl-rules.sh | --for ` prints the MDL rules one script needs, by the kinds of statement it writes** — lazy-load PR 2a. `bin/lib/mdl-rule-kinds.tsv` maps page / microflow / access / workflow / entity / any to sections located by file and heading text (never line numbers): the STOP rows by their `Kind` column, the kind-specific gotchas, `learned-microflow-patterns.md`, `microflow-preflight.md`, `ui-preflight-pages.md`, `design-spacing.md` §1–3, the detection-gaps register rows by construct, `security-is-not-a-later-script.md` and `retesting-learned-rules.md`. Output is the core (Step 0, the `any` STOP rows, gotchas, final self-check: 3,347 words) plus the kind sections, closed by "Rules for kinds K: STOP rows n/n listed" (page 10,223 words, microflow 10,186, access 4,730, entity 5,183, workflow 3,732), against the 29,388 words the mdl helper preloads at Stage 5 today. `--for` reuses exec.sh's statement classifier (`exec.sh --subjects`; Java actions and mappings count as microflow, demo users as access) and exits 1 if it fails; a heading that is gone fails loudly, never prints empty. `skills/generated/rules-.md` are the same output for harnesses that read files (Copilot, Cursor, Windsurf); `--check` (CI) fails on drift, on any STOP row, gotcha, register row or `##` section that reaches no kind (a section left out on purpose needs an `exclude` row with its reason), and on any register ERE that matches no row by itself, and on an unclosed code fence in a file it reads. No routing change yet: PR 2b demotes the preloads. Behaviour suite: `build-packs.sh --rules ""` expands the script in a pack. Fixture `tests/wave2/test-mdl-rules.sh` — lazy-load plan, option C (maintainer) diff --git a/bin/doctor.sh b/bin/doctor.sh index af52b3d9..a76ddf75 100755 --- a/bin/doctor.sh +++ b/bin/doctor.sh @@ -1450,6 +1450,10 @@ gate_selftest() { for x in $copy_excl; do [ "$entry_name" = "$x" ] && continue 2 done + # Studio Pro's lock file: copied along, it makes mxcli refuse the known-bad injection + # into the throwaway copy ("Studio Pro has this project open"), the normal case during + # setup (#577). The lock belongs to the real model, never to the scratch copy. + case "$entry_name" in *.mpr.lock) continue ;; esac cp -R "$entry" "$scratch/" 2>/dev/null || copy_ok=0 done scratch_mpr="$scratch/$(basename "$MPR")" @@ -1459,7 +1463,7 @@ gate_selftest() { return 0 fi copy_size="$(du -sh "$scratch" 2>/dev/null | awk '{print $1}')" - note "copied ${copy_size:-?} of project resources into the scratch dir (excluding .git, deployment/, node_modules/, .mpr-snapshots/)" + note "copied ${copy_size:-?} of project resources into the scratch dir (excluding .git, deployment/, node_modules/, .mpr-snapshots/, *.mpr.lock)" # (a) Baseline: the gate must resolve SOME integer off this model, clean or not — "?" here # means the gate cannot read mxbuild's own output, which is the original F-042-class defect. diff --git a/tests/wave2/test-doctor-gate-selftest.sh b/tests/wave2/test-doctor-gate-selftest.sh index e70ad077..0f3a92c4 100644 --- a/tests/wave2/test-doctor-gate-selftest.sh +++ b/tests/wave2/test-doctor-gate-selftest.sh @@ -45,6 +45,10 @@ case "${1:-}" in [ "$prev" = "-p" ] && mpr="$a" prev="$a" done + # Real mxcli refuses to write a model whose Studio Pro lock file sits beside it (#577). + if [ -n "$mpr" ] && [ -e "$mpr.lock" ]; then + echo "Error: refusing to write $(basename "$mpr"): Studio Pro has this project open (lock file $mpr.lock)" >&2; exit 1 + fi if [ -n "$mpr" ] && grep -q GateSelftest "$mdl" 2>/dev/null; then : > "$(dirname "$mpr")/.gate-selftest-bad-applied" fi @@ -216,5 +220,18 @@ assert_contains "$T/out.twotree" "known-bad control: gate read" "two-tree: known assert_missing "$T/out.twotree" "gate self-test:" "two-tree: no gate-self-test FAIL line" assert_contains "$P2/.claude/.doctor-receipt" "gate-selftest: pass" "two-tree: receipt records pass" +# ── 11: Studio Pro open (.mpr.lock beside the real model) -> lock not copied, PASS ───── +# #577: the scratch copy took the lock file along, so mxcli refused the injection into the +# throwaway copy and the self-test reported only its baseline half. +echo "== 11: Studio Pro lock file beside the model -> not copied, known-bad control still runs ==" +: > "$P/Fixture.mpr.lock" +MXBUILD_PATH="$T/mxbuild" MODE_GATE_STUB=healthy \ + bash "$DOCTOR" --quick --gate-selftest --no-docker "$P" > "$T/out.lock" 2>&1 +assert_missing "$T/out.lock" "Studio Pro has this project open" "lock: injection not refused" +assert_contains "$T/out.lock" "known-bad control: gate read" "lock: known-bad control ran" +assert_contains "$P/.claude/.doctor-receipt" "gate-selftest: pass" "lock: receipt records pass" +[ -e "$P/Fixture.mpr.lock" ] && ok "lock: the real model's lock file is untouched" || fail "lock: real lock file removed" +rm -f "$P/Fixture.mpr.lock" "$P/.gate-selftest-bad-applied" + echo; echo "$PASS passed, $FAIL failed ($T)" [ "$FAIL" -eq 0 ] From a5173d736233d5fa4021d45231272bd5a6a0882a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 20:36:37 +0000 Subject: [PATCH 03/57] fix(preflight, exec): mxcli v0.27+ exec is all or nothing; stop calling it "not transactional" STOP #22, the detection-gaps register row, iterative-build-loop's gate invariants and exec.sh's failed-exec message taught the pre-v0.27 behaviour (earlier statements stay written). On v0.27+ any write-time failure rolls the whole run back, so a non-idempotent statement placed last now fails every re-run and drops the edits above it. STOP #22's rule is unchanged; only the reasoning and the consequence move. Baseline 63,877 -> 63,872 words. Fixes #539 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- CHANGELOG.md | 1 + project-bin/exec.sh | 14 ++++++++------ skills/generated/rules-access.md | 4 ++-- skills/generated/rules-any.md | 4 ++-- skills/generated/rules-entity.md | 4 ++-- skills/generated/rules-microflow.md | 4 ++-- skills/generated/rules-page.md | 4 ++-- skills/generated/rules-workflow.md | 4 ++-- skills/iterative-build-loop.md | 4 ++-- skills/learned-detection-gaps.md | 2 +- skills/learned-mdl-preflight.md | 2 +- 11 files changed, 25 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eabec440..2f94e53c 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(preflight, detection-gaps, exec): **mxcli v0.27+ `exec` is all or nothing; STOP #22, the detection-gaps register row, `iterative-build-loop.md`'s gate invariants and `exec.sh`'s failed-exec message no longer say it is "not transactional".** STOP #22 keeps its rule (`create or modify` everywhere, one owner per document, from-empty replay) and now says why on v0.27+: any write failure rolls back the whole run, so one non-idempotent statement, even placed last, fails every re-run and drops every edit in it. `exec.sh` stops printing "PARTIAL APPLICATION LIKELY" and says which behaviour applies by version; the gate still runs. Baseline 63,877 → 63,872 words; `skills/generated/rules-*.md` re-rendered. — AlvinnYeo (#539) - fix(lint): **the toolkit's CONV020 (page actions give user feedback) is now `MXTK-UX003` (`mxtk_ux003_action_user_feedback.star`), because mxcli v0.28.0 ships a built-in CONV020 ("Created object never persisted", mendixlabs/mxcli#1518) and has no duplicate-ID guard.** Captured on a public model (`tests/wave2/fixtures/lint-conv020-v028/`): v0.28.0 reported two CONV020 findings, one per rule, `-l` listed CONV020 twice, and `CONV020: enabled: false` switched both off. `bin/sync-project.sh` migrates projects: an unedited old copy (md5 of any shipped version, CR-insensitive, or the receipt row) is replaced, `lint-baseline.json` findings whose message is the toolkit rule's are rekeyed to MXTK-UX003 (the built-in's stay CONV020), and a `lint-rules-disabled.txt` line follows the file. A lint-config opt-out of CONV020 is carried over too: a `lint-rules-disabled.txt` line for the new file keeps MXTK-UX003 off (the key stays; on v0.28.0 it switches off only the built-in). It is read the way mxcli v0.28.0 reads it (yaml.v3, captured per spelling): `enabled:` `false`/`False`/`FALSE` or `n`/`no`/`off` in any of yaml.v3's casings (`No`, `NO`, `Off`, `OFF`…, plain or quoted), as a direct child of a bare, single- or double-quoted `CONV020` key or in its flow map, from the first of `.claude/lint-config.yaml`, `lint-config.yaml`, `.lint-config.yaml`; an `enabled` nested under `options:` and a quoted `"false"` (mxcli fails to load that config) are not opt-outs. An edited copy is left in place with a warning and manual steps (with the `lint-rules-disabled.txt` line to keep such an opt-out), and the new rule is not installed beside it. A counts-only baseline, `lint-config.yaml` keys and `PROJECT.md` lines that name CONV020 get a NOTE, not a guess, printed once. New toolkit rules take an `MXTK-` id (`lint-rules/README.md` "Rule IDs"). After any mxcli upgrade, `lint-gate.sh --update-baseline` now records `mxcli_version`, and `bin/doctor.sh` prints the re-baseline hint when the installed mxcli differs from the recorded one (a baseline with no recorded version gets a note, no command; `skills/lint-that-actually-runs.md`). Decision #12 — mxcli v0.28.0 test pass - process(routing, mdl-agent): **the mdl helper no longer preloads the kind-specific MDL skills at Stage 5; it loads them per script through `bin/mdl-rules.sh`** — lazy-load PR 2b. `learned-microflow-patterns.md`, `microflow-preflight.md` and `ui-preflight-pages.md` move to on-demand; `learned-mdl-preflight.md`, `design-spacing.md`, `learned-detection-gaps.md`, `security-is-not-a-later-script.md` and `retesting-learned-rules.md` become on-demand for mdl only (`mdl?`), unchanged for every other role; a new baseline `mdl-rules` row (Stages 5 and 6, so a Stage 6 mdl pack keeps the detection-gaps and retesting rules; mdl Stage 6 preload 9,476 words) replaces them. Baseline 72,891 → 63,877 words; mdl Stage 5 preload 29,388 → 14,600 (per script, after reading the `mdl-rules` output, the helper reads ≈21.4k words for page only, ≈22.5k for microflow+access, ≈28.1k for page+microflow, ≈29.5k for all kinds: ≈0–8k saved by kind; the A/B covered rule sufficiency with kinds pre-selected, not step 4a) (the 3,347-word core of `bin/mdl-rules.sh` counted, like the runbook slice). The mdl stub's workflow step 4a and report line ask for "STOP rows for kinds K: n/n checked". `context-pack.sh`'s "Rules" list names `/bin/mdl-rules.sh ` with the step's kinds (page / microflow / entity) — after this change the only route `microflow-preflight`, `security-is-not-a-later-script`, `design-spacing`, `learned-detection-gaps` and `retesting-learned-rules` have to a pack-fed helper (fixture: a page step and a logic step). `gate-check.sh` build-ready item 2 accepts `bin/mdl-rules.sh` as well as `ui-preflight-pages`, so a project synced after this change does not flip to FAIL (fixture: both layouts). No skill text changed — lazy-load plan, option C (maintainer) - new(mdl-rules): **`bin/mdl-rules.sh | --for ` prints the MDL rules one script needs, by the kinds of statement it writes** — lazy-load PR 2a. `bin/lib/mdl-rule-kinds.tsv` maps page / microflow / access / workflow / entity / any to sections located by file and heading text (never line numbers): the STOP rows by their `Kind` column, the kind-specific gotchas, `learned-microflow-patterns.md`, `microflow-preflight.md`, `ui-preflight-pages.md`, `design-spacing.md` §1–3, the detection-gaps register rows by construct, `security-is-not-a-later-script.md` and `retesting-learned-rules.md`. Output is the core (Step 0, the `any` STOP rows, gotchas, final self-check: 3,347 words) plus the kind sections, closed by "Rules for kinds K: STOP rows n/n listed" (page 10,223 words, microflow 10,186, access 4,730, entity 5,183, workflow 3,732), against the 29,388 words the mdl helper preloads at Stage 5 today. `--for` reuses exec.sh's statement classifier (`exec.sh --subjects`; Java actions and mappings count as microflow, demo users as access) and exits 1 if it fails; a heading that is gone fails loudly, never prints empty. `skills/generated/rules-.md` are the same output for harnesses that read files (Copilot, Cursor, Windsurf); `--check` (CI) fails on drift, on any STOP row, gotcha, register row or `##` section that reaches no kind (a section left out on purpose needs an `exclude` row with its reason), and on any register ERE that matches no row by itself, and on an unclosed code fence in a file it reads. No routing change yet: PR 2b demotes the preloads. Behaviour suite: `build-packs.sh --rules ""` expands the script in a pack. Fixture `tests/wave2/test-mdl-rules.sh` — lazy-load plan, option C (maintainer) diff --git a/project-bin/exec.sh b/project-bin/exec.sh index 929cd06c..fdb3f91d 100755 --- a/project-bin/exec.sh +++ b/project-bin/exec.sh @@ -1611,9 +1611,9 @@ fi # # This is the fix for a defect that fired three times before anyone traced it: # with a bare `./mxcli exec`, a mid-script failure aborted exec.sh instantly — -# skipping the mxbuild gate, the restore, and the build-log entry. And mxcli is -# NOT transactional across statements, so every statement before the failing one -# was already applied. The result was live, ungated, unlogged model changes, +# skipping the mxbuild gate, the restore, and the build-log entry. And mxcli +# before v0.27 is NOT transactional across statements, so every statement before +# the failing one was already applied (v0.27+ rolls the whole run back). The result was live, ungated, unlogged model changes, # arrived at by the guard chain exiting through the one path with no guards on # it. A failed exec is precisely when the gate matters most. step_done baseline @@ -1736,9 +1736,11 @@ fi if [ "$EXEC_STATUS" -ne 0 ]; then echo "" - echo " ✗ mxcli exec exited $EXEC_STATUS — PARTIAL APPLICATION LIKELY." - echo " mxcli is not transactional across statements: everything before the" - echo " failing statement is already in the model. Running the gate anyway." + echo " ✗ mxcli exec exited $EXEC_STATUS." + echo " mxcli v0.27+ rolls the whole run back (\"Nothing was written\" above): no" + echo " statement of this script is in the model. Older mxcli is not transactional:" + echo " everything before the failing statement is already in the model." + echo " Running the gate anyway." fi step_done exec diff --git a/skills/generated/rules-access.md b/skills/generated/rules-access.md index 1ea0c08f..a617fbbb 100644 --- a/skills/generated/rules-access.md +++ b/skills/generated/rules-access.md @@ -47,7 +47,7 @@ Each row keeps one failure clause; the full root cause is in `process/baseline-i | 14 | Narrow an existing entity grant (fewer attributes or rights than the rule already has) | **`revoke` first, then `grant` the narrower set.** A lone narrowing `grant` merges into the existing rule instead of replacing it (BUG-145). | The old, wider rights survive the narrowing `grant`. | access | | 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. | Invalid Mendix XPath, not an mxcli bug: `CE0161` every time. The fixed form is mxcli-safe — no GUI fallback. | access | | 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: '); + const instant = await p.locator('#x').isVisible({ timeout: 5000 }); + const t = Date.now(); const waited = await H.visibleWithin(p.locator('#x'), 5000); const ms = Date.now() - t; + const miss = await H.visibleWithin(p.locator('#nope'), 300); + await b.close(); + console.log('REAL ' + JSON.stringify({ instant, waited, slow: ms >= 1000, miss })); +})().then(() => process.exit(0)); +JSEOF +OUT="$(cd "$WORK/tests/e2e" && printf '%s' "$JS" | APP_PORT=1 timeout 60 node - 2>&1)" +get() { printf '%s\n' "$OUT" | sed -n "s/^$1 //p"; } +if printf '%s\n' "$OUT" | grep -qx NOFN || [ -z "$(get FAKE)" ]; then + bad "#536 helpers.js exports no visibleWithin()" "$(printf '%s\n' "$OUT" | tail -3)" +else + [ "$(get FAKE)" = '{"late":true,"lateCalls":["waitFor:visible:2000"],"never":false}' ] \ + && ok "#536 visibleWithin() waits through waitFor({state:'visible'}) with its budget: late element true, absent false" \ + || bad "#536 visibleWithin() does not wait as stated" "$(get FAKE)" + case "$(get REAL)" in + SKIP*) echo " skip — real-browser reproduction: $(get REAL)" ;; + '{"instant":false,"waited":true,"slow":true,"miss":false}') ok "#536 Chromium: isVisible({timeout}) answers false at once; visibleWithin() waits ~2 s and answers true" ;; + *) bad "#536 real-browser reproduction" "$(get REAL)" ;; + esac +fi +# The callers that mean "wait" use it; the misleading form is gone from them. +grep -qF "await visibleWithin(page.locator(target.ready).first(), 6000);" "$SUT" \ + && ok "#536 gotoTarget() waits for target.ready" || bad "#536 gotoTarget() still reads target.ready with isVisible({timeout})" +if [ -f "$RUNNER" ]; then + n="$(grep -c 'H.visibleWithin(' "$RUNNER")" + [ "$n" -ge 3 ] && ! grep -qE "(el|box)\.isVisible\(\{ timeout" "$RUNNER" \ + && ok "#536 journey-runner click / fill / combobox wait with visibleWithin() ($n sites, no isVisible({timeout}) left on them)" \ + || bad "#536 a journey-runner action still uses isVisible({timeout})" "$n sites; $(grep -nE '(el|box)\.isVisible\(\{ timeout' "$RUNNER" | head -3)" +fi + +echo "" +echo "PASS=$PASS FAIL=$FAIL" +[ "$FAIL" -eq 0 ] From 9926b91ac350d80f0834b0cbfb8f95c727206ec6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 20:45:09 +0000 Subject: [PATCH 21/57] fix(status): WATCH when the lint baseline, acceptances or local rules are git-ignored A project .gitignore with /.claude/ kept lint-baseline.json, lint-acceptances.tsv and project-local lint rules out of git; a fresh clone then took a new baseline and lost the accepted-rise record, and no check said so. status.sh (brief and full screen) now adds a WATCH entry naming each ignored path and the .gitignore line, decided by check-ignore -q so a '!' negation counts as tracked. No gate verdict changes. New fixture test-status-lint-unversioned.sh: 4/4 (T1 and T4 fail on master). test-status-bare-var 3/3, test-status-existing-app 27/27, test-status-session-cwd 10/10. Fixes #523 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- CHANGELOG.md | 1 + bin/status.sh | 24 +++++++- tests/wave2/test-status-lint-unversioned.sh | 66 +++++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 tests/wave2/test-status-lint-unversioned.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3f08e8..04e3916a 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(status): **`bin/status.sh` WATCHes when the lint debt is git-ignored** — `lint debt is not versioned: git-ignored .claude/lint-baseline.json (.gitignore:2 '/.claude/'), …` for the baseline, `lint-acceptances.tsv` and project-local `lint-rules/`, so a `/.claude/` line in a project's `.gitignore` no longer silently drops the ratchet and the record of accepted rises from git (#523). A WATCH line only, nothing blocks. New fixture `tests/wave2/test-status-lint-unversioned.sh`. Reported by AlvinnYeo - fix(lint): **the toolkit's CONV010 rule (`conv010_act_microflow_content.star`) now allows `NanoflowCallAction`**, so an ACT_ nanoflow that delegates to a SUB_ nanoflow is no longer flagged as business logic — the same delegation the rule already allows for microflows, and what mxcli's stock copy allows (#512). Reproduced and verified with mxcli v0.28.0 on a scratch copy of the public `testdata/pedapp` model: 1 CONV010 finding before, 0 after. Projects get the rule on the next `bin/sync-project.sh` (unedited copies refresh); their CONV010 count can only drop. Reported by jopterhorst - fix(doctor): **when mxbuild stops before checking the model, the gate self-test now quotes mxbuild's own reason** (`mxbuild stopped before checking the model (baseline run): An error occurred while compiling Java actions (release version 25 not supported)`) instead of "gate cannot read mxbuild's error file"; `mxtk_mxbuild_why` appends the compiler's `error:` line from `details`, so exec.sh's "mxbuild says" line gains it too. The Build toolchain section WARNs when the Java major does not match a Mendix 11 model (11.14+ → JDK 25, ≤11.13 → JDK 21) and, when `JAVA_HOME` is exported in the shell (which beats both the per-model rule and toolkit.env), names the fix (#576). A `MXTK_JAVA_HOME` override that beats an environment `JAVA_HOME` is not in this change. Cases 12–13 in `tests/wave2/test-doctor-gate-selftest.sh`. Field report, macOS, Studio Pro 11.15.0, mxcli v0.28.0 — MendixMau - fix(doctor): **the gate self-test no longer copies Studio Pro's `.mpr.lock` into its scratch copy**, so with the project open in Studio Pro the known-bad injection is no longer refused and the self-test proves both halves instead of reporting NOT RUN (#577). Case 11 in `tests/wave2/test-doctor-gate-selftest.sh`. Field report, macOS, Studio Pro 11.15.0, mxcli v0.28.0 — MendixMau diff --git a/bin/status.sh b/bin/status.sh index 06fad8b8..c74dd42c 100755 --- a/bin/status.sh +++ b/bin/status.sh @@ -281,12 +281,34 @@ case "$STAGE_LINE" in *"Stage 5"*|*"Stage 6"*) fi ;; esac +# --- lint debt versioned? ------------------------------------------------------------------- +# The lint ratchet, the acceptance log (who accepted which rise, security tier included) and +# project-local rules live under .claude/. A project .gitignore with `/.claude/` drops all three +# from git, so a fresh clone takes whatever it sees as a new baseline and the acceptances are +# lost, with no other check noticing (#523). Re-including needs `/.claude/*` plus negations — +# git cannot re-include a file whose parent directory is excluded. +LINT_UNVERSIONED=""; _tab="$(printf '\t')" +if git -C "$PROJECT_DIR" rev-parse --git-dir >/dev/null 2>&1; then + for _lf in .claude/lint-baseline.json .claude/lint-acceptances.tsv .claude/lint-rules; do + [ -e "$PROJECT_DIR/$_lf" ] || continue + _probe="$_lf"; [ -d "$PROJECT_DIR/$_lf" ] && _probe="$(cd "$PROJECT_DIR" && ls "$_lf"/*.star 2>/dev/null | head -1)" + [ -n "$_probe" ] || continue + # -q decides (a negated `!` match is NOT ignored); -v only names the line, and prints + # negations too. + git -C "$PROJECT_DIR" check-ignore -q -- "$_probe" 2>/dev/null || continue + _ig="$(git -C "$PROJECT_DIR" check-ignore -v -- "$_probe" 2>/dev/null | head -1)" || _ig="" + _src="${_ig%%"$_tab"*}"; _gf="${_src%%:*}"; _src="${_src#*:}"; _gl="${_src%%:*}"; _gp="${_src#*:}" + LINT_UNVERSIONED="${LINT_UNVERSIONED:+$LINT_UNVERSIONED, }$_lf (${_gf}:${_gl} '${_gp}')" + done + [ -n "$LINT_UNVERSIONED" ] && LINT_UNVERSIONED="lint debt is not versioned: git-ignored $LINT_UNVERSIONED — use '/.claude/*' plus '!/.claude/' lines" +fi + # --- print ---------------------------------------------------------------------------------- TK="toolkit $TK_COMMIT_NOW"; [ -n "$TK_COMMIT_REG" ] && [ "$TK_COMMIT_REG" != "$TK_COMMIT_NOW" ] && TK="$TK (register says $TK_COMMIT_REG)" [ -n "$TK_UPD" ] && TK="$TK · updates: $TK_UPD" WATCH="" for _w in "${APPROVED_OVER:+APPROVED OVER A FAILING GATE: $APPROVED_OVER}" "$DONE_STALL" "${BP_STALE%% (*}" \ - "$( [ "${PH_TOTAL:-0}" -gt 0 ] && [ "$EAC" = 0 ] && echo "agent slots unfilled: $PH_TOTAL in $PH_FILES file(s)" )" "$PACK_GAP"; do + "$( [ "${PH_TOTAL:-0}" -gt 0 ] && [ "$EAC" = 0 ] && echo "agent slots unfilled: $PH_TOTAL in $PH_FILES file(s)" )" "$PACK_GAP" "$LINT_UNVERSIONED"; do [ -n "$_w" ] && WATCH="${WATCH:+$WATCH · }$_w" done if [ "$BRIEF" = 1 ]; then diff --git a/tests/wave2/test-status-lint-unversioned.sh b/tests/wave2/test-status-lint-unversioned.sh new file mode 100644 index 00000000..eecac5ec --- /dev/null +++ b/tests/wave2/test-status-lint-unversioned.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# test-status-lint-unversioned.sh — bin/status.sh WATCHes when the lint debt under .claude/ is +# git-ignored (#523). +# +# Usage: bash tests/wave2/test-status-lint-unversioned.sh bin/status.sh +# +# The defect: a project .gitignore with `/.claude/` keeps lint-baseline.json, lint-acceptances.tsv +# and project-local lint rules out of git; a fresh clone then takes whatever it sees as a new +# baseline and the record of accepted rises is lost, and no toolkit check said so. +# +# T1 `/.claude/` ignores all three -> WATCH names each, with .gitignore: '/.claude/' +# T2 `/.claude/*` + three `!` negations -> no lint-debt WATCH (the recommended layout) +# T3 not a git repo -> no lint-debt WATCH (nothing to version against) +# T4 the full screen carries the same WATCH +# +# GOLDEN INPUT: PROJECT.md is the real bin/init-project.sh scaffold; the check-ignore output is +# whatever this machine's git prints. gate-check.sh is stubbed silent in the toolkit copy. +export MXTK_LEAKGUARD_DENYFILE="${TMPDIR:-/tmp}/mxtk-fixture-denylist.$$" +export MXTK_NO_GUIDE=1 MXTK_SYNC_SKIP_CLONE_CHECK=1 +set -uo pipefail +SUT="${1:?usage: test-status-lint-unversioned.sh /path/to/bin/status.sh}" +case "$SUT" in /*) ;; *) SUT="$PWD/$SUT" ;; esac +SRC="$(cd "$(dirname "$SUT")/.." && pwd)" + +W="$(mktemp -d "${TMPDIR:-/tmp}/statuslu.XXXXXX")" || exit 2 +trap 'rm -rf "$W"; rm -f "$MXTK_LEAKGUARD_DENYFILE"' EXIT +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; } +bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; [ -n "${2:-}" ] && printf '%s\n' "$2" | sed 's/^/ FAIL-detail: /'; } + +mkdir -p "$W/tk" +(cd "$SRC" && tar --exclude=.git --exclude=node_modules -cf - .) | (cd "$W/tk" && tar -xf -) +cp "$SUT" "$W/tk/bin/status.sh" +mkproj() { # mkproj + local d="$W/proj-$1"; mkdir -p "$d"; : > "$d/Fixture.mpr" + "$W/tk/bin/init-project.sh" "$d" >/dev/null 2>&1 + mkdir -p "$d/.claude/lint-rules" + echo '{}' > "$d/.claude/lint-baseline.json" + printf 'ts\trule\n' > "$d/.claude/lint-acceptances.tsv" + echo '# rule' > "$d/.claude/lint-rules/local_rule.star" + if [ "$2" = 1 ]; then git -C "$d" init -q; printf '%b' "$3" > "$d/.gitignore"; fi + echo "$d" +} +printf '#!/usr/bin/env bash\nexit 0\n' > "$W/tk/bin/gate-check.sh" + +echo "== bin/status.sh lint-debt WATCH (#523) ==" +P1="$(mkproj ignored 1 'node_modules/\n/.claude/\n')" +out="$(bash "$W/tk/bin/status.sh" "$P1" --brief 2>&1)" +w="$(grep '^WATCH' <<<"$out")" +case "$w" in + *"lint debt is not versioned"*".claude/lint-baseline.json (.gitignore:2 '/.claude/')"*".claude/lint-acceptances.tsv"*".claude/lint-rules"*) ok "T1 /.claude/ ignored: WATCH names all three with .gitignore:2" ;; + *) bad "T1 want the lint-debt WATCH naming three paths" "$out" ;; +esac + +P2="$(mkproj negated 1 '/.claude/*\n!/.claude/lint-baseline.json\n!/.claude/lint-acceptances.tsv\n!/.claude/lint-rules/\n')" +out="$(bash "$W/tk/bin/status.sh" "$P2" --brief 2>&1)" +if grep -q 'lint debt is not versioned' <<<"$out"; then bad "T2 negated layout must not WATCH" "$out"; else ok "T2 '/.claude/*' plus negations: no lint-debt WATCH"; fi + +P3="$(mkproj nogit 0 '')" +out="$(bash "$W/tk/bin/status.sh" "$P3" --brief 2>&1)" +if grep -q 'lint debt is not versioned' <<<"$out"; then bad "T3 no git repo must not WATCH" "$out"; else ok "T3 not a git repo: no lint-debt WATCH"; fi + +out="$(bash "$W/tk/bin/status.sh" "$P1" 2>&1)" +if grep -q '^WATCH .*lint debt is not versioned' <<<"$out"; then ok "T4 full screen: same WATCH"; else bad "T4 full screen lacks the WATCH" "$out"; fi + +echo "-- $PASS passed, $FAIL failed"; [ "$FAIL" -eq 0 ] From 7f5cf8d36876362c2bdbc172a431c8cee1eba9b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 20:45:12 +0000 Subject: [PATCH 22/57] fix(coverage-preflight): scope a per-module ledger file to the BRDs it names A per-module coverage-ledger.md was measured against every BRD in the project, so BRDs only other modules claim read fully UNCLAIMED. When every pointer in it carries a BRD prefix, measure only the BRDs the prefixes name and list the rest as another module's, as the directory form already does. Slash-pointer and project-wide ledgers are unchanged. Part of #543 (option 1; cross-ledger UNCLAIMED is option 2) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- CHANGELOG.md | 1 + project-bin/coverage-preflight.sh | 43 ++++++++++++++++++- .../test-coverage-preflight-ledger-dir.sh | 17 ++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af2668d..90424228 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(coverage-preflight): **a per-module ledger file (`architecture/modules//coverage-ledger.md` or `-coverage-ledger.md`) whose pointers all carry a BRD prefix is measured only against the BRDs those prefixes name; the rest are listed as another module's, like the directory form** (#543, option 1 of the triage). Before, every BRD in the project was measured against it, so BRDs only other modules claim read fully UNCLAIMED (6 of 8 BRDs, ~8,700 false leaves in the reporter's run). A ledger with any `/slash` pointer, the project-wide ledger, and one whose prefixes name no BRD are measured as before. Leaves of a shared BRD that another module claims still read UNCLAIMED (option 2, not built). Only removes false findings; no verdict goes to FAIL. Fixture `test-coverage-preflight-ledger-dir.sh` cases 7–8 (case 7 fails on master). — AlvinnYeo (#543) - fix(verify-module): **the journeys rung walks the folder form `journeys//*.journey.json` (all files, one runner call, positive control the same) when `journeys/.journey.json` is absent, instead of FAULT "no journeys/.journey.json"** (#542). The single file still wins when both exist; `controls/` is not passed as journeys. A journey whose persona is not `TEST_USER` reads INVALID with the runner's own remedy; per-persona runs are left for a follow-up. Fixture `test-verify-module-446.sh` V10 (fails on master). — AlvinnYeo (#542) - fix(verify-module, page-scope): **the page-scope rung's budget is `VM_TMO_PAGESCOPE` (default 900 s, was a fixed 300), and a timed-out rung's whole process tree is killed, so page-scope's Python child no longer survives the watchdog and writes `page-scope.json` after the design audit read the old one** (#541). The watchdog killed only the PID it started; descendants are now listed and killed (where `pgrep` exists; Git Bash keeps the old single-PID kill). `page-scope.sh` writes to a temp file and renames it. Measured by the reporter: ~90 pages at ~4.8 s each, the audit measured 88 pages while the orphan later wrote 94. Fixture `test-verify-module-446.sh` V9 (fails on master; V3 fails on master too in this container, unrelated). — AlvinnYeo (#541) - fix(gate-check): **build-ready now names a `{{PLACEHOLDER}}` whose description spans lines and is still in an agent stub, as a WARN** (#509). The per-line grep never matched a two-line placeholder, so a stub with mdl-agent's four-line `PROJECT_LOCAL_SOURCES_OF_TRUTH` untouched read "no unfilled placeholders". WARN, not FAIL, so no existing project flips; whether it should block is a maintainer call. Fixture `test-bug03-gates.sh` T18 (on the real templates). — jopterhorst (#509) diff --git a/project-bin/coverage-preflight.sh b/project-bin/coverage-preflight.sh index 8d6e9430..ea8f71a3 100755 --- a/project-bin/coverage-preflight.sh +++ b/project-bin/coverage-preflight.sh @@ -457,10 +457,51 @@ if [ -d "$LEDGER" ]; then RC=2 fi else - for b in $BRDS; do + # A per-MODULE ledger file (architecture/modules//coverage-ledger.md or -coverage-ledger.md) + # was measured against every BRD in the project, so a BRD only other modules claim read fully + # UNCLAIMED — thousands of false leaves on a multi-module project (#543). When every pointer in + # it carries a BRD prefix (F001/...), only the BRDs those prefixes name are measured here; the + # rest are listed as another module's, the same as the directory form. A ledger with any + # /slash pointer applies to every BRD and is measured against all of them, as before; so is the + # project-wide ledger, and so is a per-module one whose prefixes name no BRD (the engine then + # reports them PHANTOM). Leaves of a shared BRD that another module claims still read + # UNCLAIMED here: judging across all ledgers is a separate decision (#543 option 2). + SCOPED=0; MEAS_BRDS="$BRDS"; OTHERS="" + if [ -n "$MODULE" ] && { [ "$LEDGER" = "$LEDGER_CANON" ] || [ "$LEDGER" = "$LEDGER_FLAT" ]; } && [ "$NBRD" -gt 1 ]; then + PTRS="$(awk ' + function trim(s) { gsub(/[\140]/, "", s); gsub(/^[ \t]+|[ \t]+$/, "", s); return s } + { t = $0; gsub(/^[ \t]+|[ \t]+$/, "", t) } + t !~ /^\|.*\|$/ { next } + { sub(/^\|/, "", t); split(t, c, "|"); f = trim(c[1]) + if (tolower(f) == "pointer" || f ~ /^:?-+:?$/ || f == "") next + sub(/[ \t]*\([0-9]+\)[ \t]*$/, "", f) + n = split(f, tok, ","); for (i = 1; i <= n; i++) print trim(tok[i]) }' "$LEDGER")" + if [ -n "$PTRS" ] && ! printf '%s\n' "$PTRS" | grep -q '^/'; then + PREFIXES=" $(printf '%s\n' "$PTRS" | sed 's|/.*||' | sort -u | tr '\n' ' ') " + _meas="" + for b in $BRDS; do + bb="$(basename "$b" .brd.json)"; hit=0 + for id in "$bb" "${bb%%-*}" $(jq -r '.id? // empty | strings' "$b" 2>/dev/null || true); do + case "$PREFIXES" in *" $id "*) hit=1; break ;; esac + done + if [ "$hit" -eq 1 ]; then _meas="$_meas $b"; else OTHERS="$OTHERS $bb"; fi + done + [ -n "$_meas" ] && { SCOPED=1; MEAS_BRDS="$_meas"; } + fi + fi + [ "$SCOPED" -eq 1 ] || OTHERS="" + NMEAS=0 + for b in $MEAS_BRDS; do + NMEAS=$((NMEAS + 1)) # shellcheck disable=SC2086 "$ENGINE" $ENGINE_ARGS "$b" "$LEDGER" || raise_rc $? done + if [ "$SCOPED" -eq 1 ]; then + NOTHERS=0; [ -n "$OTHERS" ] && NOTHERS=$(printf '%s\n' $OTHERS | grep -c .) + echo "" + echo " module ledger: $NMEAS of $NBRD BRD(s) measured against $LEDGER (the BRDs its pointers name);" + echo " $NOTHERS BRD(s) it does not claim (another module's, not measured):${OTHERS:- none}" + fi fi # --------------------------------------------------------------------------- diff --git a/tests/wave2/test-coverage-preflight-ledger-dir.sh b/tests/wave2/test-coverage-preflight-ledger-dir.sh index ab215b3a..9538cf96 100755 --- a/tests/wave2/test-coverage-preflight-ledger-dir.sh +++ b/tests/wave2/test-coverage-preflight-ledger-dir.sh @@ -114,6 +114,23 @@ rc=$(run "$P5" Orders) [ "$rc" -eq 0 ] && grep -q 'ledger: architecture/modules/Orders/coverage-ledger.md' "$WORK/out" \ && ok "empty directory falls through to the file" || bad "empty directory shadowed the file" "rc=$rc" +# --- 7. a per-module FILE with BRD-prefixed pointers measures only the BRDs it names (#543) --- +P7="$WORK/p7"; mk_project "$P7" +ledger "$P7/architecture/modules/Orders/coverage-ledger.md" +sed -i.bak -e 's|`/|`FA01/|' "$P7/architecture/modules/Orders/coverage-ledger.md" && rm -f "$P7/architecture/modules/Orders/coverage-ledger.md.bak" +rc=$(run "$P7" Orders) +[ "$rc" -eq 0 ] && ok "prefixed module ledger: clean (rc 0), FB02 not read as UNCLAIMED" \ + || bad "prefixed module ledger measured against another module's BRD (#543)" "rc=$rc $(tr '\n' ' ' < "$WORK/out" | cut -c1-300)" +grep -q '1 of 2 BRD(s) measured' "$WORK/out" && grep -q "does not claim.*FB02-beta" "$WORK/out" \ + && ok "denominator stated, the unclaimed BRD named as another module's" || bad "no module-ledger denominator" + +# --- 8. a /slash module ledger still applies to every BRD (unchanged) ------------------------ +P8="$WORK/p8"; mk_project "$P8" +ledger "$P8/architecture/modules/Orders/coverage-ledger.md" +rc=$(run "$P8" Orders) +[ "$(grep -c 'brd.json' "$WORK/out")" -ge 2 ] && ! grep -q 'does not claim' "$WORK/out" \ + && ok "slash-pointer module ledger is measured against both BRDs, as before (rc $rc)" || bad "slash ledger scoped" "$(tr '\n' ' ' < "$WORK/out" | cut -c1-300)" + echo "" echo "PASS=$PASS FAIL=$FAIL" [ "$FAIL" -eq 0 ] From cd2e85edb718ae6b8fcf8961ee076b27e135f949 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 20:46:51 +0000 Subject: [PATCH 23/57] new(journeys): browser Back action and opt-in ui.no5xx row (#564) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the reporter's ~36-line patch. `{ "do": "back" }` calls goBack (networkidle timeout is a WARN, not a failure) and throws when Back left the app (backLeftApp: login page, about:blank, another origin). `"ui": { "no5xx": true }` collects every response >= 500 from the step's first action to its landing guard and records one row before any bail-out (action failed / not reached); seal() removes the listener. Opt-in, so existing journeys are unchanged. validate-journeys.js accepts `back` and checks no5xx is a boolean; journey-examples.md documents both. Field evidence (reporter's project, Mendix 11.14): Back journey 60 PASS / 2 FAIL on the defective build, 62/62 after the fix. Fixture: tests/wave2/test-journey-back-no5xx.sh 10/10 (original 0/4); validate-journeys --selftest 25/25; journey fixtures, selftest and rung4-scope pass. Not done: making no5xx the default and a positive-control mutant for it (the issue's asks 2-3) — they change results for existing journeys. Fixes #564 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr --- CHANGELOG.md | 1 + examples/validate-journeys.js | 7 ++- project-tests/e2e/journey-runner.js | 50 ++++++++++++++- skills/journey-examples.md | 2 + tests/wave2/test-journey-back-no5xx.sh | 87 ++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 2 deletions(-) create mode 100755 tests/wave2/test-journey-back-no5xx.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ce060081..103abc0a 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 +- new(journeys): **a journey can press the browser's Back button (`{ "do": "back" }`) and opt a step into a 5xx check (`"ui": { "no5xx": true }`)** (#564). Back is a path no in-app click takes (the Mendix client rebuilds the page and re-sends its kept client state), so a defect there could not be pinned by a journey; and a page whose data sources all failed (HTTP 560 on `/xas/`) still drew its frame, so `ui.ready` read PASS. `back` throws when it leaves the app (login page, another origin), not on a `networkidle` timeout; `no5xx` records `no HTTP 5xx during the step` from the first action to the landing guard, before any bail-out, and is opt-in, so no existing journey changes result. `validate-journeys.js` accepts `back` and checks `ui.no5xx` is a boolean; `journey-examples.md` documents both. Ported from the reporter's local patch, which took a Back journey from 60 PASS / 2 FAIL (both the no5xx rows) on the defective build to 62/62 after the app fix. Fixture `tests/wave2/test-journey-back-no5xx.sh` (10 cases); the listener's response API checked in Chromium against a server answering 560. — AlvinnYeo (#564) - fix(e2e): **the journey runner's `click` / `fill` / `combobox` actions and `helpers.js` `gotoTarget()` now wait for their element to become visible (new `helpers.visibleWithin(locator, ms)`), instead of `isVisible({ timeout })`, which Playwright ignores and answers at once** (#536, the sites #547 left). An element attached but still fading in, or inside a popup still opening, threw "not visible" in 0 ms and failed the step. No check gets stricter: a step that passed still passes; one that failed only because the element was a moment late now passes, and a truly missing element fails after its 6 s instead of at once. The instant probes elsewhere (monkey oracle, walkthrough, sign-in) are left as they are. Fixture `tests/wave2/test-e2e-visible-wait.sh` (stand-in locator, plus the issue's reproduction in Chromium where Playwright has a browser). — AlvinnYeo (#536) - fix(journeys): **`--positive-control` no longer reports a mutant that never reached its check as "BROKEN PRECONDITION WENT GREEN": when nothing failed but a row was INVALID (a seed the previous mutant consumed), the control row reads `INVALID — mutant not run: ""`, and a journey that declares `consumes` gets a WARN up front** (#560). Mutants share one data set, so for a journey whose action consumes its seed state every mutant after the first that passed the action stopped at `seeds resolved = INVALID`, and three rungs read FAIL "went green" (7 expected, 2 proven) on a journey that was fine. The exit code and the proven count are unchanged (a control passes only when every row is PASS). A per-mutant reset hook and `--only-rung` (the issue's fixes 2–3) are not in this change. Fixture `test-journey-control-exit.sh` (+5 cases, 12/12). — AlvinnYeo (#560) - fix(journeys): **a named control tagged in `outcome.label` can now be PROVEN: the outcome check is named ` end state: