From ce2bda27c52258d29d75087bb0b1d98e550a5ead Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:05:08 +0000 Subject: [PATCH 01/12] =?UTF-8?q?fix(conformance):=20read=20"(0=20?= =?UTF-8?q?=E2=80=A6)"=20and=20"not=20found"=20only=20where=20mxcli=20prin?= =?UTF-8?q?ts=20them=20(#470)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit probe() matched both absence markers anywhere in mxcli's answer, so a built microflow whose documentation said "(0 = …)" and every navigation describe with a not-found page scored ABSENT and went STALE. An error is now a line starting "Error:", an empty set is a last non-blank line of the "(0 …)" summary form; CRLF stripped. New fixture replays verbatim captures already in the repo on both layouts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + project-bin/conformance-check.sh | 21 ++- tests/wave2/test-conformance-absent-marker.sh | 137 ++++++++++++++++++ 3 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 tests/wave2/test-conformance-absent-marker.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index edafc87e..2cf0111c 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(conformance): **a definition that merely contains "(0 " or "not found" is no longer scored ABSENT** (#470). `conformance-check.sh` matched both markers anywhere in mxcli's answer, so a built microflow whose documentation read "HttpStatus (0 = no response or time-out)" went STALE (five false STALEs in one module), and so does every `describe navigation` with a not-found page, which mxcli prints as " not found page ". It now reads an error only on a line starting `Error:`, and an empty set only when the last non-blank line is the `(0 …)` summary (also `(0 workflow group(s))`); CRLF output reads the same. New fixture `tests/wave2/test-conformance-absent-marker.sh` replays two verbatim captures already in the repo (the v0.25.0 navigation describe of mxcli's public test app, and a v0.23.0 microflow describe with the issue's doc sentence spliced in), both layouts; 6 FAIL on master, 18/18 with the fix. — jopterhorst ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/project-bin/conformance-check.sh b/project-bin/conformance-check.sh index 531bbb94..8aed3e91 100755 --- a/project-bin/conformance-check.sh +++ b/project-bin/conformance-check.sh @@ -360,10 +360,23 @@ probe() { esac if [ $rc -ne 0 ]; then echo "ABSENT"; return; fi if [ -z "$(printf '%s' "$out" | tr -d '[:space:]')" ]; then echo "ABSENT"; return; fi - case "$out" in - Error:*|*"not found"*) echo "ABSENT"; return ;; - *"(0 "*) echo "ABSENT"; return ;; # empty result set, e.g. "(0 entities)" - esac + # Read the two absence markers only where mxcli prints them, never in the definition body. + # A substring match read a DESCRIBE whose documentation said "HttpStatus (0 = no response or + # time-out)" as an empty set, and the verbatim v0.25.0 `describe navigation` of mxcli's public + # test app carries " not found page Pages.Vehicle_Overview" (a navigation setting): both + # scored ABSENT, so built rows went STALE (#470). mxcli prints an error as a line starting + # `Error:` (stderr is merged above), and a table's summary as its own last line, + # `fmt.Fprintf("\n%s\n", Summary)` with Summary = "(%d entities)", "(%d workflow group(s))", … + # A body line never starts at column 0 with either: documentation is " * …", statements are + # indented. CR stripped for a Windows-built mxcli. Here-strings, not `printf | grep -q`: + # under pipefail that pipe can lose to SIGPIPE on a long definition (test-pipefail-grep-q.sh). + local last + out="$(tr -d '\r' <<<"$out")" + if grep -q '^Error:' <<<"$out"; then echo "ABSENT"; return; fi + last="$(sed '/^[[:space:]]*$/d' <<<"$out" | tail -n 1)" + if grep -Eq '^\(0 .*\)[[:space:]]*$' <<<"$last"; then + echo "ABSENT"; return # empty result set, e.g. "(0 entities)" + fi echo "PRESENT" } diff --git a/tests/wave2/test-conformance-absent-marker.sh b/tests/wave2/test-conformance-absent-marker.sh new file mode 100644 index 00000000..0b1c42b2 --- /dev/null +++ b/tests/wave2/test-conformance-absent-marker.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# Usage: bash test-conformance-absent-marker.sh [path-to-conformance-check.sh] +# +# Fixture for project-bin/conformance-check.sh's probe(): where it looks for the two "absent" +# markers in mxcli's answer (issue #470). +# +# The defect: any "(0 " or "not found" ANYWHERE in the output scored the element ABSENT, so a +# built row went STALE when the definition merely contained those characters. The issue's field +# case: a microflow whose documentation reads "HttpStatus (0 = no response or time-out)" — five +# false STALEs in one module. The same substring match hits every `describe navigation` with a +# not-found page set, because mxcli prints that setting as " not found page ". +# +# Golden input. No mxcli binary was available when this fixture was written, so it replays +# captures already in this repo, verbatim: +# - fixtures/design-audit-nav/describe-navigation-responsive-v025.txt — `describe navigation +# Responsive`, mxcli v0.25.0 on mxcli's public test app testapp-views/TestApp.mpr (carries +# " not found page Pages.Vehicle_Overview"); replayed without its four "-- GOLDEN" header lines; +# - fixtures/context-pack/capture/describe/MICROFLOW__Sales.ACT_ApprovalStep_Approve.out — +# `DESCRIBE MICROFLOW`, mxcli v0.23.0. The issue's doc text is quoted verbatim from #470 and +# spliced into that capture's documentation line (the only edit), since #470 quotes the +# sentence, not the whole definition. +# The empty-set reply follows mxcli's own printer (mdl/executor/format.go: the summary is +# printed as "\n%s\n" after the table, Summary = "(%d entities)", "(%d workflow group(s))", …), +# not a capture — re-capture it from a real `SHOW … IN ` when a binary is at hand. +# +# Cases, each on BOTH layouts (single-tree, and two-tree with the .mpr under app/): +# 1. a built microflow whose documentation says "(0 = …)" is PRESENT (was ABSENT -> STALE) +# 2. a built navigation profile with a not-found page is PRESENT (was ABSENT -> STALE) +# 3. the unedited microflow capture stays PRESENT +# 4. an empty result set, "(0 entities)" as the last line, stays ABSENT +# 5. an empty set whose noun has parentheses, "(0 workflow group(s))", stays ABSENT +# 6. a missing element ("Error: … not found", rc 1) stays ABSENT +# 7. an "Error:" line with rc 0 is still ABSENT +# 8. CRLF output (a Windows-built mxcli) reads the same: the doc'd microflow PRESENT, the +# empty set ABSENT + +set -uo pipefail + +SUT="${1:-}" +[ -z "$SUT" ] && SUT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/project-bin/conformance-check.sh" +if [ ! -f "$SUT" ]; then + echo "SKIP: subject not found at $SUT" + echo "SCORE: 0/0 — nothing to test" + exit 0 +fi +SUT="$(cd "$(dirname "$SUT")" && pwd)/$(basename "$SUT")" +[ -f "$(dirname "$SUT")/_common.sh" ] || { echo "FAIL — _common.sh not beside $SUT"; exit 1; } +FIX="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/fixtures" +NAV="$FIX/design-audit-nav/describe-navigation-responsive-v025.txt" +MF="$FIX/context-pack/capture/describe/MICROFLOW__Sales.ACT_ApprovalStep_Approve.out" +for f in "$NAV" "$MF"; do [ -f "$f" ] || { echo "FAIL — golden capture missing: $f"; exit 1; }; done + +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); echo " ok — $1"; } +bad() { FAIL=$((FAIL+1)); echo " FAIL — $1"; [ -n "${2:-}" ] && echo " got: $2"; } + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/conf-absent.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT + +setup() { # setup -> sets P + P="$WORK/$1-$2" + rm -rf "$P"; mkdir -p "$P/architecture/modules/Sales" "$P/cap" + if [ "$1" = two ]; then mkdir -p "$P/app"; : > "$P/app/App.mpr"; else : > "$P/App.mpr"; fi + grep -v '^-- GOLDEN\|^-- built from\|^-- testdata/\|^-- the same text' "$NAV" > "$P/cap/nav.out" + cp "$MF" "$P/cap/mf.out" + # The one edit: the issue's verbatim documentation text in place of the capture's. + sed 's#^ \* Page button: approve the current step with the comment typed on it\.$# * Page button: approve. HttpStatus (0 = no response or time-out).#' \ + "$MF" > "$P/cap/mfdoc.out" + printf '| Name |\n|------|\n\n(0 entities)\n' > "$P/cap/empty.out" + printf '| Name |\n|------|\n\n(0 workflow group(s))\n' > "$P/cap/emptyparen.out" + if [ "$2" = crlf ]; then + for f in "$P"/cap/*.out; do sed 's/$/\r/' "$f" > "$f.tmp" && mv "$f.tmp" "$f"; done + fi + cat > "$P/mxcli" <<'EOF' +#!/usr/bin/env bash +cmd=""; while [ $# -gt 0 ]; do [ "$1" = -c ] && cmd="$2"; shift; done +c="$(dirname "$0")/cap" +case "$cmd" in + "DESCRIBE NAVIGATION Responsive") cat "$c/nav.out" ;; + "DESCRIBE MICROFLOW Sales.ACT_Plain") cat "$c/mf.out" ;; + "DESCRIBE MICROFLOW Sales.ACT_Doc") cat "$c/mfdoc.out" ;; + "SHOW ENTITIES IN Empty") cat "$c/empty.out" ;; + "SHOW WORKFLOWS IN Empty") cat "$c/emptyparen.out" ;; + "DESCRIBE MICROFLOW Sales.ACT_Rc0") echo "Error: microflow not found: Sales.ACT_Rc0" ;; + *) echo "Error: microflow not found: ${cmd##* }" >&2; exit 1 ;; +esac +EOF + chmod +x "$P/mxcli" + cat > "$P/architecture/modules/Sales/coverage-ledger.md" <<'EOF' +| Pointer | Kind | Name | Row | Proof | Acceptance | Status | +|---|---|---|---|---|---|---| +| `/microflows/1` | microflow | ACT_Doc | 1.1 | exec.sh | `DESCRIBE MICROFLOW Sales.ACT_Doc` | built | +| `/navigation/0` | navigation | Responsive | 1.2 | exec.sh | `DESCRIBE NAVIGATION Responsive` | built | +| `/microflows/0` | microflow | ACT_Plain | 1.3 | exec.sh | `DESCRIBE MICROFLOW Sales.ACT_Plain` | built | +| `/entities/0` | entity | Nothing | 1.4 | exec.sh | `SHOW ENTITIES IN Empty` | not-built | +| `/workflows/0` | workflow | Nothing | 1.5 | exec.sh | `SHOW WORKFLOWS IN Empty` | not-built | +| `/microflows/2` | microflow | ACT_Gone | 1.6 | exec.sh | `DESCRIBE MICROFLOW Sales.ACT_Gone` | not-built | +| `/microflows/3` | microflow | ACT_Rc0 | 1.7 | exec.sh | `DESCRIBE MICROFLOW Sales.ACT_Rc0` | not-built | +EOF +} + +observed() { # observed -> the observed column of today's report + P_PTR="\`$1\`" awk -F'\t' '$2==ENVIRON["P_PTR"] {print $4}' "$P"/docs/conformance/report-*.tsv 2>/dev/null | head -1 +} + +for layout in single two; do + for eol in lf crlf; do + setup "$layout" "$eol" + ( cd "$P" && PROJECT_ROOT="$P" bash "$SUT" --quiet ) > "$WORK/out" 2>&1; rc=$? + tag="[$layout/$eol]" + if [ "$eol" = lf ]; then + [ "$rc" = 0 ] && ok "$tag run exits 0" || bad "$tag run exit $rc" "$(tr '\n' ' ' < "$WORK/out" | cut -c1-300)" + [ "$(observed /microflows/1)" = PRESENT ] && ok "$tag 1. doc with '(0 = …)' is PRESENT" \ + || bad "$tag 1. doc with '(0 = …)' read as $(observed /microflows/1) (the #470 false STALE)" + [ "$(observed /navigation/0)" = PRESENT ] && ok "$tag 2. navigation with a not-found page is PRESENT" \ + || bad "$tag 2. navigation read as $(observed /navigation/0)" + [ "$(observed /microflows/0)" = PRESENT ] && ok "$tag 3. unedited capture PRESENT" \ + || bad "$tag 3. unedited capture read as $(observed /microflows/0)" + [ "$(observed /entities/0)" = ABSENT ] && ok "$tag 4. '(0 entities)' is ABSENT" \ + || bad "$tag 4. '(0 entities)' read as $(observed /entities/0)" + [ "$(observed /workflows/0)" = ABSENT ] && ok "$tag 5. '(0 workflow group(s))' is ABSENT" \ + || bad "$tag 5. '(0 workflow group(s))' read as $(observed /workflows/0)" + [ "$(observed /microflows/2)" = ABSENT ] && ok "$tag 6. Error + rc 1 is ABSENT" \ + || bad "$tag 6. missing element read as $(observed /microflows/2)" + [ "$(observed /microflows/3)" = ABSENT ] && ok "$tag 7. Error line with rc 0 is ABSENT" \ + || bad "$tag 7. rc-0 Error read as $(observed /microflows/3)" + else + [ "$(observed /microflows/1)" = PRESENT ] && [ "$(observed /entities/0)" = ABSENT ] \ + && ok "$tag 8. CRLF: doc'd microflow PRESENT, empty set ABSENT" \ + || bad "$tag 8. CRLF misread" "mf=$(observed /microflows/1) empty=$(observed /entities/0)" + fi + done +done + +echo "" +echo "PASS=$PASS FAIL=$FAIL" +[ "$FAIL" -eq 0 ] From 8427edcdb5de534ac11bc95d6c5e78ee90e2572a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:07:25 +0000 Subject: [PATCH 02/12] fix(monkey): recognise non-English sign-out controls, configurable per project (#469) SIGN_OUT matched English captions only, so a Dutch app's "Afmelden" was clicked, the monkey landed on login.html and scored a false CRASH. The default now covers nl/de/fr/es/pt/it captions, a project adds its own via project.config.js signOutPattern or SIGN_OUT_PATTERN, and rapidTab steps past a focused sign-out control instead of pressing Enter on it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + project-tests/e2e/config.js | 3 ++ project-tests/e2e/monkey.js | 44 ++++++++++++++++++-- project-tests/e2e/project.config.template.js | 5 +++ tests/wave2/test-monkey-left-app.sh | 27 ++++++++++++ 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edafc87e..4ac533ca 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(monkey): **the monkey no longer signs itself out of a non-English app** (#469). `monkey.js` recognised only English sign-out captions, so in a Dutch app it clicked "Afmelden", landed on `login.html`, scored a false CRASH and fuzzed nothing more (seed 204262061: 3 observations, 1 crash-class; 24 and 0 with the label excluded). The default now also covers Dutch, German, French, Spanish, Portuguese and Italian sign-out captions (sign-in captions such as "Aanmelden" stay fuzzable); a project adds its own in `project.config.js` `signOutPattern` or `SIGN_OUT_PATTERN`, added to the default, never replacing it. `rapidTab` no longer presses Enter while focus is on a sign-out control (top bar, nav item, aria-label or title), the route by which a layout control outside `.mx-page` was reached. `test-monkey-left-app.sh` gains the issue's captions, sign-in near-misses, the project pattern and the rapidTab guard: 5 FAIL on master, 18/18 with the fix. — jopterhorst ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/project-tests/e2e/config.js b/project-tests/e2e/config.js index d0c72b1b..1ff9a4a5 100644 --- a/project-tests/e2e/config.js +++ b/project-tests/e2e/config.js @@ -206,6 +206,9 @@ const cfg = { // this app has no nav tree, so '.mx-navigationtree, .mx-page' alone is not // enough to prove it routed). readySelectors: PROJ.readySelectors || ['.mx-navigationtree', '.mx-page'], + // Extra sign-out captions for monkey.js, ADDED to its multilingual default (#469): a RegExp + // or regex string from project.config.js `signOutPattern`, or SIGN_OUT_PATTERN in the env. + signOutPattern: process.env.SIGN_OUT_PATTERN || PROJ.signOutPattern || null, // This project runs natively under Studio Pro (no Docker), so `mxcli oql -p` // alone would try to route through `docker compose exec` and fail. --direct // connects straight to the M2EE admin API on the host instead. diff --git a/project-tests/e2e/monkey.js b/project-tests/e2e/monkey.js index a3dd128c..65c1dd7e 100644 --- a/project-tests/e2e/monkey.js +++ b/project-tests/e2e/monkey.js @@ -153,7 +153,42 @@ async function typeSafely(page, handle, text) { // toggle), so Back left the app — to login.html, then to about:blank. The oracle scored the // non-app page "blank page", every later round on that target scored it again, and the next // target was never reached: 10 crash-class findings, one target unfuzzed, no app defect. -const SIGN_OUT = /\b(sign|log)\s*(out|off)\b/i; +// +// SIGN_OUT IS MULTILINGUAL, AND A PROJECT CAN ADD TO IT (#469). The English-only pattern missed +// a Dutch app's "Afmelden": the monkey signed itself out in round 1, scored login.html as a +// CRASH, and fuzzed nothing more (seed 204262061: 3 observations, 1 crash-class; 24 and 0 once +// the label was excluded). The default covers the common captions of the languages Mendix apps +// ship in most; anything else goes in project.config.js `signOutPattern` (a RegExp or a regex +// string) or the SIGN_OUT_PATTERN env var, and is ADDED to the default, never a replacement. +const SIGN_OUT_DEFAULT = new RegExp('\\b(?:' + [ + '(?:sign|log)\\s*(?:out|off)', // en: Sign out, Log off, Logout + 'afmelden', 'uitloggen', // nl + 'abmelden', 'ausloggen', // de + 'se\\s+d[ée]connecter', 'd[ée]connexion', // fr + 'cerrar\\s+sesi[óo]n', // es + '(?:terminar|encerrar)\\s+sess[ãa]o', // pt + 'disconnetti', // it +].join('|') + ')\\b', 'i'); +function signOutPattern(extra) { + if (!extra) return SIGN_OUT_DEFAULT; + const src = extra instanceof RegExp ? extra.source : String(extra); + try { return new RegExp(`(?:${SIGN_OUT_DEFAULT.source})|(?:${src})`, 'i'); } + catch (e) { + console.log(` ⚠ signOutPattern ${JSON.stringify(src)} is not a valid regex (${e.message}) — using the built-in sign-out captions only`); + return SIGN_OUT_DEFAULT; + } +} +const SIGN_OUT = signOutPattern(cfg.signOutPattern); +// The keyboard can reach controls the button locator never offers: the layout's top bar, a +// navigation item, an icon-only button whose caption is its aria-label or title. +async function focusIsSignOut(page) { + const label = await page.evaluate(() => { + const e = document.activeElement; + if (!e || e === document.body) return ''; + return [e.innerText || e.textContent || '', e.getAttribute('aria-label') || '', e.getAttribute('title') || ''].join(' | '); + }).catch(() => ''); + return SIGN_OUT.test(label); +} function leftApp(url, baseUrl) { const base = String(baseUrl).replace(/\/+$/, ''); if (!url || (url !== base && !url.startsWith(base + '/'))) return true; @@ -208,7 +243,10 @@ async function round(page, target, bucket) { } } else if (abuse === 'rapidTab') { for (let i = 0; i < 15; i++) await page.keyboard.press('Tab').catch(() => {}); - await page.keyboard.press('Enter').catch(() => {}); + // Never press Enter on a sign-out control (#469): step past it, a few Tabs at most. + for (let k = 0; k < 5 && await focusIsSignOut(page); k++) await page.keyboard.press('Tab').catch(() => {}); + if (await focusIsSignOut(page)) what += ' (focus stayed on a sign-out control: Enter skipped)'; + else await page.keyboard.press('Enter').catch(() => {}); } else if (abuse === 'reloadMidFlow') { if (btn) btn.click({ force: true, timeout: 4000 }).catch(() => {}); await page.waitForTimeout(300); @@ -224,7 +262,7 @@ async function round(page, target, bucket) { } // leftApp is exported for tests/wave2/test-monkey-left-app.sh; requiring this file runs nothing. -module.exports = { leftApp, SIGN_OUT }; +module.exports = { leftApp, SIGN_OUT, SIGN_OUT_DEFAULT, signOutPattern }; // ── main ──────────────────────────────────────────────────────────────────── if (require.main === module) (async () => { diff --git a/project-tests/e2e/project.config.template.js b/project-tests/e2e/project.config.template.js index 790e23c2..0bb35334 100644 --- a/project-tests/e2e/project.config.template.js +++ b/project-tests/e2e/project.config.template.js @@ -586,6 +586,11 @@ module.exports = { mxcli: process.env.MXCLI || './mxcli', credentials, readySelectors: READY_SELECTORS, + // Sign-out captions monkey.js must never click, ADDED to its built-in list (English, Dutch, + // German, French, Spanish, Portuguese, Italian). Set it when the app's sign-out control + // reads otherwise, e.g. /\bkirjaudu ulos\b/i — a monkey that signs itself out scores the + // login page as a crash and fuzzes nothing more (#469). + signOutPattern: null, dsCss: DS_CSS, NAV, TARGETS, diff --git a/tests/wave2/test-monkey-left-app.sh b/tests/wave2/test-monkey-left-app.sh index 654260e8..37fa1645 100755 --- a/tests/wave2/test-monkey-left-app.sh +++ b/tests/wave2/test-monkey-left-app.sh @@ -64,6 +64,14 @@ L('OTHER', 'http://localhost:80801/p/cases'); L('SLASHBASE', 'http://localhost:8080/p/cases', base + '/'); const S = t => console.log(`S:${t} ${M.SIGN_OUT.test(t)}`); ['Sign out', 'Log off', 'Logout', 'Switch theme', 'Signature', 'Log in'].forEach(S); +// #469: the captions the issue names, verbatim, and near-misses that must stay fuzzable. +['Afmelden', 'Uitloggen', 'Abmelden', 'Se déconnecter', 'Aanmelden', 'Inloggen', 'Anmelden', 'Se connecter', 'Melden'].forEach(S); +const X = (k, extra, t) => console.log(`X:${k} ${typeof M.signOutPattern === 'function' ? M.signOutPattern(extra).test(t) : 'NOFN'}`); +X('cfgre', /\bkirjaudu ulos\b/i, 'Kirjaudu ulos'); +X('cfgstr', '\\bkirjaudu ulos\\b', 'Kirjaudu ulos'); +X('cfgkeeps', /\bkirjaudu ulos\b/i, 'Afmelden'); +X('cfgbad', '(', 'Afmelden'); +X('cfgnone', null, 'Kirjaudu ulos'); process.exit(0); EOF OUT="$(cd "$WORK/tests/e2e" && printf '%s' "$JS" | APP_PORT=1 timeout 30 node - 2>&1)" @@ -91,6 +99,25 @@ s() { printf '%s\n' "$OUT" | sed -n "s/^S:$1 //p"; } [ "$(s 'Switch theme')" = false ] && [ "$(s 'Signature')" = false ] && [ "$(s 'Log in')" = false ] \ && ok "SIGN_OUT leaves Switch theme (captured), Signature, Log in fuzzable" || bad "SIGN_OUT over-matches" "$(s 'Switch theme')/$(s 'Signature')/$(s 'Log in')" +# #469 — non-English sign-out captions (verbatim from the issue's field run and its list). +[ "$(s 'Afmelden')" = true ] && [ "$(s 'Uitloggen')" = true ] && [ "$(s 'Abmelden')" = true ] && [ "$(s 'Se déconnecter')" = true ] \ + && ok "SIGN_OUT matches Afmelden (captured), Uitloggen, Abmelden, Se déconnecter" \ + || bad "a non-English sign-out label is clicked — the #469 false CRASH" "$(s 'Afmelden')/$(s 'Uitloggen')/$(s 'Abmelden')/$(s 'Se déconnecter')" +[ "$(s 'Aanmelden')" = false ] && [ "$(s 'Inloggen')" = false ] && [ "$(s 'Anmelden')" = false ] && [ "$(s 'Se connecter')" = false ] && [ "$(s 'Melden')" = false ] \ + && ok "SIGN_OUT leaves sign-IN and look-alike captions fuzzable (Aanmelden, Inloggen, Anmelden, Se connecter, Melden)" \ + || bad "SIGN_OUT over-matches a sign-in caption" "$(s 'Aanmelden')/$(s 'Inloggen')/$(s 'Anmelden')/$(s 'Se connecter')/$(s 'Melden')" +x() { printf '%s\n' "$OUT" | sed -n "s/^X:$1 //p"; } +[ "$(x cfgre)" = true ] && [ "$(x cfgstr)" = true ] && ok "a project signOutPattern (RegExp or string) is honoured" \ + || bad "project signOutPattern ignored" "$(x cfgre)/$(x cfgstr)" +[ "$(x cfgkeeps)" = true ] && ok "a project pattern adds to the default, it does not replace it" || bad "project pattern dropped the default" "$(x cfgkeeps)" +[ "$(x cfgbad)" = true ] && [ "$(x cfgnone)" = false ] && ok "an invalid project pattern falls back to the default; none means default only" \ + || bad "invalid/absent project pattern mishandled" "$(x cfgbad)/$(x cfgnone)" +grep -q 'signOutPattern: *process.env.SIGN_OUT_PATTERN' "$E2E/config.js" && grep -q '^ *signOutPattern:' "$CFG" \ + && ok "config.js reads signOutPattern (env, then project.config) and the template declares it" \ + || bad "signOutPattern is not plumbed through config.js / the template (a consumer with no producer)" +awk '/rapidTab/ {r=1} r && /focusIsSignOut\(page\)/ {f=1} f && /keyboard\.press\(.Enter.\)/ {e=1} END{exit !e}' "$SUT" \ + && ok "rapidTab checks focus for a sign-out control before Enter" || bad "rapidTab presses Enter without checking what has focus (#469)" + # The tested functions must be the ones the rounds use. awk '/hasNotText: *SIGN_OUT/ {f=1} END{exit !f}' "$SUT" && ok "round()'s button locator filters SIGN_OUT" \ || bad "the button locator does not filter SIGN_OUT — the tested regex is not the one that runs" From f5310f66809d82c5afdff18ec7936a1b0f4cee2d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:07:34 +0000 Subject: [PATCH 03/12] fix(status): brace variables before an em-dash so --brief prints NEXT (#467) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a single-byte locale (and on macOS bash 3.2 even under en_US.UTF-8) bash reads the first byte of a multibyte character as part of a bare variable name, so "$LAST_FAIL— fix" expands LAST_FAIL and set -u aborts status.sh before it prints NEXT. - bin/status.sh: ${LAST_FAIL} and ${NOT_DONE_PASS} on the two NEXT lines. - project-bin/constants-audit.sh, tests/wave2/test-runbook-slice.sh: the same idiom (an ellipsis, and a case pattern), braced. - bin/check-portability.sh: flag any bare $NAME followed by a non-ASCII byte, in every scanned file, matched with LC_ALL=C. - tests/wave2/test-status-bare-var.sh: the lint cases on any machine, and both NEXT rules under an ISO-8859-1 locale where one exists. Reproduced on bash 5.2 with a compiled en_US.ISO-8859-1 locale: both NEXT rules aborted on master and print after the fix. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + bin/check-portability.sh | 15 +++++ bin/status.sh | 4 +- project-bin/constants-audit.sh | 2 +- tests/wave2/test-runbook-slice.sh | 2 +- tests/wave2/test-status-bare-var.sh | 87 +++++++++++++++++++++++++++++ 6 files changed, 107 insertions(+), 4 deletions(-) create mode 100755 tests/wave2/test-status-bare-var.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index edafc87e..4912aa76 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): **`status.sh --brief` no longer aborts with "LAST_FAIL: unbound variable" before printing NEXT (#467).** Two NEXT lines wrote a bare `$LAST_FAIL`/`$NOT_DONE_PASS` straight before an em-dash; in a single-byte locale (and on macOS bash 3.2 even under `en_US.UTF-8`) bash reads the dash's first byte as part of the name and `set -u` kills the run. Braced there and at the two other sites with the same idiom (`project-bin/constants-audit.sh`'s `…`, a case pattern in `test-runbook-slice.sh`). `check-portability.sh` now flags any bare `$NAME` followed by a non-ASCII byte, in every file including the meta-exempt ones (matched with `LC_ALL=C`, so the check does not depend on the locale it guards against; `# portability-ok:` opts out). New `tests/wave2/test-status-bare-var.sh`: the lint cases on any machine, plus both NEXT rules run under an ISO-8859-1 locale where one exists (reproduced on bash 5.2 with a compiled `en_US.ISO-8859-1`: both rules aborted on master, both print after). — jopterhorst ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/bin/check-portability.sh b/bin/check-portability.sh index 444dee0f..7ee1cef2 100755 --- a/bin/check-portability.sh +++ b/bin/check-portability.sh @@ -67,6 +67,21 @@ while IFS= read -r f; do report "$f" 1 "CRLF line endings" "bash fails on line 1. Ensure .gitattributes has *.sh text eol=lf." fi + # 2. A bare $NAME followed directly by a non-ASCII byte (an em-dash, an ellipsis). In a + # single-byte locale bash reads that byte as part of the variable NAME (macOS bash 3.2 did + # it under en_US.UTF-8 too), so set -u aborts with "NAME: unbound variable" (#467: + # status.sh --brief died before printing NEXT). Checked before is_meta: no file is exempt + # from this one. Matched byte-wise (LC_ALL=C) so the result does not depend on the locale + # it is meant to guard against. Fix: brace it, ${NAME}—. + while IFS= read -r hit; do + [ -n "$hit" ] || continue + case "$hit" in *portability-ok:*) continue ;; esac + report "$f" "${hit%%:*}" "bare \$NAME followed by a non-ASCII character" \ + 'Brace the name: ${NAME}. A single-byte locale (and bash 3.2) reads the first byte as part of the name; set -u then aborts.' + done <: unbound variable" (macOS +# bash 3.2 did it under en_US.UTF-8 too). bash 5 in a UTF-8 locale never shows it, which is why +# it shipped. So the runs below use an ISO-8859-1 locale: the first one `locale -a` lists, or a +# compiled one via LOCPATH (localedef -i en_US -f ISO-8859-1 "$LOCPATH/en_US.ISO-8859-1"). +# No such locale: T1/T2 SKIP (stated, not silent) and T3 still runs. +# +# T1 latest BUILD-LOG row a fail -> NEXT "last gate FAILED for: 010-a.mdl — …", no abort +# T2 latest row a pass, not done- -> NEXT "gate-passed but not done-: 010-a.mdl — …", no abort +# T3 static guard, any machine: bin/check-portability.sh (same toolkit) flags a bare name +# before an em-dash and an ellipsis, passes the braced form, a comment line, a positional +# $1 and a `# portability-ok:` line, and the shipped status.sh/constants-audit.sh are clean +# +# GOLDEN INPUT: PROJECT.md is the real bin/init-project.sh scaffold; the BUILD-LOG row shape is +# project-bin/exec.sh log_build()'s. gate-check.sh is stubbed silent in the toolkit copy so the +# NEXT ladder reaches the two rules under test instead of stopping at a scaffold finding. +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-bare-var.sh /path/to/bin/status.sh}" +case "$SUT" in /*) ;; *) SUT="$PWD/$SUT" ;; esac +SRC="$(cd "$(dirname "$SUT")/.." && pwd)" + +W="$(mktemp -d "${TMPDIR:-/tmp}/statusbv.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: /'; } + +echo "== T3 check-portability.sh bare-\$NAME rule (#467) ==" +PORT="$SRC/bin/check-portability.sh" +mkdir -p "$W/lint/bad" "$W/lint/good" +# Non-ASCII written as octal escapes so this fixture file itself stays clean under the rule. +EM="$(printf '\342\200\224')"; EL="$(printf '\342\200\246')" +printf '#!/usr/bin/env bash\necho "failed: $X%s fix"\n' "$EM" > "$W/lint/bad/a.sh" +printf '#!/usr/bin/env bash\ns="$s%s"\n' "$EL" > "$W/lint/bad/b.sh" +printf '#!/usr/bin/env bash\necho "${X}%s ok"\n# $X%s in a comment\necho "$1%s"\necho "$X%s" # portability-ok: test\n' \ + "$EM" "$EM" "$EM" "$EM" > "$W/lint/good/c.sh" +out="$(bash "$PORT" "$W/lint/bad" 2>&1)"; rc=$? +n="$(printf '%s\n' "$out" | grep -c 'bare \$NAME followed by a non-ASCII')" +if [ "$rc" -eq 1 ] && [ "$n" -eq 2 ]; then ok "T3a em-dash and ellipsis after a bare name: 2 of 2 flagged"; else bad "T3a want rc 1, 2 hits; got rc $rc, $n" "$out"; fi +out="$(bash "$PORT" "$W/lint/good" 2>&1)"; rc=$? +if [ "$rc" -eq 0 ]; then ok "T3b braced, comment, \$1 and portability-ok: not flagged"; else bad "T3b want rc 0, got $rc" "$out"; fi +mkdir -p "$W/lint/shipped"; cp "$SUT" "$W/lint/shipped/status.sh" +cp "$SRC/project-bin/constants-audit.sh" "$W/lint/shipped/" +out="$(bash "$PORT" "$W/lint/shipped" 2>&1)" +if grep -q 'bare \$NAME' <<<"$out"; then bad "T3c shipped scripts carry a bare name before non-ASCII" "$out"; else ok "T3c status.sh and constants-audit.sh: 0 bare names before non-ASCII"; fi + +LOC="$(locale -a 2>/dev/null | grep -iE '^en_US\.ISO-?8859-?1$' | head -1)" +[ -z "$LOC" ] && [ -n "${LOCPATH:-}" ] && [ -d "$LOCPATH/en_US.ISO-8859-1" ] && LOC=en_US.ISO-8859-1 +done_() { echo "-- $PASS passed, $FAIL failed"; [ "$FAIL" -eq 0 ]; exit $?; } +if [ -z "$LOC" ]; then echo " SKIP T1-T2: no ISO-8859-1 locale (see header to compile one)"; done_; fi +# The locale must actually reproduce the trap, or a pass below proves nothing. +if LC_ALL="$LOC" bash -uc "X=a; : \"\$X$EM\"" 2>/dev/null; then + echo " SKIP T1-T2: bash under $LOC does not read the byte as a name character here"; done_ +fi + +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/mdlsource" "$d/docs"; : > "$d/Fixture.mpr" + "$W/tk/bin/init-project.sh" "$d" >/dev/null 2>&1 + { echo "| when (ISO-8601 local) | script | gate | result | detail |"; echo "|---|---|---|---|---|"; } > "$d/docs/BUILD-LOG.md" + echo "-- a" > "$d/mdlsource/010-a.mdl" + echo "| 2026-10-13T10:00:00+0200 | \`010-a.mdl\` | $1 | x | 010-a.mdl |" >> "$d/docs/BUILD-LOG.md" + echo "$d" +} +PF="$(mkproj fail)"; PP="$(mkproj pass)" +printf '#!/usr/bin/env bash\nexit 0\n' > "$W/tk/bin/gate-check.sh" + +echo "== bin/status.sh --brief under $LOC (#467) ==" +check() { # check + local out; out="$(LC_ALL="$LOC" bash "$W/tk/bin/status.sh" "$2" --brief 2>&1)" + case "$out" in *"unbound variable"*) bad "$1: aborted" "$out"; return ;; esac + if grep -q "^NEXT $3" <<<"$out"; then ok "$1"; else bad "$1: NEXT not printed" "$out"; fi +} +check "T1 failed row: NEXT names the script, no abort" "$PF" 'last gate FAILED for: 010-a.mdl ' +check "T2 gate-pass, not done-: NEXT names it, no abort" "$PP" 'gate-passed but not done-: 010-a.mdl ' + +done_ From 160d180cfca1429690c2eb03fd23942b22d80bec Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:09:22 +0000 Subject: [PATCH 04/12] fix(page-fidelity): score a popup wireframe's dialog, not the page behind it (#471) contentOf() tried div.main before anything else, and a popup wireframe's .shell-behind carries the caller's div.main, so the popup was scored against the caller's page. A wireframe with a .dialog now scores the dialog; a .popup-stage without one scores the stage minus .shell-behind. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + project-bin/page-fidelity.js | 18 +++++- .../fixtures/page-fidelity-popup/CAPTURE.md | 19 ++++++ .../product-newedit-describe-v023.mdl | 63 +++++++++++++++++++ .../product-newedit-popup.html | 48 ++++++++++++++ tests/wave2/test-page-fidelity-mocks.sh | 19 ++++++ 6 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 tests/wave2/fixtures/page-fidelity-popup/CAPTURE.md create mode 100644 tests/wave2/fixtures/page-fidelity-popup/product-newedit-describe-v023.mdl create mode 100644 tests/wave2/fixtures/page-fidelity-popup/product-newedit-popup.html diff --git a/CHANGELOG.md b/CHANGELOG.md index edafc87e..7622d7a7 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-fidelity): **a popup wireframe is scored against its dialog, not the page drawn behind it** (#471). A popup wireframe draws the caller's page behind a scrim (`.popup-stage > .shell-behind + .dialog-scrim > .dialog`); `page-fidelity.js` looked for `div.main` first and landed in the shell, so a popup whose fields and bindings all matched scored 29% in the field, every miss being the caller's page head, h1 and card. A wireframe with a `.dialog` (class word, so `dialog-scrim` does not count) now scores the dialog only; a `.popup-stage` without one scores the stage minus `.shell-behind`; every other wireframe keeps its boundary. `test-page-fidelity-mocks.sh` gains the popup cases on a verbatim v0.23.0 describe of a real popup page and a wireframe drawn in the issue's shape: 18% and 6 FAIL on master, 100% and 75/75 with the fix. — jopterhorst ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/project-bin/page-fidelity.js b/project-bin/page-fidelity.js index 804d202f..36eab7fc 100755 --- a/project-bin/page-fidelity.js +++ b/project-bin/page-fidelity.js @@ -163,8 +163,22 @@ function contentOf(html) { // div.content same shape when the wireframe has no .main wrapper // popup/dialog wireframes with annotation siblings // then strip annotation chrome either way. - const m = html.match(/]*>([\s\S]*?)<\/main>/i); - let s = m ? m[1] + // + // POPUP FIRST (#471). A popup wireframe draws the CALLER's page behind a scrim for context: + // `.popup-stage > .shell-behind + .dialog-scrim > .dialog`. The shell behind carries its own + // div.main, so the list below landed on the caller's page and scored the popup against it: + // 29% in the field for a popup whose fields and bindings all matched, every miss in the + // shell. When the wireframe draws a `.dialog`, the dialog is the page; the shell is not + // scored at all. Matched on the class WORD, so `dialog-scrim`/`dialog-head` do not count. + const cls = name => new RegExp(`<(div|section|dialog|aside)\\b[^>]*class="(?:[^"]*\\s)?${name}(?:\\s[^"]*)?"`, 'i'); + const popup = innerBalanced(html, cls('dialog')) + || (() => { // a stage with no .dialog: everything but the shell + const stage = innerBalanced(html, cls('popup-stage')); + return stage === null ? null : dropBalanced(stage, cls('shell-behind')); + })(); + const m = popup === null ? html.match(/]*>([\s\S]*?)<\/main>/i) : null; + let s = popup !== null ? popup + : m ? m[1] : innerBalanced(html, /<(div)[^>]*class="[^"]*\bwf-screen\b[^"]*"/i) || innerBalanced(html, /<(div)[^>]*class="[^"]*\bwf-shell\b[^"]*"/i) || innerBalanced(html, /<(div)[^>]*class="[^"]*\bmockup-frame\b[^"]*"/i) diff --git a/tests/wave2/fixtures/page-fidelity-popup/CAPTURE.md b/tests/wave2/fixtures/page-fidelity-popup/CAPTURE.md new file mode 100644 index 00000000..2cae4adc --- /dev/null +++ b/tests/wave2/fixtures/page-fidelity-popup/CAPTURE.md @@ -0,0 +1,19 @@ +# page-fidelity-popup — a popup wireframe and the popup page it draws (#471) + +`product-newedit-describe-v023.mdl` is **verbatim** `mxcli describe page Sales.Product_NewEdit` +output (mxcli v0.23.0, Mendix 11.14.0, captured 2026-10-05), copied unchanged from +`tests/wave2/fixtures/context-pack/capture/describe/PAGE__Sales.Product_NewEdit.out`. It is a +real popup page: `Layout: Atlas_Core.PopupLayout`, a form grid, Cancel and Save in a footer. + +`product-newedit-popup.html` is **hand-drawn**, because issue #471 quotes the shape of the field +wireframe, not the file: `.popup-stage > .shell-behind + .dialog-scrim > .dialog`. The shell +behind holds the caller's page as such wireframes draw it (a sidebar, a `div.main` with a page +head, its `

` and a card), and the dialog holds the popup page's own title, fields, help +texts and buttons, taken from the describe above. The `wf-bar` and `wf-note` annotation +siblings are the toolkit's own convention. + +On master the scorer chose `div.main` inside `.shell-behind` and printed 18%, every miss from +the shell (the issue measured 29% on the field popup and 13% on a throwaway one, the same way). +With the fix it scores the dialog: 100%. + +Re-capture the wireframe from a real popup wireframe when one can be shared. diff --git a/tests/wave2/fixtures/page-fidelity-popup/product-newedit-describe-v023.mdl b/tests/wave2/fixtures/page-fidelity-popup/product-newedit-describe-v023.mdl new file mode 100644 index 00000000..34fcd84d --- /dev/null +++ b/tests/wave2/fixtures/page-fidelity-popup/product-newedit-describe-v023.mdl @@ -0,0 +1,63 @@ +create or modify page Sales.Product_NewEdit ( + Title: 'Catalog item', + Layout: Atlas_Core.PopupLayout, + Folder: 'Catalog/Pages', + Params: { $Product: Sales.Product } +) { + layoutgrid g { + row row1 { + column col1 (DesktopWidth: AutoFill) { + dataview dv (DataSource: $Product, ReadOnlyStyle: Control) { + -- Context: $currentObject (Product) + container formGrid (Class: 'form-grid') { + container fgSku (Class: 'form-group') { + textbox tbSku (Label: 'SKU *', Attribute: Sku) + dynamictext helpSku ( + Content: 'Set when the item is created. It cannot change afterwards.', + Class: 'form-help' + ) + } + container fgName (Class: 'form-group') { + textbox tbName (Label: 'Name *', Attribute: Name) + } + container fgDescription (Class: 'form-group span-2') { + textarea taDescription (Label: 'Description', Attribute: Description) + } + container fgCategory (Class: 'form-group') { + textbox tbCategory (Label: 'Category', Attribute: Category) + } + container fgPrice (Class: 'form-group') { + textbox tbPrice (Label: 'Unit price *', Attribute: UnitPrice) + } + container fgBrand (Class: 'form-group') { + combobox cbBrand ( + Label: 'Brand *', + Attribute: Product_Brand, + DataSource: database from Sales.Brand, + CaptionAttribute: Name + ) + } + container fgActive (Class: 'form-group') { + checkbox cbActive (Label: 'Active', Attribute: Active) + dynamictext helpActive ( + Content: 'Inactive items are not offered on new sales orders.', + Class: 'form-help' + ) + } + } + footer footer1 { + actionbutton btnCancel (Caption: 'Cancel', Action: cancel_changes close_page, Class: 'btn') + actionbutton btnSave ( + Caption: 'Save', + Action: microflow Sales.ACT_Product_Save(Product: $Product), + ButtonStyle: Primary, + Class: 'btn btn-primary' + ) + } + } + } + } + } +} + +grant view on page Sales.Product_NewEdit to Sales.Admin, Sales.SalesOfficer; diff --git a/tests/wave2/fixtures/page-fidelity-popup/product-newedit-popup.html b/tests/wave2/fixtures/page-fidelity-popup/product-newedit-popup.html new file mode 100644 index 00000000..d9675926 --- /dev/null +++ b/tests/wave2/fixtures/page-fidelity-popup/product-newedit-popup.html @@ -0,0 +1,48 @@ + + + + +Wireframe: Sales.Product_NewEdit (popup) + + + +
Wireframe · Sales.Product_NewEdit · popup over Product_Overview
+ +
The overview behind the scrim is the caller's page, drawn for context only.
+ + diff --git a/tests/wave2/test-page-fidelity-mocks.sh b/tests/wave2/test-page-fidelity-mocks.sh index abf768e7..47e8bca7 100755 --- a/tests/wave2/test-page-fidelity-mocks.sh +++ b/tests/wave2/test-page-fidelity-mocks.sh @@ -247,6 +247,25 @@ hasnt "no layout hint when no missed class looks like chrome" "$OUT19" "is not i hasnt "no layout hint for page classes (seg-tabs, dg-*)" "$OUT6" "is not in the input" has "the hint names the layout when region-* is missed" "$OUT16" "layout LayoutTest.App_Default is not in the input" +echo " -- a popup wireframe scores the dialog, not the caller's page behind it (#471)" +# fixtures/page-fidelity-popup/CAPTURE.md: the MDL is a verbatim mxcli v0.23.0 describe of a +# popup page; the wireframe draws it in the issue's shape, .popup-stage > .shell-behind + +# .dialog-scrim > .dialog, with the caller's overview (div.main, h1, card) behind the scrim. +PF="$TOOLKIT/tests/wave2/fixtures/page-fidelity-popup" +OUT20=$(cd "$TMP" && node "$SUT" --no-log "$PF/product-newedit-popup.html" Product_NewEdit "$PF/product-newedit-describe-v023.mdl" 2>&1) +echo "$OUT20" | sed 's/^/ | /' +hasnt "the caller's h1 behind the scrim is not scored" "$OUT20" "heading: Product catalog" +hasnt "the caller's card behind the scrim is not scored" "$OUT20" "heading: Active items" +hasnt "the caller's button is not an owed action" "$OUT20" "action: New item" +has "the dialog's fields and buttons all match" "$OUT20" "text-match 100%" +sed 's/class="dialog" /class="sheet" /' "$PF/product-newedit-popup.html" > "$TMP/stage-only.html" +OUT21=$(cd "$TMP" && node "$SUT" --no-log "$TMP/stage-only.html" Product_NewEdit "$PF/product-newedit-describe-v023.mdl" 2>&1) +hasnt "a .popup-stage with no .dialog still drops the shell" "$OUT21" "heading: Product catalog" +has "...and still scores what is in front of it" "$OUT21" "headings 1/1" +sed 's/class="dialog" /class="panel" /; s/class="popup-stage"/class="stage"/' "$PF/product-newedit-popup.html" > "$TMP/no-popup.html" +OUT22=$(cd "$TMP" && node "$SUT" --no-log "$TMP/no-popup.html" Product_NewEdit "$PF/product-newedit-describe-v023.mdl" 2>&1) +has "without .dialog/.popup-stage, dialog-scrim alone does not switch boundary (div.main, as before)" "$OUT22" "heading: Product catalog" + echo printf 'test-page-fidelity-mocks: %d passed, %d failed\n' "$PASS" "$FAIL" [ "$FAIL" -eq 0 ] From 5a739041bd6d46505ffbeacb6cf197355500544b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:22:05 +0000 Subject: [PATCH 05/12] fix(portability): stop [ \t] in sed/grep brackets trimming a trailing t (#468) BSD/macOS sed reads [ \t] in a bracket expression as "space, backslash or t"; only GNU sed expands \t there, and grep never does. Every trim built on it also ate a leading or trailing letter t and left real tabs. - project-bin/context-pack.sh: names() trimmed Mod.ENUM_Product to Mod.ENUM_Produc, then reported it "not in the folder plan". - bin/lib/closeout.sh: _co_trim and the Stage prefix strip. - bin/lib/entry-mode.sh: the answer-prefix strip and the value trim (a tab-separated "answered:migration" read as no entry mode). - bin/gate-check.sh: the intake section count grep. - tests/wave2/test-progress-report-field-fixes.sh: [^\t] -> a real tab. - bin/check-portability.sh: flag \t inside a bracket that follows sed/grep on the line (awk is fine; $'...' strings are skipped). - tests: a BSD-sed (sed --posix shim) pass in test-context-pack.sh over its captured golden brief, and test-sed-tab-bracket.sh for closeout, entry-mode and the lint. Both fail on master and pass here. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + bin/check-portability.sh | 15 ++++ bin/gate-check.sh | 2 +- bin/lib/closeout.sh | 4 +- bin/lib/entry-mode.sh | 4 +- project-bin/context-pack.sh | 2 +- tests/wave2/test-context-pack.sh | 22 +++++ .../wave2/test-progress-report-field-fixes.sh | 2 +- tests/wave2/test-sed-tab-bracket.sh | 82 +++++++++++++++++++ 9 files changed, 127 insertions(+), 7 deletions(-) create mode 100755 tests/wave2/test-sed-tab-bracket.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index edafc87e..4e4c6d67 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(portability): **`[ \t]` in a sed or grep bracket no longer trims a trailing "t" (#468).** BSD/macOS sed reads `[ \t]` as "space, backslash or t" (only GNU sed expands `\t` there, and grep never does), so `context-pack.sh` printed `Mod.ENUM_Product` as `Mod.ENUM_Produc` and called it "not in the folder plan"; `closeout.sh`'s cell trim cut "bulk import" to "bulk impor" and "test" to "est"; `entry-mode.sh` read an answer written `answered:migration` as no entry mode at all; and `gate-check.sh`'s intake section count matched `##t…` but never `##`. All five sites now use `[[:blank:]]`, and a test fixture's `[^\t]` uses a real tab (`$'…'`). `check-portability.sh` flags `\t` inside a bracket that follows `sed`/`grep` on the line (awk expands `\t` everywhere and is not flagged; a `$'…'` string is skipped). Reproduced on GNU sed through a `sed --posix` shim, which reads the bracket the BSD way: the captured context-pack golden brief (`Sales.Product`, `Sales.ACT_ApprovalStep_Reject`) failed 4 of 4 new BSD-sed checks on master and passes after; new `tests/wave2/test-sed-tab-bracket.sh` covers `closeout.sh`, `entry-mode.sh` and the lint (6 of 9 fail on master). — jopterhorst ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/bin/check-portability.sh b/bin/check-portability.sh index 444dee0f..2802a115 100755 --- a/bin/check-portability.sh +++ b/bin/check-portability.sh @@ -157,6 +157,21 @@ while IFS= read -r f; do fi ;; esac + # \t inside a sed/grep bracket expression is not a tab. BSD/macOS sed reads [ \t] as + # "space, backslash or t" (GNU sed alone expands it), and grep reads it that way on + # every platform, so a trim stripped a trailing "t": Mod.ENUM_Product became + # Mod.ENUM_Produc in context-pack.sh (#468). awk expands \t in a regex everywhere, so + # only a bracket that FOLLOWS sed/grep on the line counts; a bracket inside a $'...' + # string holds a real tab and is skipped. + case "$txt" in + *sed*'\t'*|*grep*'\t'*) + if grep -Eq -- '(sed|grep)[^|]*\[[^]]*\\t' <<<"$txt" \ + && ! grep -Eq -- "\\\$'[^']*\\[[^]]*\\\\t" <<<"$txt"; then + report "$f" "$ln" "\\t inside a sed/grep bracket expression (BSD: backslash or t, not tab)" \ + 'Use [[:blank:]] (space or tab) or [[:space:]], or a real tab via $'"'"'\t'"'"'.' + fi ;; + esac + case "$txt" in *'grep -P'*|*'grep -qP'*|*'grep -oP'*) report "$f" "$ln" "grep -P is GNU-only" \ diff --git a/bin/gate-check.sh b/bin/gate-check.sh index 9ec15e29..09450360 100755 --- a/bin/gate-check.sh +++ b/bin/gate-check.sh @@ -761,7 +761,7 @@ check_stage_P() { else if (l ~ /^unverified/ && l ~ /how to verify/) ok=1 } END { flush(); print bad }' "$f") - total=$(grep -c '^##[ \t]' "$f") + total=$(grep -c '^##[[:blank:]]' "$f") # Always name the file that was graded. A Stage P line that does not is what let the # wrong-file bug survive for weeks: the output was locally coherent and globally wrong. diff --git a/bin/lib/closeout.sh b/bin/lib/closeout.sh index 5243c2a1..a0daf45c 100755 --- a/bin/lib/closeout.sh +++ b/bin/lib/closeout.sh @@ -107,7 +107,7 @@ _co_checkpoints() { esac } -_co_trim() { printf '%s' "$1" | sed -E 's/^[ \t]+//; s/[ \t]+$//; s/^\*+//; s/\*+$//; s/^[ \t]+//; s/[ \t]+$//'; } +_co_trim() { printf '%s' "$1" | sed -E 's/^[[:blank:]]+//; s/[[:blank:]]+$//; s/^\*+//; s/\*+$//; s/^[[:blank:]]+//; s/[[:blank:]]+$//'; } # Truncate to N chars on a word boundary, with an ellipsis. Cells in a real register run to # 2,000 characters; the block is a recap, the register is the record. _co_trunc() { @@ -117,7 +117,7 @@ _co_trunc() { } # Stage cell → bare stage token ("Stage 3" → 3, "P" → P). Empty when it is not a stage. _co_stage_of() { - local s; s="$(_co_trim "$1" | sed -E 's/^[Ss]tage[ \t]*//')" + local s; s="$(_co_trim "$1" | sed -E 's/^[Ss]tage[[:blank:]]*//')" case "$s" in P|p) echo P ;; [0-7]) echo "$s" ;; *) echo "" ;; esac } # First word of a status cell, upper-cased, punctuation stripped — the same "controlled diff --git a/bin/lib/entry-mode.sh b/bin/lib/entry-mode.sh index 91771774..486149ed 100644 --- a/bin/lib/entry-mode.sh +++ b/bin/lib/entry-mode.sh @@ -34,14 +34,14 @@ entry_mode_token() { # doing it after would let that stripping treat "(2026-09-01)" as if it opened at the # start of the value and eat everything from "assumed (" onward, value included. The # register's bare value ("migration") has no such prefix and passes through untouched. - v="$(printf '%s' "$v" | sed -E 's/^[ \t>*_-]*(answered|answer|a|decision|assumed)[ \t]*(\([^)]*\))?[ \t]*:[ \t]*//')" + v="$(printf '%s' "$v" | sed -E 's/^[[:blank:]>*_-]*(answered|answer|a|decision|assumed)[[:blank:]]*(\([^)]*\))?[[:blank:]]*:[[:blank:]]*//')" v="${v%%(*}" # drop a trailing parenthetical: "migration (not greenfield)" v="${v%%,*}" # drop anything after the first comma v="${v%%;*}" # ...or semicolon # Trim leading/trailing noise, INCLUDING markdown emphasis markers on both ends # ("**migration**", "_greenfield_") — the old trailing trim only stripped spaces, # tabs and a period, so a bolded or italicised value never matched anything. - v="$(printf '%s' "$v" | sed 's/^[ \t>*_-]*//; s/[ \t.*_-]*$//')" + v="$(printf '%s' "$v" | sed 's/^[[:blank:]>*_-]*//; s/[[:blank:].*_-]*$//')" full="$v" # Documented multi-word phrases resolve on the WHOLE value, before the first-word # truncation below: the runbook's own label for the fourth mode is "Change an existing diff --git a/project-bin/context-pack.sh b/project-bin/context-pack.sh index a07a1eed..29f34c0a 100755 --- a/project-bin/context-pack.sh +++ b/project-bin/context-pack.sh @@ -114,7 +114,7 @@ SLICE="$(printf '%s' "$ROW" | cut -d'|' -f4)" case "$SLICE" in -|—|none) SLICE="" ;; esac # a cell is a comma-separated list of qualified names; "—" or "-" means none -names() { printf '%s\n' "$1" | tr ',' '\n' | sed 's/^[ \t]*//; s/[ \t]*$//' | grep -vE '^(-|—|none)?$'; } +names() { printf '%s\n' "$1" | tr ',' '\n' | sed 's/^[[:blank:]]*//; s/[[:blank:]]*$//' | grep -vE '^(-|—|none)?$'; } # folder for a document: its row in the "Document folder plan" table, by full or short name folder_of() { diff --git a/tests/wave2/test-context-pack.sh b/tests/wave2/test-context-pack.sh index cb0ba375..bf6c5632 100644 --- a/tests/wave2/test-context-pack.sh +++ b/tests/wave2/test-context-pack.sh @@ -16,6 +16,10 @@ # an unrecognised value warns and runs as on. # 7. OLD BRIEF: a brief with no "### Build steps" table (written before packs) exits 3, like off. # 6. BOTH LAYOUTS: the .mpr at the root, and under app/ (two-tree). +# 8. BSD SED (#468): `[ \t]` in a sed bracket is "space, backslash or t" on macOS, so a trim +# ate the trailing t of Sales.ACT_ApprovalStep_Reject and Sales.Product and both steps +# exited 1. On GNU sed the case re-runs through a `sed --posix` shim, which reads the +# bracket the same way BSD does; on macOS the plain runs above already are that case. # # Usage: bash test-context-pack.sh [path-to-context-pack.sh] @@ -73,6 +77,24 @@ for layout in root app; do check "unknown step exits 2" "$(run "$P" 9.9)" "2" done +echo "== BSD sed bracket semantics (#468) ==" +if sed --posix -n p /dev/null 2>&1; then + REAL_SED="$(command -v sed)"; mkdir -p "$TMP/bsdsed" + printf '#!/usr/bin/env bash\nexec "%s" --posix "$@"\n' "$REAL_SED" > "$TMP/bsdsed/sed"; chmod +x "$TMP/bsdsed/sed" + if [ "$(printf 'Product\n' | "$TMP/bsdsed/sed" 's/[ \t]*$//')" = "Produc" ]; then # portability-ok: the #468 defect, run on purpose to prove the shim reproduces it + P="$TMP/bsd"; mkproj "$P" root + runbsd() { ( cd "$1" && PATH="$TMP/bsdsed:$PATH" PROJECT_ROOT="$1" bash "$CP" Sales "$2" --out "$1/pack.md" ) 2>"$1/err"; echo $?; } + check "bsd sed: 5.1 exits 0 (example name ends in t)" "$(runbsd "$P" 5.1)" "0" + has "bsd sed: 5.1 example kept its trailing t" "### Sales.ACT_ApprovalStep_Reject (microflow)" "$P/pack.md" + check "bsd sed: 5.2 exits 0 (Reads name ends in t)" "$(runbsd "$P" 5.2)" "0" + has "bsd sed: 5.2 Sales.Product described" "Sales.Product: Active" "$P/pack.md" + else + echo " SKIP bsd sed: this sed --posix expands \\t in a bracket; the case is not reproducible here" + fi +else + echo " (sed is not GNU sed: the runs above already used BSD bracket semantics)" +fi + echo "== switch ==" P="$TMP/root"; mkdir -p "$P/docs/brain" diff --git a/tests/wave2/test-progress-report-field-fixes.sh b/tests/wave2/test-progress-report-field-fixes.sh index bf387456..23cfe346 100644 --- a/tests/wave2/test-progress-report-field-fixes.sh +++ b/tests/wave2/test-progress-report-field-fixes.sh @@ -459,7 +459,7 @@ grep -q "discovery cold" "$WORK/out15.b" && ok "a copied project with its cache hasnt "$WORK/out15/b.html" "Sessions from other folders (1)" "the copy does not show the original project's sessions" CLAUDE_CONFIG_DIR="$CFG15" PROGRESS_REPORT_DEBUG=1 "$PY" "$GEN" "$P15" --out "$WORK/out15/c.html" >"$WORK/out15.c" 2>&1 grep -q "discovery warm" "$WORK/out15.c" && ok "the original's own cache is still warm" || bad "the original's cache is warm" -sed -i.bak '1s/rules=[^\t]*/rules=share=0.10;calls=5/' "$P15/.claude/progress-report-sessions.txt"; rm -f "$P15/.claude/progress-report-sessions.txt.bak" +sed -i.bak $'1s/rules=[^\t]*/rules=share=0.10;calls=5/' "$P15/.claude/progress-report-sessions.txt"; rm -f "$P15/.claude/progress-report-sessions.txt.bak" CLAUDE_CONFIG_DIR="$CFG15" PROGRESS_REPORT_DEBUG=1 "$PY" "$GEN" "$P15" --out "$WORK/out15/d.html" >"$WORK/out15.d" 2>&1 grep -q "discovery cold" "$WORK/out15.d" && ok "a cache written under other thresholds is rebuilt" || bad "other thresholds rebuild the cache" diff --git a/tests/wave2/test-sed-tab-bracket.sh b/tests/wave2/test-sed-tab-bracket.sh new file mode 100755 index 00000000..e75b1612 --- /dev/null +++ b/tests/wave2/test-sed-tab-bracket.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# test-sed-tab-bracket.sh — `[ \t]` in a sed/grep bracket is not a tab off GNU sed (#468). +# +# Usage: bash tests/wave2/test-sed-tab-bracket.sh bin/lib/entry-mode.sh +# +# BSD/macOS sed reads `[ \t]` as "space, backslash or t" (only GNU sed expands \t there), so +# every trim built on it also ate a leading or trailing letter t and left real tabs alone. +# context-pack.sh's own case is pinned in test-context-pack.sh (its captured golden brief has +# Sales.Product and Sales.ACT_ApprovalStep_Reject); this fixture covers the two libraries +# that carried the same trim, and the lint that keeps it out. +# +# On GNU sed the BSD reading is reproduced through a `sed --posix` shim on PATH (verified to +# turn "Product" into "Produc" with the old pattern before anything is asserted); on a machine +# whose sed is already BSD the shim is not needed and the functions run as they are. +# +# T1 closeout.sh _co_trim keeps a trailing/leading t ("bulk import", "**test harness**") +# and still trims a real tab; _co_stage_of reads "Stage3" as 3 +# T2 entry-mode.sh entry_mode_token reads a tab-separated answer ("answered:migration", +# "greenfield"), which the old trim left untouched and the first-word cut then emptied +# T3 bin/check-portability.sh (same toolkit) flags `sed 's/[ \t]*$//'` and +# `grep -c '^##[ \t]'`, passes [[:blank:]], an awk regex and a $'...' real tab + +set -uo pipefail +SUT="${1:?usage: test-sed-tab-bracket.sh /path/to/bin/lib/entry-mode.sh}" +case "$SUT" in /*) ;; *) SUT="$PWD/$SUT" ;; esac +LIB="$(cd "$(dirname "$SUT")" && pwd)" +TK="$(cd "$LIB/../.." && pwd)" +W="$(mktemp -d "${TMPDIR:-/tmp}/sedtab.XXXXXX")" || exit 2 +trap 'rm -rf "$W"' 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: /'; } +check() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1" "expected [$3], got [$2]"; fi; } +TAB="$(printf '\t')" + +SHIMPATH="$PATH" +if sed --posix -n p /dev/null 2>&1; then + mkdir -p "$W/bin" + printf '#!/usr/bin/env bash\nexec "%s" --posix "$@"\n' "$(command -v sed)" > "$W/bin/sed"; chmod +x "$W/bin/sed" + SHIMPATH="$W/bin:$PATH" + echo "== GNU sed: running under a sed --posix shim (BSD bracket reading) ==" +else + echo "== sed is not GNU sed: running as-is ==" +fi +probe="$(printf 'Product\n' | PATH="$SHIMPATH" sed 's/[ \t]*$//')" # portability-ok: the #468 defect, run on purpose +if [ "$probe" != "Produc" ]; then + echo " SKIP T1-T2: this sed expands \\t in a bracket, the old defect cannot show here" +else + run() { PATH="$SHIMPATH" bash -c ". '$LIB/closeout.sh'; . '$SUT'; $1"; } + echo "== T1 closeout.sh ==" + check "T1a trailing t kept" "$(run "_co_trim ' Decision about bulk import '")" "Decision about bulk import" + check "T1b leading t kept" "$(run "_co_trim '**test harness**'")" "test harness" + check "T1c real tab trimmed" "$(run "_co_trim '${TAB}CONFIRMED${TAB}'")" "CONFIRMED" + check "T1d Stage3 is stage 3" "$(run "_co_stage_of 'Stage${TAB}3'")" "3" + echo "== T2 entry-mode.sh ==" + check "T2a answered:migration" "$(run "entry_mode_token 'answered:${TAB}migration'" 2>/dev/null)" "migration" + check "T2b greenfield" "$(run "entry_mode_token '${TAB}greenfield'" 2>/dev/null)" "greenfield" + check "T2c bold value unchanged" "$(run "entry_mode_token '**requirements-driven**'" 2>/dev/null)" "requirements-driven" +fi + +echo "== T3 check-portability.sh rule ==" +mkdir -p "$W/lint/bad" "$W/lint/good" +# Written with @ for the backslash, so this file does not carry the pattern it plants. +tr '@' '\\' > "$W/lint/bad/a.sh" <<'X' +#!/usr/bin/env bash +x="$(printf '%s' "$1" | sed 's/^[ @t]*//; s/[ @t]*$//')" +total=$(grep -c '^##[ @t]' "$2") +X +cat > "$W/lint/good/b.sh" <<'X' +#!/usr/bin/env bash +x="$(printf '%s' "$1" | sed 's/^[[:blank:]]*//')" +y="$(awk '{gsub(/^[ \t]+/, ""); print}' "$2")" +sed -i.bak $'1s/rules=[^\t]*/rules=x/' "$2" +X +out="$(bash "$TK/bin/check-portability.sh" "$W/lint/bad" 2>&1)"; rc=$? +n="$(grep -c 'inside a sed/grep bracket' <<<"$out")" +if [ "$rc" -eq 1 ] && [ "$n" -eq 2 ]; then ok "T3a sed trim and grep -c: 2 of 2 flagged"; else bad "T3a want rc 1 and 2 hits, got rc $rc and $n" "$out"; fi +out="$(bash "$TK/bin/check-portability.sh" "$W/lint/good" 2>&1)"; rc=$? +check "T3b [[:blank:]], awk regex, \$'...' tab: clean" "$rc" "0" + +echo "-- $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] From 7eedb5cdbbb8686b5b7daef1642830b7ee2accac Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:44:18 +0000 Subject: [PATCH 06/12] fix(progress-report): one nav group, one id=stamp, no build-plan.html note (#479 items 1-3) - Top bar: drop the Stage P 0..6 chips and their outline observer; the bar is one group of section links (Stages, Build, Pages, Tips) with Stages jumping to the stage rail (id=stages). Sticky and phone rules kept, #stage-N card anchors kept, and the header line now states the current stage. - id=stamp: only the "Last updated" span keeps it. The session-log span, the build/score/plan notes and the footer note get unique ids plus a data-stamp marker. build-plan-status.sh --refresh now ignores lines matching id=stamp|data-stamp, so its no-rewrite rule is unchanged and pages written before this still compare. - Header: "replaces the former build-plan.html" removed. Fixtures: polish P7 (nav, anchors, current stage, unique ids, stamp count, volatile-line markers, --refresh byte-identical); field-fixes F11 flipped, F14 asserts the autoreload's live controls ride on the one id=stamp line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 1 + project-bin/build-plan-status.sh | 4 +- project-bin/progress-report.py | 26 ++++++------ .../wave2/test-progress-report-field-fixes.sh | 5 ++- tests/wave2/test-progress-report-polish.sh | 40 +++++++++++++++++++ 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5a253d7..aa0d6487 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(progress-report): **the top bar is one group of section links (Stages, Build, Pages, Tips) instead of stage chips plus links; the page carries one `id=stamp`, and the header drops the build-plan.html note.** The sticky bar loses the `Stage P 0 … 6` chips, which repeated the stage list right below it, and gains a `Stages` link to that list; the `#stage-N` card anchors stay for links from other pages, and the header now states the current stage. Five elements shared `id=stamp` (invalid HTML); the "Last updated" span keeps it and the others get their own ids plus a `data-stamp` marker, which `build-plan-status.sh --refresh` now reads (with `id=stamp`, so pages written before this still compare) to leave an unchanged page alone. "replaces the former build-plan.html" is gone from the header sub-line. Fixes #479 items 1–3 — MendixMau (review 2026-10-20) - fix(progress-report): **the build plan is shown once: the phase cards are the plan.** Expanding a card now shows its full rows (#, Kind, Step, Produces/Proves, Depends on, State, Scripts) and Claims, which only the duplicate "Full plan tables" block carried; that block is gone and views A (script folders) and B (module proof) are their own section. The `now` phase renders open with a selected style, opening another phase closes the previous one (accordion; `#phase-N` links still open their card), and a viewer's saved open/closed state survives the 30 s reload and wins over the default (#474) — field feedback from a requirements-driven project at Stage 5 — MendixMau (field feedback 2026-10-20) ## v2026.10.19 diff --git a/project-bin/build-plan-status.sh b/project-bin/build-plan-status.sh index c7f1ac32..b450ca84 100755 --- a/project-bin/build-plan-status.sh +++ b/project-bin/build-plan-status.sh @@ -739,8 +739,10 @@ if [ "$WRITE_HTML" -eq 1 ]; then if "$PY" "$GEN" "$ROOT" "${GEN_ARGS[@]}" >/dev/null 2>"$OUT.err.$$"; then # Two renders of the same state differ only in the stamp lines. A refresh that changed # nothing leaves the file alone, or every exec.sh run would dirty the working tree. + # Stamp lines carry a data-stamp attribute (one of them also id=stamp); 'id=stamp' is kept in + # the pattern so a page written before #479, with id=stamp on every such line, still compares. if [ "$REFRESH" -eq 1 ] && [ -f "$OUT" ] \ - && [ "$(grep -v 'id=stamp' "$OUT")" = "$(grep -v 'id=stamp' "$OUT_TMP")" ]; then + && [ "$(grep -vE 'id=stamp|data-stamp' "$OUT")" = "$(grep -vE 'id=stamp|data-stamp' "$OUT_TMP")" ]; then rm -f "${OUT_TMP:?}" else mv -f "$OUT_TMP" "$OUT" diff --git a/project-bin/progress-report.py b/project-bin/progress-report.py index 951675d7..72aac68a 100755 --- a/project-bin/progress-report.py +++ b/project-bin/progress-report.py @@ -1392,7 +1392,7 @@ def rule(ratio, kind, title, seen, matters, fix, why, lost_s=0.0, lost_tok=0.0, health = run_health(cs) return fired[:ADV["max_items"]], quiet + fired[ADV["max_items"]:], health, cs, hidden[0] -JS = "" +JS = "" # ----------------------------------------------------------------- render CSS = """ :root{--bg:#f6f7f9;--card:#fff;--ink:#1c2430;--mut:#667085;--line:#e3e7ee;--accent:#2f6fed;--ok:#1f9d63;--warn:#d98a0b;--bad:#c93c3c;--idle:#d9dde5;--main:#2f6fed;--sub:#8b5cf6;--soft:#eef2fb;--shadow:0 1px 2px rgba(20,30,50,.06)} @@ -1435,8 +1435,8 @@ def rule(ratio, kind, title, seen, matters, fix, why, lost_s=0.0, lost_tok=0.0, .note{font-size:12px;color:var(--mut);margin:4px 0} html{scroll-behavior:smooth}[id^=stage-],[id^=agents-],[id^=phase-],#tips,#pages{scroll-margin-top:56px} .snav{position:sticky;top:0;z-index:20;background:var(--bg);border-bottom:1px solid var(--line);padding:8px 16px;display:flex;gap:6px;align-items:center;flex-wrap:wrap} -.snav b{font-size:12px;color:var(--mut);margin-right:4px}.snav a{min-width:34px;text-align:center;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid var(--line);font-weight:650;font-size:13px;color:var(--ink)} -.snav a.passed{border-color:var(--ok)}.snav a.cur{background:var(--accent);color:#fff;border-color:var(--accent)}.snav a:hover{text-decoration:none;border-color:var(--accent)} +.snav a{min-width:34px;text-align:center;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid var(--line);font-weight:650;font-size:13px;color:var(--ink)} +.snav a:hover{text-decoration:none;border-color:var(--accent)} .hd h3 a{color:inherit}.updated{display:inline-block;margin-top:6px;padding:3px 10px;border-radius:8px;background:var(--soft);font-size:13px;font-weight:600} .layout{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:16px;align-items:start}.layout.solo{display:block}.content{min-width:0}.side{position:sticky;top:52px;max-height:calc(100vh - 64px);overflow:auto;padding-right:2px}.side h2{margin-top:0}.health{margin-top:8px}.health .hi{font-size:13px;margin-top:3px}.grp{margin-top:8px;border:1px solid var(--line);border-radius:10px;background:var(--card)}.grp>summary{cursor:pointer;padding:9px 12px;font-size:14px}.grp .speed,.grp>.split,.grp>div,.grp .tip{padding:0 12px 10px}.tip{border-top:1px solid var(--line);padding-top:10px}.tip h3{font-size:13px;margin:0 0 4px}.tip p{margin:0 0 4px;font-size:12.5px}.p-info{background:#8793a6;color:#fff}@media(max-width:1000px){.layout{grid-template-columns:minmax(0,1fr)}.side{position:static;max-height:none}}.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:4px}.p-green{background:#2e9e5b;color:#fff}.p-amber{background:#e0a100;color:#fff}.p-red{background:#d1383d;color:#fff}.pill{padding:1px 9px;margin-left:6px;border-radius:99px;font-size:12px}.speed{margin-top:8px;font-size:13px}.split{display:flex;gap:2px;margin:6px 0}.seg{background:var(--accent);color:#fff;font-size:11px;padding:3px 6px;border-radius:4px;white-space:nowrap;overflow:hidden}.seg:nth-child(2){opacity:.75}.seg:nth-child(3){background:var(--idle);color:var(--ink)}.seg:nth-child(4){opacity:.55}.muted{color:var(--mut);font-size:12px}\n.advl{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:10px;margin-top:8px} .advi{border-left:4px solid var(--warn)}.advi.hi{border-left-color:var(--bad)}.advi .rk{font-size:12px;color:var(--mut)}.advi h3{margin:2px 0 6px}.advi p{margin:4px 0;font-size:13px}.advi .why{color:var(--mut);font-size:12px} @@ -1516,13 +1516,15 @@ def money(c): return "n/a" if unp else "$%.0f" % c w("") w("") w("Progress report: %s" % (esc(info["name"]), CSS)) - w("
" % ("" if L else " solo")) + # One group of section links (issue #479). The stage list is one link away; the current stage is in the + # header line below, and the #stage-N card anchors stay for links from other pages. + w("
" % ("" if L else " solo")) last_ev = datetime.datetime.fromtimestamp(an["t1"]).strftime("%Y-%m-%d %H:%M") if L else "no session log" w("

Progress report: %s

" % esc(info["name"])) - w("Last updated %s %s session log last event %s%s
" % (gen, datetime.datetime.now().astimezone().strftime("%Z"), last_ev, + w("Last updated %s %s session log last event %s%s
" % (gen, datetime.datetime.now().astimezone().strftime("%Z"), last_ev, " · Live · refreshes every 30 s · Pause" if args.reload else "")) - w("
Stage P to 6 · entry mode %s · PROJECT.md · gate-check page · replaces the former build-plan.html
" % - (esc(info["entry_mode"]), href(os.path.join(proj, "PROJECT.md")), href(os.path.join(proj, "index.html")))) + w("
Current stage: %s · Stage P to 6 · entry mode %s · PROJECT.md · gate-check page
" % + (info["current"] or "P", esc(("Stage %s: %s" % (info["current"], STAGE_NAME[info["current"]])) if info["current"] in STAGE_NAME else "unknown"), esc(info["entry_mode"]), href(os.path.join(proj, "PROJECT.md")), href(os.path.join(proj, "index.html")))) how_parts = [] if L and DISCOVERY.get("mention") and not args.session_log: rows = "".join("%s%s%d%%" % (esc(u["id"]), datetime.datetime.fromtimestamp(u["t"]).strftime("%Y-%m-%d"), round(u["share"] * 100)) for u in sorted(DISCOVERY.get("used", []), key=lambda u: u["t"])) @@ -1585,7 +1587,7 @@ def money(c): return "n/a" if unp else "$%.0f" % c parts.append("
%d check(s) ran without needing action; %d more are inherent to how the toolkit and mxcli work (build time, required gates and reviews, waiting) and are never shown.
" % (len(quiet), n_inherent)) aside = "".join(parts) # stage rail - w("

Stage rail: Kickoff (P) to Verify and report (6)

Each card shows gate state, artifact progress%s, decisions and links.%s
" % (", wall time, tokens, sub-agents" if L else "", " Time and token splits per stage are approximate (see footer)." if L else "")) + w("

Stage rail: Kickoff (P) to Verify and report (6)

Each card shows gate state, artifact progress%s, decisions and links.%s
" % (", wall time, tokens, sub-agents" if L else "", " Time and token splits per stage are approximate (see footer)." if L else "")) wins = {s: (a, b) for s, a, b in an["wins"]} for s in ["pre"] + STAGES: sd = S[s] @@ -1746,10 +1748,10 @@ def chip(f, cls=""): w("
" + "".join("" % (k, v, pct(v, bp["total"]), KC.get(k, "#888")) for k, v in kinds.items()) + "
") w("
" + "".join("%s %d" % (KC.get(k, "#888"), k, v) for k, v in kinds.items()) + "
") lb = (facts["last_run"]["t"], facts["last_run"]["script"], facts["last_run"]["result"]) if facts else None - if lb: w("
%s. Last build step: %s (%s), %s at %s.
" % (esc(facts_line(facts)), esc(lb[1][:90]), esc(re.sub(r"[^\w .,/()+-]", "", lb[2])[:40].strip() or "no result"), esc(ago(lb[0])), esc(datetime.datetime.fromtimestamp(lb[0]).strftime("%Y-%m-%d %H:%M")))) - if facts and facts["score"]: w("
Latest model score %.1f (%s).
" % (facts["score"]["score"], esc(facts["score"]["script"][:40]))) + if lb: w("
%s. Last build step: %s (%s), %s at %s.
" % (esc(facts_line(facts)), esc(lb[1][:90]), esc(re.sub(r"[^\w .,/()+-]", "", lb[2])[:40].strip() or "no result"), esc(ago(lb[0])), esc(datetime.datetime.fromtimestamp(lb[0]).strftime("%Y-%m-%d %H:%M")))) + if facts and facts["score"]: w("
Latest model score %.1f (%s).
" % (facts["score"]["score"], esc(facts["score"]["script"][:40]))) if bp.get("note"): w("
%s.
" % esc(bp["note"])) - w("
Kinds are the plan's own Kind column; row state is the plan's State column read by prefix (built or done = built, partial or in progress = partial, anything else = not built). Plan read %s from %s.
" % (esc(bp.get("generated") or "unknown"), "architecture/build-plan.md" if bp.get("source") == "md" else "architecture/build-plan.json")) + w("
Kinds are the plan's own Kind column; row state is the plan's State column read by prefix (built or done = built, partial or in progress = partial, anything else = not built). Plan read %s from %s.
" % (esc(bp.get("generated") or "unknown"), "architecture/build-plan.md" if bp.get("source") == "md" else "architecture/build-plan.json")) w("
") for p in bp["phases"]: now = p is cur @@ -1787,7 +1789,7 @@ def chip(f, cls=""): direct = "progress-report.py <project> " + (" ".join("--session-log %s" % esc(x) for x in args.session_log) if args.session_log else "--logs-dir <dir>") w("
  • Inputs. Project folder: %s. Session log(s) (%s): %s. Rates file: %s. To regenerate with the same inputs: %s, or %s.
  • " % (esc(proj), how, logs_txt, rates_txt, regen, direct)) w("") - w("
    Rebuild: project-bin/build-plan-status.sh --html. Setting in PROJECT.md: Progress report: off, ask or auto (now %s). Generated %s.
    %s
    " % (esc(info["setting"]), gen, aside)) + w("
    Rebuild: project-bin/build-plan-status.sh --html. Setting in PROJECT.md: Progress report: off, ask or auto (now %s). Generated %s.
    %s
    " % (esc(info["setting"]), gen, aside)) w(JS) if args.reload: w("" % ("(function(){\nvar VK='pr-view:'+location.pathname,PK='pr-paused',IDLE=20000,EVERY=30000,last=%d,active=%d,lastInput=0,de=document.documentElement;\nfunction get(k){try{return sessionStorage.getItem(k)}catch(e){return null}}\nfunction put(k,v){try{if(v===null)sessionStorage.removeItem(k);else sessionStorage.setItem(k,v)}catch(e){}}\ntry{history.scrollRestoration='manual'}catch(e){}\nfunction restore(){try{var st=JSON.parse(get(VK)||'null');if(!st)return;\n var d=st.d||{};for(var id in d){var e=document.getElementById(id);if(e&&e.tagName==='DETAILS')e.open=!!d[id]}\n var sb=de.style.scrollBehavior;de.style.scrollBehavior='auto';window.scrollTo(0,st.y||0);de.style.scrollBehavior=sb;\n }catch(e){}}\nrestore();window.addEventListener('load',function(){restore();put(VK,null)});\nfunction save(){var d={};var all=document.querySelectorAll('details[id]');for(var i=0;i/dev/null 2>&1; then node - "$WORK/out14/r.html" <<'JSEOF' && ok "node: state saved and restored, no reload within 20 s of input, Pause stops it" || bad "autoreload behaviour (node)" const fs = require("fs"); diff --git a/tests/wave2/test-progress-report-polish.sh b/tests/wave2/test-progress-report-polish.sh index 59fe2091..08bee57a 100644 --- a/tests/wave2/test-progress-report-polish.sh +++ b/tests/wave2/test-progress-report-polish.sh @@ -16,6 +16,10 @@ # P6b an older progress-report.py without --phase-detail (a partial sync) still renders, with the short row tables. # P6c the page's own scripts, run in node against a DOM shim: the now phase is open on load, opening phase 1 # closes phase 2, a #phase-3 link on load wins over the default, and restored state wins over the default. +# P7 top bar and header (#479): one group of section links (Stages, Build, Pages), no stage chips; the bar stays +# sticky and phone-safe; every #stage-N card anchor stays; the header states the current stage; exactly one +# id=stamp, every other volatile line marked data-stamp, every id on the page unique; the autoreload's +# live controls sit on the id=stamp line; no "replaces the former build-plan.html" in the header. # P3 no session log: one calm line (not the orange box), the regenerate hint inside "How this was measured"; # with --no-log the page never claims it looked in folders or logs. # @@ -147,6 +151,42 @@ assert.deepStrictEqual(load("", saved).open(), ["phase-1"], "restored state wins JSEOF else ok "SKIP: node not available for the accordion behaviour test"; fi +echo "P7 top bar, unique stamp id, header wording (#479)" +P7="$WORK/p7"; mkproj "$P7" "**Stage 5 - Build**, in progress - gates passed: P, 0, 1, 2, 3, 4" +cp "$P1/architecture/build-plan.md" "$P7/architecture/" +mkdir -p "$P7/.claude/loop" # recent activity, so the page carries the autoreload script and its live controls +printf 'ts\tscript\tgate\tresult\telapsed_s\tmxbuild_errors\tlint_rc\tlint_total\trestored\tscore\n%s\t02-billing.mdl\tpass\tapplied\t40\t0\t0\t\t0\t90.0\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$P7/.claude/loop/metrics-ledger.tsv" +CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P7" --html >/dev/null 2>&1 +R7="$P7/architecture/progress-report.html" +[ "$(grep -c 'id=stamp' "$R7")" -le 1 ] && ok "grep -c 'id=stamp' is at most 1 ($(grep -c 'id=stamp' "$R7"))" || bad "id=stamp appears $(grep -c 'id=stamp' "$R7") times" +"$PY" - "$R7" "$ROOT/project-bin/progress-report.py" <<'PYEOF7' && ok "one nav group with Stages, sticky and phone-safe; stage anchors kept; current stage in the header; ids unique; volatile lines marked; autoreload controls on the stamp line" || bad "top bar / stamp / header (P7)" +import re, sys, collections +h = open(sys.argv[1], encoding="utf-8").read(); gen = open(sys.argv[2], encoding="utf-8").read() +nav = re.search(r"", h).group(1) +links = re.findall(r"]*>([^<]+)", nav) +assert [t for _, t in links] == ["Stages", "Build", "Pages"], links # no Tips without a session log +assert not re.search(r"href='#stage-", nav) and "" not in nav, nav # the chip group is gone +assert "id=stages" in h and h.index("id=stages") < h.index("id='stage-P'"), "Stages target sits above the stage cards" +for s_ in ["P", "0", "1", "2", "3", "4", "5", "6"]: assert "id='stage-%s'" % s_ in h, s_ +assert ".snav{position:sticky;top:0" in gen and ".snav{padding:6px 12px;flex-wrap:nowrap;overflow-x:auto" in gen +head = h[h.index("
    "):h.index("
    ")] +assert re.search(r"Current stage: Stage 5: [^<]+", head), "current stage in the header" +assert "replaces the former build-plan.html" not in head +ids = re.findall(r"""\bid=['"]?([\w:.-]+)""", h) +dup = [i for i, c in collections.Counter(ids).items() if c > 1]; assert not dup, dup +lines = h.split("\n") +st = [l for l in lines if "id=stamp" in l]; assert len(st) == 1 and "Last updated" in st[0], st +for marker in ("Last updated", "session log last event", "Last build step", "Latest model score", "Plan read", "Rebuild: project-bin"): + for l in lines: + if marker in l and "/dev/null 2>&1 +[ "$(cksum < "$R7")" = "$SUM7" ] && ok "--refresh leaves the page byte-identical when only stamp lines would change" || bad "--refresh rewrote an unchanged page" + echo "P2 placeholder failure on a later stage is pending" P2="$WORK/p2"; mkproj "$P2" "**Stage P - Kickoff**, in progress - gates passed: none" cat > "$P2/index.html" <<'HTML' From 75d474f570145a4729ab255489723225dd4b40e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:49:15 +0000 Subject: [PATCH 07/12] fix(conformance): read "Error:" on the first line only (#470 review) mxcli prints enumeration, scheduled-event and image-collection docs raw, so a doc line can start "Error:"; the any-line check scored those ABSENT. Only the first non-blank line is read now (an -c error also exits 1, which the rc test already scores). The summary regex drops its trailing-space class so the CRLF cases test the CR strip itself. Fixture gains a raw enum-doc case and separate CRLF cases (24/24; 6 FAIL on master; 4 FAIL on the previous head). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 3 +- project-bin/conformance-check.sh | 26 +++++++++++------ tests/wave2/test-conformance-absent-marker.sh | 28 +++++++++++++++---- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cf0111c..a766a83c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,8 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased -- fix(conformance): **a definition that merely contains "(0 " or "not found" is no longer scored ABSENT** (#470). `conformance-check.sh` matched both markers anywhere in mxcli's answer, so a built microflow whose documentation read "HttpStatus (0 = no response or time-out)" went STALE (five false STALEs in one module), and so does every `describe navigation` with a not-found page, which mxcli prints as " not found page ". It now reads an error only on a line starting `Error:`, and an empty set only when the last non-blank line is the `(0 …)` summary (also `(0 workflow group(s))`); CRLF output reads the same. New fixture `tests/wave2/test-conformance-absent-marker.sh` replays two verbatim captures already in the repo (the v0.25.0 navigation describe of mxcli's public test app, and a v0.23.0 microflow describe with the issue's doc sentence spliced in), both layouts; 6 FAIL on master, 18/18 with the fix. — jopterhorst +- fix(conformance): **a definition that merely contains "(0 " or "not found" is no longer scored ABSENT** (#470). `conformance-check.sh` matched both markers anywhere in mxcli's answer, so a built microflow whose documentation read "HttpStatus (0 = no response or time-out)" went STALE (five false STALEs in one module), and so does every `describe navigation` with a not-found page, which mxcli prints as " not found page ". It now reads an error only on the first non-blank line (`Error:…`, the old start-of-output meaning; enumeration, scheduled-event and image-collection docs print raw, so a doc line may start `Error:`), and an empty set only when the last non-blank line is exactly the `(0 …)` summary (also `(0 workflow group(s))`); CRLF output reads the same. New fixture `tests/wave2/test-conformance-absent-marker.sh` replays two verbatim captures already in the repo (the v0.25.0 navigation describe of mxcli's public test app, and a v0.23.0 microflow describe with the issue's doc sentence spliced in), plus an enumeration with a raw `Error:` doc line and CRLF-only cases, both layouts; 6 FAIL on master, 24/24 with the fix. — jopterhorst + ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/project-bin/conformance-check.sh b/project-bin/conformance-check.sh index 8aed3e91..f5520cc4 100755 --- a/project-bin/conformance-check.sh +++ b/project-bin/conformance-check.sh @@ -364,17 +364,25 @@ probe() { # A substring match read a DESCRIBE whose documentation said "HttpStatus (0 = no response or # time-out)" as an empty set, and the verbatim v0.25.0 `describe navigation` of mxcli's public # test app carries " not found page Pages.Vehicle_Overview" (a navigation setting): both - # scored ABSENT, so built rows went STALE (#470). mxcli prints an error as a line starting - # `Error:` (stderr is merged above), and a table's summary as its own last line, - # `fmt.Fprintf("\n%s\n", Summary)` with Summary = "(%d entities)", "(%d workflow group(s))", … - # A body line never starts at column 0 with either: documentation is " * …", statements are - # indented. CR stripped for a Windows-built mxcli. Here-strings, not `printf | grep -q`: - # under pipefail that pipe can lose to SIGPIPE on a long definition (test-pipefail-grep-q.sh). - local last + # scored ABSENT, so built rows went STALE (#470). + # Error — mxcli puts its error FIRST (stderr is merged above, and `-c` pairs it with exit 1, + # which the rc test already scored); so only the first non-blank line is read, the + # old `Error:*` start-of-output meaning. Not any line: a body line CAN start at + # column 0 — enumerations, scheduled events and image collections print their + # documentation raw inside `/** … */` (one ` * ` on the first line only), and a + # multi-line string prints raw too, so a doc line "Error: …" is still PRESENT. + # (0 …) — a table's summary is printed as its own LAST line, `fmt.Fprintf("\n%s\n", Summary)` + # with Summary = "(%d entities)", "(%d workflow group(s))", …; a definition ends in + # `/`, `;` or a grant, never in a parenthesised count. + # CR stripped for a Windows-built mxcli (printed "\n%s\n", so nothing else trails the summary). + # Here-strings, not `printf | grep -q`: under pipefail + # that pipe can lose to SIGPIPE on a long definition (test-pipefail-grep-q.sh). + local first last out="$(tr -d '\r' <<<"$out")" - if grep -q '^Error:' <<<"$out"; then echo "ABSENT"; return; fi + first="$(sed -n '/[^[:space:]]/{p;q;}' <<<"$out")" + case "$first" in Error:*) echo "ABSENT"; return ;; esac last="$(sed '/^[[:space:]]*$/d' <<<"$out" | tail -n 1)" - if grep -Eq '^\(0 .*\)[[:space:]]*$' <<<"$last"; then + if grep -Eq '^\(0 .*\)$' <<<"$last"; then # no trailing class: the CR strip above is what lets CRLF output match echo "ABSENT"; return # empty result set, e.g. "(0 entities)" fi echo "PRESENT" diff --git a/tests/wave2/test-conformance-absent-marker.sh b/tests/wave2/test-conformance-absent-marker.sh index 0b1c42b2..e0dd5b71 100644 --- a/tests/wave2/test-conformance-absent-marker.sh +++ b/tests/wave2/test-conformance-absent-marker.sh @@ -31,8 +31,13 @@ # 5. an empty set whose noun has parentheses, "(0 workflow group(s))", stays ABSENT # 6. a missing element ("Error: … not found", rc 1) stays ABSENT # 7. an "Error:" line with rc 0 is still ABSENT -# 8. CRLF output (a Windows-built mxcli) reads the same: the doc'd microflow PRESENT, the -# empty set ABSENT +# 8. CRLF output (a Windows-built mxcli): the doc'd microflow PRESENT, and the empty set +# ABSENT — the summary regex has no trailing-space class, so this passes only because of +# the CR strip (delete the `tr -d '\r'` and it fails); an rc-0 "Error:" first line ABSENT +# 9. an enumeration whose documentation has a raw line starting "Error:" stays PRESENT: mxcli +# prints enumeration docs as "/**\n * %s\n */" with the multi-line text raw +# (mdl/executor/cmd_enumerations.go), so a doc line can start at column 0. Built from that +# printer, not captured. set -uo pipefail @@ -68,6 +73,8 @@ setup() { # setup -> sets P "$MF" > "$P/cap/mfdoc.out" printf '| Name |\n|------|\n\n(0 entities)\n' > "$P/cap/empty.out" printf '| Name |\n|------|\n\n(0 workflow group(s))\n' > "$P/cap/emptyparen.out" + printf '/**\n * Payment states.\nError: shown when the provider rejects the payment.\n */\ncreate or modify enumeration Sales.PayStatus (\n Paid '"'"'Paid'"'"',\n Error '"'"'Error'"'"'\n);\n/\n' > "$P/cap/enum.out" + printf 'Error: microflow not found: Sales.ACT_Rc0\n' > "$P/cap/rc0.out" if [ "$2" = crlf ]; then for f in "$P"/cap/*.out; do sed 's/$/\r/' "$f" > "$f.tmp" && mv "$f.tmp" "$f"; done fi @@ -81,7 +88,8 @@ case "$cmd" in "DESCRIBE MICROFLOW Sales.ACT_Doc") cat "$c/mfdoc.out" ;; "SHOW ENTITIES IN Empty") cat "$c/empty.out" ;; "SHOW WORKFLOWS IN Empty") cat "$c/emptyparen.out" ;; - "DESCRIBE MICROFLOW Sales.ACT_Rc0") echo "Error: microflow not found: Sales.ACT_Rc0" ;; + "DESCRIBE MICROFLOW Sales.ACT_Rc0") cat "$c/rc0.out" ;; + "DESCRIBE ENUMERATION Sales.PayStatus") cat "$c/enum.out" ;; *) echo "Error: microflow not found: ${cmd##* }" >&2; exit 1 ;; esac EOF @@ -96,6 +104,7 @@ EOF | `/workflows/0` | workflow | Nothing | 1.5 | exec.sh | `SHOW WORKFLOWS IN Empty` | not-built | | `/microflows/2` | microflow | ACT_Gone | 1.6 | exec.sh | `DESCRIBE MICROFLOW Sales.ACT_Gone` | not-built | | `/microflows/3` | microflow | ACT_Rc0 | 1.7 | exec.sh | `DESCRIBE MICROFLOW Sales.ACT_Rc0` | not-built | +| `/enumerations/0` | enumeration | PayStatus | 1.8 | exec.sh | `DESCRIBE ENUMERATION Sales.PayStatus` | built | EOF } @@ -124,10 +133,17 @@ for layout in single two; do || bad "$tag 6. missing element read as $(observed /microflows/2)" [ "$(observed /microflows/3)" = ABSENT ] && ok "$tag 7. Error line with rc 0 is ABSENT" \ || bad "$tag 7. rc-0 Error read as $(observed /microflows/3)" + [ "$(observed /enumerations/0)" = PRESENT ] && ok "$tag 9. enum doc with a raw 'Error:' line stays PRESENT" \ + || bad "$tag 9. enum doc line 'Error: …' read as $(observed /enumerations/0)" else - [ "$(observed /microflows/1)" = PRESENT ] && [ "$(observed /entities/0)" = ABSENT ] \ - && ok "$tag 8. CRLF: doc'd microflow PRESENT, empty set ABSENT" \ - || bad "$tag 8. CRLF misread" "mf=$(observed /microflows/1) empty=$(observed /entities/0)" + [ "$(observed /microflows/1)" = PRESENT ] && ok "$tag 8a. CRLF: doc'd microflow PRESENT" \ + || bad "$tag 8a. CRLF doc'd microflow read as $(observed /microflows/1)" + [ "$(observed /entities/0)" = ABSENT ] && [ "$(observed /workflows/0)" = ABSENT ] \ + && ok "$tag 8b. CRLF: '(0 …)\\r' summaries are ABSENT (the CR strip, not a trailing-space class)" \ + || bad "$tag 8b. CRLF empty set misread" "entities=$(observed /entities/0) workflows=$(observed /workflows/0)" + [ "$(observed /microflows/3)" = ABSENT ] && [ "$(observed /enumerations/0)" = PRESENT ] \ + && ok "$tag 8c. CRLF: rc-0 'Error:' first line ABSENT, enum doc 'Error:' line PRESENT" \ + || bad "$tag 8c. CRLF Error handling" "rc0=$(observed /microflows/3) enum=$(observed /enumerations/0)" fi done done From d751f8df163dd73a3d4fafc1fae3d7813934da9f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:50:41 +0000 Subject: [PATCH 08/12] fix(monkey): whole-caption match for non-English sign-out, keep project flags (#469 review) The non-English words are ordinary verbs, so a word match left "Ziek afmelden", "Afmelden nieuwsbrief", "Vom Kurs abmelden" and "Disconnetti dispositivo" unfuzzed; they now match only as the whole caption, and the focus check tests text, aria-label and title one by one. A project RegExp keeps its flags (g/y dropped). The rapidTab static guard is scoped to that branch: the old one passed a blind-Enter mutation, the new one fails it. Fixture 22/22; 8 FAIL on master, 3 on the previous head. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 3 ++- project-tests/e2e/monkey.js | 42 ++++++++++++++++++----------- tests/wave2/test-monkey-left-app.sh | 21 +++++++++++++-- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac533ca..c280f530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,8 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased -- fix(monkey): **the monkey no longer signs itself out of a non-English app** (#469). `monkey.js` recognised only English sign-out captions, so in a Dutch app it clicked "Afmelden", landed on `login.html`, scored a false CRASH and fuzzed nothing more (seed 204262061: 3 observations, 1 crash-class; 24 and 0 with the label excluded). The default now also covers Dutch, German, French, Spanish, Portuguese and Italian sign-out captions (sign-in captions such as "Aanmelden" stay fuzzable); a project adds its own in `project.config.js` `signOutPattern` or `SIGN_OUT_PATTERN`, added to the default, never replacing it. `rapidTab` no longer presses Enter while focus is on a sign-out control (top bar, nav item, aria-label or title), the route by which a layout control outside `.mx-page` was reached. `test-monkey-left-app.sh` gains the issue's captions, sign-in near-misses, the project pattern and the rapidTab guard: 5 FAIL on master, 18/18 with the fix. — jopterhorst +- fix(monkey): **the monkey no longer signs itself out of a non-English app** (#469). `monkey.js` recognised only English sign-out captions, so in a Dutch app it clicked "Afmelden", landed on `login.html`, scored a false CRASH and fuzzed nothing more (seed 204262061: 3 observations, 1 crash-class; 24 and 0 with the label excluded). The default now also covers Dutch, German, French, Spanish, Portuguese and Italian sign-out captions, matched as the whole caption so ordinary verbs stay fuzzable ("Ziek afmelden", "Vom Kurs abmelden", and sign-in captions such as "Aanmelden"); a project adds its own in `project.config.js` `signOutPattern` or `SIGN_OUT_PATTERN`, added to the default, never replacing it, with its own regex flags kept. `rapidTab` no longer presses Enter while focus is on a sign-out control (top bar, nav item, aria-label or title, each tested on its own), the route by which a layout control outside `.mx-page` was reached. `test-monkey-left-app.sh` gains the issue's captions, sign-in near-misses, the over-match captions, the project pattern and its flags, and a rapidTab guard scoped to that branch (it fails on a blind-Enter mutation): 8 FAIL on master, 22/22 with the fix. — jopterhorst + ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/project-tests/e2e/monkey.js b/project-tests/e2e/monkey.js index 65c1dd7e..8bb2263f 100644 --- a/project-tests/e2e/monkey.js +++ b/project-tests/e2e/monkey.js @@ -160,19 +160,27 @@ async function typeSafely(page, handle, text) { // the label was excluded). The default covers the common captions of the languages Mendix apps // ship in most; anything else goes in project.config.js `signOutPattern` (a RegExp or a regex // string) or the SIGN_OUT_PATTERN env var, and is ADDED to the default, never a replacement. -const SIGN_OUT_DEFAULT = new RegExp('\\b(?:' + [ - '(?:sign|log)\\s*(?:out|off)', // en: Sign out, Log off, Logout - 'afmelden', 'uitloggen', // nl - 'abmelden', 'ausloggen', // de - 'se\\s+d[ée]connecter', 'd[ée]connexion', // fr - 'cerrar\\s+sesi[óo]n', // es - '(?:terminar|encerrar)\\s+sess[ãa]o', // pt - 'disconnetti', // it -].join('|') + ')\\b', 'i'); +// +// The non-English words match the WHOLE caption only. They are also ordinary verbs, so a word +// match left "Ziek afmelden" (report sick), "Afmelden nieuwsbrief", "Vom Kurs abmelden" and +// "Disconnetti dispositivo" unfuzzed. The English phrase keeps its word match, as before. +const SIGN_OUT_DEFAULT = new RegExp( + '\\b(?:sign|log)\\s*(?:out|off)\\b' + // en: Sign out, Log off, Logout + '|^\\s*(?:' + [ + 'afmelden', 'uitloggen', // nl + 'abmelden', 'ausloggen', // de + 'se\\s+d[ée]connecter', 'd[ée]connexion', // fr + 'cerrar\\s+sesi[óo]n', // es + '(?:terminar|encerrar)\\s+sess[ãa]o', // pt + 'disconnetti', // it + ].join('|') + ')\\s*$', 'i'); +// A project RegExp keeps its own flags (m, s, u, …), merged with the default's `i`; `g` and `y` +// are dropped because they make .test() stateful across calls. function signOutPattern(extra) { if (!extra) return SIGN_OUT_DEFAULT; const src = extra instanceof RegExp ? extra.source : String(extra); - try { return new RegExp(`(?:${SIGN_OUT_DEFAULT.source})|(?:${src})`, 'i'); } + const flags = [...new Set(('i' + (extra instanceof RegExp ? extra.flags : '')).replace(/[gy]/g, ''))].join(''); + try { return new RegExp(`(?:${SIGN_OUT_DEFAULT.source})|(?:${src})`, flags); } catch (e) { console.log(` ⚠ signOutPattern ${JSON.stringify(src)} is not a valid regex (${e.message}) — using the built-in sign-out captions only`); return SIGN_OUT_DEFAULT; @@ -182,13 +190,15 @@ const SIGN_OUT = signOutPattern(cfg.signOutPattern); // The keyboard can reach controls the button locator never offers: the layout's top bar, a // navigation item, an icon-only button whose caption is its aria-label or title. async function focusIsSignOut(page) { - const label = await page.evaluate(() => { + const labels = await page.evaluate(() => { const e = document.activeElement; - if (!e || e === document.body) return ''; - return [e.innerText || e.textContent || '', e.getAttribute('aria-label') || '', e.getAttribute('title') || ''].join(' | '); - }).catch(() => ''); - return SIGN_OUT.test(label); + if (!e || e === document.body) return []; + return [e.innerText || e.textContent || '', e.getAttribute('aria-label') || '', e.getAttribute('title') || '']; + }).catch(() => []); + return isSignOutLabel(labels); } +// Each label on its own: the non-English captions are anchored to the whole caption. +function isSignOutLabel(labels) { return labels.some(t => t && SIGN_OUT.test(t)); } function leftApp(url, baseUrl) { const base = String(baseUrl).replace(/\/+$/, ''); if (!url || (url !== base && !url.startsWith(base + '/'))) return true; @@ -262,7 +272,7 @@ async function round(page, target, bucket) { } // leftApp is exported for tests/wave2/test-monkey-left-app.sh; requiring this file runs nothing. -module.exports = { leftApp, SIGN_OUT, SIGN_OUT_DEFAULT, signOutPattern }; +module.exports = { leftApp, SIGN_OUT, SIGN_OUT_DEFAULT, signOutPattern, isSignOutLabel }; // ── main ──────────────────────────────────────────────────────────────────── if (require.main === module) (async () => { diff --git a/tests/wave2/test-monkey-left-app.sh b/tests/wave2/test-monkey-left-app.sh index 37fa1645..2c31aa4c 100755 --- a/tests/wave2/test-monkey-left-app.sh +++ b/tests/wave2/test-monkey-left-app.sh @@ -72,6 +72,13 @@ X('cfgstr', '\\bkirjaudu ulos\\b', 'Kirjaudu ulos'); X('cfgkeeps', /\bkirjaudu ulos\b/i, 'Afmelden'); X('cfgbad', '(', 'Afmelden'); X('cfgnone', null, 'Kirjaudu ulos'); +// Review of #481: ordinary verbs inside a longer caption are not sign-out controls. +['Ziek afmelden', 'Afmelden nieuwsbrief', 'Vom Kurs abmelden', 'Disconnetti dispositivo', ' Afmelden '].forEach(S); +X('cfgflags', /^kirjaudu ulos$/m, 'Valikko\nKirjaudu ulos'); +const I = (k, labels) => console.log(`I:${k} ${typeof M.isSignOutLabel === 'function' ? M.isSignOutLabel(labels) : 'NOFN'}`); +I('aria', ['', 'Afmelden', '']); +I('title', ['\ue000', '', 'Uitloggen']); +I('none', ['Ziek afmelden', 'Menu', '']); process.exit(0); EOF OUT="$(cd "$WORK/tests/e2e" && printf '%s' "$JS" | APP_PORT=1 timeout 30 node - 2>&1)" @@ -93,7 +100,7 @@ fi [ "$(line OTHER)" = true ] && ok "a lookalike origin (port prefix match) is outside the app" || bad "prefix match leaks another origin in" "$(line OTHER)" [ "$(line SLASHBASE)" = false ] && ok "a baseUrl with a trailing slash still matches its routes" || bad "trailing-slash baseUrl breaks the match" "$(line SLASHBASE)" -s() { printf '%s\n' "$OUT" | sed -n "s/^S:$1 //p"; } +s() { printf '%s\n' "$OUT" | K="S:$1" awk '{b=$NF; sub(/ [^ ]*$/, ""); if ($0 == ENVIRON["K"]) print b}'; } # exact label: "Afmelden" must not read "Afmelden nieuwsbrief" [ "$(s 'Sign out')" = true ] && [ "$(s 'Log off')" = true ] && [ "$(s 'Logout')" = true ] \ && ok "SIGN_OUT matches Sign out (captured), Log off, Logout" || bad "a sign-out label is not excluded" "$(s 'Sign out')/$(s 'Log off')/$(s 'Logout')" [ "$(s 'Switch theme')" = false ] && [ "$(s 'Signature')" = false ] && [ "$(s 'Log in')" = false ] \ @@ -112,10 +119,20 @@ x() { printf '%s\n' "$OUT" | sed -n "s/^X:$1 //p"; } [ "$(x cfgkeeps)" = true ] && ok "a project pattern adds to the default, it does not replace it" || bad "project pattern dropped the default" "$(x cfgkeeps)" [ "$(x cfgbad)" = true ] && [ "$(x cfgnone)" = false ] && ok "an invalid project pattern falls back to the default; none means default only" \ || bad "invalid/absent project pattern mishandled" "$(x cfgbad)/$(x cfgnone)" +[ "$(s 'Ziek afmelden')" = false ] && [ "$(s 'Afmelden nieuwsbrief')" = false ] && [ "$(s 'Vom Kurs abmelden')" = false ] && [ "$(s 'Disconnetti dispositivo')" = false ] \ + && ok "a non-English verb inside a longer caption stays fuzzable (Ziek afmelden, Afmelden nieuwsbrief, Vom Kurs abmelden, Disconnetti dispositivo)" \ + || bad "SIGN_OUT over-matches an ordinary caption" "$(s 'Ziek afmelden')/$(s 'Afmelden nieuwsbrief')/$(s 'Vom Kurs abmelden')/$(s 'Disconnetti dispositivo')" +[ "$(s ' Afmelden ')" = true ] && ok "a whole caption with surrounding whitespace still matches" || bad "padded caption missed" "$(s ' Afmelden ')" +[ "$(x cfgflags)" = true ] && ok "a project RegExp keeps its own flags (m)" || bad "project RegExp flags dropped" "$(x cfgflags)" +i() { printf '%s\n' "$OUT" | sed -n "s/^I:$1 //p"; } +[ "$(i aria)" = true ] && [ "$(i title)" = true ] && [ "$(i none)" = false ] \ + && ok "focus labels (text, aria-label, title) are tested one by one" || bad "focus label test wrong" "$(i aria)/$(i title)/$(i none)" grep -q 'signOutPattern: *process.env.SIGN_OUT_PATTERN' "$E2E/config.js" && grep -q '^ *signOutPattern:' "$CFG" \ && ok "config.js reads signOutPattern (env, then project.config) and the template declares it" \ || bad "signOutPattern is not plumbed through config.js / the template (a consumer with no producer)" -awk '/rapidTab/ {r=1} r && /focusIsSignOut\(page\)/ {f=1} f && /keyboard\.press\(.Enter.\)/ {e=1} END{exit !e}' "$SUT" \ +# Scoped to the rapidTab branch itself: the ABUSES list and focusIsSignOut's definition also +# mention these names, and an unscoped match passed a blind Enter (review of #481). +awk '/abuse === .rapidTab./{r=1;next} r&&/else if \(abuse ===/{r=0} r&&/focusIsSignOut\(page\)/{f=1} r&&/press\(.Enter.\)/{if(!f)bad=1; e=1} END{exit !(e&&!bad)}' "$SUT" \ && ok "rapidTab checks focus for a sign-out control before Enter" || bad "rapidTab presses Enter without checking what has focus (#469)" # The tested functions must be the ones the rounds use. From 22ac22e98a10075ca71d779a49295148081fe3c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:52:14 +0000 Subject: [PATCH 09/12] fix(page-fidelity): switch to the dialog boundary only with popup evidence (#471 review) .dialog is the design system's own component, so any class-word .dialog turned a full-page wireframe with an inline confirm into that confirm (39% on master, 5% on the first cut). The dialog boundary now needs a .popup-stage or .dialog-scrim in the wireframe, or a popup layout in the page MDL; otherwise the old boundary list applies unchanged. Fixture 78/78: 8 FAIL on master, 1 (the inline-confirm regression) on the previous head. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 3 +- project-bin/page-fidelity.js | 37 ++++++++++++++++--------- tests/wave2/test-page-fidelity-mocks.sh | 17 +++++++++++- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7622d7a7..33f99d9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,8 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased -- fix(page-fidelity): **a popup wireframe is scored against its dialog, not the page drawn behind it** (#471). A popup wireframe draws the caller's page behind a scrim (`.popup-stage > .shell-behind + .dialog-scrim > .dialog`); `page-fidelity.js` looked for `div.main` first and landed in the shell, so a popup whose fields and bindings all matched scored 29% in the field, every miss being the caller's page head, h1 and card. A wireframe with a `.dialog` (class word, so `dialog-scrim` does not count) now scores the dialog only; a `.popup-stage` without one scores the stage minus `.shell-behind`; every other wireframe keeps its boundary. `test-page-fidelity-mocks.sh` gains the popup cases on a verbatim v0.23.0 describe of a real popup page and a wireframe drawn in the issue's shape: 18% and 6 FAIL on master, 100% and 75/75 with the fix. — jopterhorst +- fix(page-fidelity): **a popup wireframe is scored against its dialog, not the page drawn behind it** (#471). A popup wireframe draws the caller's page behind a scrim (`.popup-stage > .shell-behind + .dialog-scrim > .dialog`); `page-fidelity.js` looked for `div.main` first and landed in the shell, so a popup whose fields and bindings all matched scored 29% in the field, every miss being the caller's page head, h1 and card. With popup evidence (a `.popup-stage` or `.dialog-scrim` in the wireframe, or a page whose MDL names a popup layout) the scorer now scores the `.dialog` only (class word, so `dialog-head` does not count), or the stage minus `.shell-behind` when there is no `.dialog`. Without that evidence nothing changes, so a full-page wireframe that draws an inline `.dialog` confirm (the design system's own component) scores exactly as before, and a bare-`.dialog` popup wireframe still reads ``. `test-page-fidelity-mocks.sh` gains the popup cases on a verbatim v0.23.0 describe of a real popup page and a wireframe drawn in the issue's shape, plus layout-only evidence and the full-page-plus-inline-confirm regression case: 18% and 8 FAIL on master, 100% and 78/78 with the fix. — jopterhorst + ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/project-bin/page-fidelity.js b/project-bin/page-fidelity.js index 36eab7fc..acda5f49 100755 --- a/project-bin/page-fidelity.js +++ b/project-bin/page-fidelity.js @@ -143,7 +143,7 @@ function innerBalanced(html, openRe) { return html.slice(open, end); } -function contentOf(html) { +function contentOf(html, popupLayout) { // Boundary preference, most-specific first: //
    ToeicBuddy shape — content-only wireframes // div.main the topbar portal full-page shape — the wireframe mocks the WHOLE app @@ -161,17 +161,27 @@ function contentOf(html) { // 06/07 .mockup-frame. Measured, not assumed — every one of them // fell through to before this list existed. // div.content same shape when the wireframe has no .main wrapper - // popup/dialog wireframes with annotation siblings - // then strip annotation chrome either way. + // popup/dialog wireframes with annotation siblings — a bare `.dialog` + // popup with no stage and no scrim still lands here, as it always did + // then strip annotation chrome either way. Ahead of the whole list, only with popup + // evidence: the dialog itself (see POPUP below). // - // POPUP FIRST (#471). A popup wireframe draws the CALLER's page behind a scrim for context: - // `.popup-stage > .shell-behind + .dialog-scrim > .dialog`. The shell behind carries its own - // div.main, so the list below landed on the caller's page and scored the popup against it: - // 29% in the field for a popup whose fields and bindings all matched, every miss in the - // shell. When the wireframe draws a `.dialog`, the dialog is the page; the shell is not - // scored at all. Matched on the class WORD, so `dialog-scrim`/`dialog-head` do not count. + // POPUP, ONLY WITH POPUP EVIDENCE (#471). A popup wireframe draws the CALLER's page behind a + // scrim for context: `.popup-stage > .shell-behind + .dialog-scrim > .dialog`. The shell + // behind carries its own div.main, so the list above landed on the caller's page and scored + // the popup against it: 29% in the field for a popup whose fields and bindings all matched, + // every miss in the shell. So when the wireframe is a popup, the dialog is the page. + // + // But `.dialog` alone is not evidence: it is the toolkit's own design-system component, and + // a full-page wireframe may draw an inline confirm with it. Switching on any `.dialog` scored + // a grouped overview plus a confirm as the confirm alone (39% on master, 5% on the first cut + // of this fix; review of #482). Evidence is a `.popup-stage` or `.dialog-scrim` in the + // wireframe, or a page whose MDL names a popup layout (`Layout: …Popup…`, the same test the + // chrome set uses). Matched on the class WORD, so `dialog-head`/`dialog-foot` never count. const cls = name => new RegExp(`<(div|section|dialog|aside)\\b[^>]*class="(?:[^"]*\\s)?${name}(?:\\s[^"]*)?"`, 'i'); - const popup = innerBalanced(html, cls('dialog')) + const popupEvidence = !!popupLayout || cls('popup-stage').test(html) || cls('dialog-scrim').test(html); + const popup = !popupEvidence ? null + : innerBalanced(html, cls('dialog')) || (() => { // a stage with no .dialog: everything but the shell const stage = innerBalanced(html, cls('popup-stage')); return stage === null ? null : dropBalanced(stage, cls('shell-behind')); @@ -332,7 +342,7 @@ function bindRows(html) { return rows; } -function wfFacts(html) { +function wfFacts(html, popupLayout) { // HTML COMMENTS ARE NOT PAGE. contentOf() picks its boundary with a regex, and a template // whose header comment says "put the screen inside
    … the bind table goes AFTER //
    " handed it the comment text as the page. Field case, 2026-10-02, two builds @@ -341,7 +351,7 @@ function wfFacts(html) { // neither describing the page. Strip comments before anything reads the document. html = html.replace(//g, ' '); const mock = localMockClasses(html); - let main = contentOf(html); + let main = contentOf(html, popupLayout); const mockUsed = []; const structural = []; for (const c of mock) { @@ -753,9 +763,10 @@ function score(wf, mdl, chromeExtra) { return { pct: den ? Math.round(100 * num / den) : null, h, b, k, c, bd }; } -const wf = wfFacts(readWireframe()); const ownMdl = pageMdl(); if (!ownMdl.trim()) { console.error('page-fidelity: no declaration of page "' + PAGE + '" found in input'); process.exit(2); } +// Read first: a popup layout is evidence that a `.dialog` in the wireframe is the page (#471). +const wf = wfFacts(readWireframe(), /Layout:\s*[^,)\n]*Popup/i.test(ownMdl)); const snips = expandSnippets(ownMdl); const mdl = ownMdl + snips.extra; const lay = layoutChrome(SRCS, ownMdl); diff --git a/tests/wave2/test-page-fidelity-mocks.sh b/tests/wave2/test-page-fidelity-mocks.sh index 47e8bca7..a4676d87 100755 --- a/tests/wave2/test-page-fidelity-mocks.sh +++ b/tests/wave2/test-page-fidelity-mocks.sh @@ -264,7 +264,22 @@ hasnt "a .popup-stage with no .dialog still drops the shell" "$OUT21" "heading: has "...and still scores what is in front of it" "$OUT21" "headings 1/1" sed 's/class="dialog" /class="panel" /; s/class="popup-stage"/class="stage"/' "$PF/product-newedit-popup.html" > "$TMP/no-popup.html" OUT22=$(cd "$TMP" && node "$SUT" --no-log "$TMP/no-popup.html" Product_NewEdit "$PF/product-newedit-describe-v023.mdl" 2>&1) -has "without .dialog/.popup-stage, dialog-scrim alone does not switch boundary (div.main, as before)" "$OUT22" "heading: Product catalog" +has "popup evidence but no .dialog and no stage: the old div.main boundary, as before" "$OUT22" "heading: Product catalog" +# Popup evidence from the page alone: no stage, no scrim, but the MDL names Atlas_Core.PopupLayout. +sed 's/class="popup-stage"/class="stage"/; s/class="dialog-scrim"/class="scrim"/' "$PF/product-newedit-popup.html" > "$TMP/layout-only.html" +OUT23=$(cd "$TMP" && node "$SUT" --no-log "$TMP/layout-only.html" Product_NewEdit "$PF/product-newedit-describe-v023.mdl" 2>&1) +hasnt "a popup layout in the MDL is evidence enough: the shell is not scored" "$OUT23" "heading: Product catalog" +has "...and the dialog is" "$OUT23" "text-match 100%" +echo " -- a full-page wireframe with an inline .dialog scores as before (review of #482)" +# .dialog is the design system's component; a full page may draw an open confirm with it. On +# master this wireframe scored 39% (the confirm counted as page content); the first cut of #471 +# scored the confirm alone, 5%. No stage, no scrim, a full-page layout: no popup evidence. +sed 's#
    #&#' \ + "$FIX/grouped-overview.html" > "$TMP/inline-dialog.html" +grep -q 'Delete project' "$TMP/inline-dialog.html" || bad "inline-dialog setup: the sed did not insert the confirm" "" +OUT24=$(cd "$TMP" && node "$SUT" --no-log "$TMP/inline-dialog.html" Demo_Overview "$FIX/demo-overview.mdl" 2>&1) +echo "$OUT24" | sed 's/^/ | /' +has "an inline confirm does not become the boundary: the master score, unchanged" "$OUT24" "text-match 39% headings 1/2 actions 1/3 content 1/2 classes 1/5" echo printf 'test-page-fidelity-mocks: %d passed, %d failed\n' "$PASS" "$FAIL" From 1aa6aebc1c7365fd18732f359b582c2a915092f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 12:00:58 +0000 Subject: [PATCH 10/12] fix(progress-report): review fixes for #486 (scroll margin, unknown stage, dead rule, sync note) - #stages and #build-detail get the 56px scroll margin, so the nav targets no longer land under the sticky bar; asserted in polish P7. - An unreadable current stage renders plain "Current stage: unknown" instead of a link to #stage-P; asserted in polish P7. - Drop the dead .snav b{flex:0 0 auto} phone rule. - CHANGELOG: a project with a locally edited bin/build-plan-status.sh keeps the old id=stamp filter after sync and rewrites the page on every refresh; it needs sync-project.sh --only-bin build-plan-status.sh --upgrade-bin build-plan-status.sh alongside this. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 2 +- project-bin/progress-report.py | 9 +++++---- tests/wave2/test-progress-report-polish.sh | 6 ++++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa0d6487..ce2ff135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased -- fix(progress-report): **the top bar is one group of section links (Stages, Build, Pages, Tips) instead of stage chips plus links; the page carries one `id=stamp`, and the header drops the build-plan.html note.** The sticky bar loses the `Stage P 0 … 6` chips, which repeated the stage list right below it, and gains a `Stages` link to that list; the `#stage-N` card anchors stay for links from other pages, and the header now states the current stage. Five elements shared `id=stamp` (invalid HTML); the "Last updated" span keeps it and the others get their own ids plus a `data-stamp` marker, which `build-plan-status.sh --refresh` now reads (with `id=stamp`, so pages written before this still compare) to leave an unchanged page alone. "replaces the former build-plan.html" is gone from the header sub-line. Fixes #479 items 1–3 — MendixMau (review 2026-10-20) +- fix(progress-report): **the top bar is one group of section links (Stages, Build, Pages, Tips) instead of stage chips plus links; the page carries one `id=stamp`, and the header drops the build-plan.html note.** The sticky bar loses the `Stage P 0 … 6` chips, which repeated the stage list right below it, and gains a `Stages` link to that list; the `#stage-N` card anchors stay for links from other pages, and the header now states the current stage. Five elements shared `id=stamp` (invalid HTML); the "Last updated" span keeps it and the others get their own ids plus a `data-stamp` marker, which `build-plan-status.sh --refresh` now reads (with `id=stamp`, so pages written before this still compare) to leave an unchanged page alone. The filter now depends on both scripts: a project whose `bin/build-plan-status.sh` is locally edited keeps the old `id=stamp` filter after sync, so the page is rewritten on every refresh; such a project needs `sync-project.sh --only-bin build-plan-status.sh --upgrade-bin build-plan-status.sh` alongside this. "replaces the former build-plan.html" is gone from the header sub-line. Fixes #479 items 1–3 — MendixMau (review 2026-10-20) - fix(progress-report): **the build plan is shown once: the phase cards are the plan.** Expanding a card now shows its full rows (#, Kind, Step, Produces/Proves, Depends on, State, Scripts) and Claims, which only the duplicate "Full plan tables" block carried; that block is gone and views A (script folders) and B (module proof) are their own section. The `now` phase renders open with a selected style, opening another phase closes the previous one (accordion; `#phase-N` links still open their card), and a viewer's saved open/closed state survives the 30 s reload and wins over the default (#474) — field feedback from a requirements-driven project at Stage 5 — MendixMau (field feedback 2026-10-20) ## v2026.10.19 diff --git a/project-bin/progress-report.py b/project-bin/progress-report.py index 72aac68a..5e27a503 100755 --- a/project-bin/progress-report.py +++ b/project-bin/progress-report.py @@ -1433,7 +1433,7 @@ def rule(ratio, kind, title, seen, matters, fix, why, lost_s=0.0, lost_tok=0.0, .stale{color:var(--warn)}.r-partial td:last-child{color:var(--warn)}.r-built td:last-child{color:var(--ok)} .stack{display:flex;height:16px;border-radius:5px;overflow:hidden;background:var(--idle)}.stack>i{display:block;height:100%} .note{font-size:12px;color:var(--mut);margin:4px 0} -html{scroll-behavior:smooth}[id^=stage-],[id^=agents-],[id^=phase-],#tips,#pages{scroll-margin-top:56px} +html{scroll-behavior:smooth}[id^=stage-],[id^=agents-],[id^=phase-],#stages,#build-detail,#tips,#pages{scroll-margin-top:56px} .snav{position:sticky;top:0;z-index:20;background:var(--bg);border-bottom:1px solid var(--line);padding:8px 16px;display:flex;gap:6px;align-items:center;flex-wrap:wrap} .snav a{min-width:34px;text-align:center;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid var(--line);font-weight:650;font-size:13px;color:var(--ink)} .snav a:hover{text-decoration:none;border-color:var(--accent)} @@ -1452,7 +1452,7 @@ def rule(ratio, kind, title, seen, matters, fix, why, lost_s=0.0, lost_tok=0.0, .pages .tree{margin-top:6px;font-size:13px}.pg{display:grid;grid-template-columns:minmax(120px,220px) minmax(0,1fr);gap:2px 12px;padding:5px 0;border-bottom:1px solid var(--line)}.pg:last-child{border-bottom:0} .pg .dir{font-weight:600;overflow-wrap:anywhere}.pg ul{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:3px 14px}.pg li{overflow-wrap:anywhere}.pg details{margin:0}.pg details ul{margin-top:4px} footer{margin-top:36px;font-size:13px;color:var(--mut)}footer li{margin:3px 0} -@media(max-width:520px){code,pre,.stage dd,.stage dt{overflow-wrap:anywhere;min-width:0}.stage dl{grid-template-columns:minmax(0,1fr)}.stage dd{text-align:left}.hd .sub{white-space:normal;flex:1 1 100%}.snav{padding:6px 12px;flex-wrap:nowrap;overflow-x:auto;max-width:100%}.snav a{flex:0 0 auto}.snav b{flex:0 0 auto}.updated{max-width:100%;overflow-wrap:anywhere}.advl{grid-template-columns:minmax(0,1fr)}.pg{grid-template-columns:minmax(0,1fr)}.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.rail{grid-template-columns:minmax(0,1fr)}main{padding:12px}h1{font-size:20px}.kpi .v{font-size:19px}} +@media(max-width:520px){code,pre,.stage dd,.stage dt{overflow-wrap:anywhere;min-width:0}.stage dl{grid-template-columns:minmax(0,1fr)}.stage dd{text-align:left}.hd .sub{white-space:normal;flex:1 1 100%}.snav{padding:6px 12px;flex-wrap:nowrap;overflow-x:auto;max-width:100%}.snav a{flex:0 0 auto}.updated{max-width:100%;overflow-wrap:anywhere}.advl{grid-template-columns:minmax(0,1fr)}.pg{grid-template-columns:minmax(0,1fr)}.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.rail{grid-template-columns:minmax(0,1fr)}main{padding:12px}h1{font-size:20px}.kpi .v{font-size:19px}} """ KC = {"BRIEF": "#8b5cf6", "BUILD": "#2f6fed", "PROVE": "#1f9d63", "HARNESS": "#d98a0b", "RUN": "#c93c3c"} @@ -1523,8 +1523,9 @@ def money(c): return "n/a" if unp else "$%.0f" % c w("

    Progress report: %s

    " % esc(info["name"])) w("Last updated %s %s session log last event %s%s
    " % (gen, datetime.datetime.now().astimezone().strftime("%Z"), last_ev, " · Live · refreshes every 30 s · Pause" if args.reload else "")) - w("
    Current stage: %s · Stage P to 6 · entry mode %s · PROJECT.md · gate-check page
    " % - (info["current"] or "P", esc(("Stage %s: %s" % (info["current"], STAGE_NAME[info["current"]])) if info["current"] in STAGE_NAME else "unknown"), esc(info["entry_mode"]), href(os.path.join(proj, "PROJECT.md")), href(os.path.join(proj, "index.html")))) + nowstage = ("Stage %s: %s" % (info["current"], info["current"], esc(STAGE_NAME[info["current"]]))) if info["current"] in STAGE_NAME else "unknown" + w("
    Current stage: %s · Stage P to 6 · entry mode %s · PROJECT.md · gate-check page
    " % + (nowstage, esc(info["entry_mode"]), href(os.path.join(proj, "PROJECT.md")), href(os.path.join(proj, "index.html")))) how_parts = [] if L and DISCOVERY.get("mention") and not args.session_log: rows = "".join("%s%s%d%%" % (esc(u["id"]), datetime.datetime.fromtimestamp(u["t"]).strftime("%Y-%m-%d"), round(u["share"] * 100)) for u in sorted(DISCOVERY.get("used", []), key=lambda u: u["t"])) diff --git a/tests/wave2/test-progress-report-polish.sh b/tests/wave2/test-progress-report-polish.sh index 08bee57a..4773e87c 100644 --- a/tests/wave2/test-progress-report-polish.sh +++ b/tests/wave2/test-progress-report-polish.sh @@ -169,6 +169,9 @@ assert not re.search(r"href='#stage-", nav) and "" not in nav, nav assert "id=stages" in h and h.index("id=stages") < h.index("id='stage-P'"), "Stages target sits above the stage cards" for s_ in ["P", "0", "1", "2", "3", "4", "5", "6"]: assert "id='stage-%s'" % s_ in h, s_ assert ".snav{position:sticky;top:0" in gen and ".snav{padding:6px 12px;flex-wrap:nowrap;overflow-x:auto" in gen +sm = re.search(r"([^{}]*)\{scroll-margin-top:56px\}", gen).group(1).split(",") +assert "#stages" in sm and "#build-detail" in sm, sm # nav targets clear the sticky bar +assert ".snav b{" not in gen, "no rule for the removed Stage label" head = h[h.index("
    "):h.index("
    ")] assert re.search(r"Current stage: Stage 5: [^<]+", head), "current stage in the header" assert "replaces the former build-plan.html" not in head @@ -182,6 +185,9 @@ for marker in ("Last updated", "session log last event", "Last build step", "Lat assert "id=livetxt" in st[0] and "id=livebtn" in st[0], "autoreload's live controls ride on the stamp line" PYEOF7 hasnt "$R7" "replaces the former build-plan.html" "the old build-plan.html wording is gone from the page header" +P7u="$WORK/p7u"; mkproj "$P7u" "**Somewhere in the middle**, no stage named" +CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P7u" --html >/dev/null 2>&1 +has "$P7u/architecture/progress-report.html" "Current stage: unknown" "an unreadable current stage reads plain 'unknown', no link to #stage-P" # --refresh still leaves an unchanged page alone: the volatile lines are found by id=stamp or data-stamp SUM7="$(cksum < "$R7")"; sleep 1 CLAUDE_CONFIG_DIR="$EMPTY" bash "$SCRIPT" "$P7" --refresh >/dev/null 2>&1 From bab64c52631dd6a98eb374ba4239f83d4cd57a83 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 12:07:36 +0000 Subject: [PATCH 11/12] fix(monkey): correct the master FAIL count to 9 (#481 review) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caf24d3b..577914c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased -- fix(monkey): **the monkey no longer signs itself out of a non-English app** (#469). `monkey.js` recognised only English sign-out captions, so in a Dutch app it clicked "Afmelden", landed on `login.html`, scored a false CRASH and fuzzed nothing more (seed 204262061: 3 observations, 1 crash-class; 24 and 0 with the label excluded). The default now also covers Dutch, German, French, Spanish, Portuguese and Italian sign-out captions, matched as the whole caption so ordinary verbs stay fuzzable ("Ziek afmelden", "Vom Kurs abmelden", and sign-in captions such as "Aanmelden"); a project adds its own in `project.config.js` `signOutPattern` or `SIGN_OUT_PATTERN`, added to the default, never replacing it, with its own regex flags kept. `rapidTab` no longer presses Enter while focus is on a sign-out control (top bar, nav item, aria-label or title, each tested on its own), the route by which a layout control outside `.mx-page` was reached. `test-monkey-left-app.sh` gains the issue's captions, sign-in near-misses, the over-match captions, the project pattern and its flags, and a rapidTab guard scoped to that branch (it fails on a blind-Enter mutation): 8 FAIL on master, 22/22 with the fix. — jopterhorst +- fix(monkey): **the monkey no longer signs itself out of a non-English app** (#469). `monkey.js` recognised only English sign-out captions, so in a Dutch app it clicked "Afmelden", landed on `login.html`, scored a false CRASH and fuzzed nothing more (seed 204262061: 3 observations, 1 crash-class; 24 and 0 with the label excluded). The default now also covers Dutch, German, French, Spanish, Portuguese and Italian sign-out captions, matched as the whole caption so ordinary verbs stay fuzzable ("Ziek afmelden", "Vom Kurs abmelden", and sign-in captions such as "Aanmelden"); a project adds its own in `project.config.js` `signOutPattern` or `SIGN_OUT_PATTERN`, added to the default, never replacing it, with its own regex flags kept. `rapidTab` no longer presses Enter while focus is on a sign-out control (top bar, nav item, aria-label or title, each tested on its own), the route by which a layout control outside `.mx-page` was reached. `test-monkey-left-app.sh` gains the issue's captions, sign-in near-misses, the over-match captions, the project pattern and its flags, and a rapidTab guard scoped to that branch (it fails on a blind-Enter mutation): 9 FAIL on master, 22/22 with the fix. — jopterhorst - fix(progress-report): **the build plan is shown once: the phase cards are the plan.** Expanding a card now shows its full rows (#, Kind, Step, Produces/Proves, Depends on, State, Scripts) and Claims, which only the duplicate "Full plan tables" block carried; that block is gone and views A (script folders) and B (module proof) are their own section. The `now` phase renders open with a selected style, opening another phase closes the previous one (accordion; `#phase-N` links still open their card), and a viewer's saved open/closed state survives the 30 s reload and wins over the default (#474) — field feedback from a requirements-driven project at Stage 5 — MendixMau (field feedback 2026-10-20) ## v2026.10.19 From 6605557fed27fafc142bb31f5f9181f7a93370be Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 12:11:46 +0000 Subject: [PATCH 12/12] fix(portability): anchor the sed/grep tab lint to the command word (#485 review) The lint matched "sed" inside words such as "closed", "parsed" or "used", so an awk regex after one of them would turn CI red. Anchored as (^|[^[:alnum:]_])(sed|grep)([[:space:]]|$); verified it still flags the true sed/grep sites and not the two awk probes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VJgWP5vEoAsNsJYqCDMGNw --- bin/check-portability.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/check-portability.sh b/bin/check-portability.sh index 2802a115..701c3170 100755 --- a/bin/check-portability.sh +++ b/bin/check-portability.sh @@ -165,7 +165,7 @@ while IFS= read -r f; do # string holds a real tab and is skipped. case "$txt" in *sed*'\t'*|*grep*'\t'*) - if grep -Eq -- '(sed|grep)[^|]*\[[^]]*\\t' <<<"$txt" \ + if grep -Eq -- '(^|[^[:alnum:]_])(sed|grep)([[:space:]]|$)[^|]*\[[^]]*\\t' <<<"$txt" \ && ! grep -Eq -- "\\\$'[^']*\\[[^]]*\\\\t" <<<"$txt"; then report "$f" "$ln" "\\t inside a sed/grep bracket expression (BSD: backslash or t, not tab)" \ 'Use [[:blank:]] (space or tab) or [[:space:]], or a real tab via $'"'"'\t'"'"'.'