Skip to content

Commit 6648935

Browse files
MendixMauclaude
andauthored
Browser lane: right journeys selected, LOOK from journey screenshots, page-scope producer, no session leaks (#588)
affected-tests.sh selects journeys by the widgets of changed pages and their callers (all journeys on navigation/layout or when unsure); look-ledger.sh shots maps journey screenshots to pages for the LOOK; page-audit and design-audit regenerate a missing or stale page-scope.json; page-audit, design-audit and monkey log out. Review: MERGE-AFTER-FIX — ui-loop.md rule sentence restored (re-reviewed MERGE); harness-scan engine list gains scope-ensure.js (reviewed MERGE) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr
1 parent 95e1b30 commit 6648935

31 files changed

Lines changed: 1384 additions & 22 deletions

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m
1919

2020
## Unreleased
2121

22+
- fix(page-audit, design-audit, monkey, ui-loop): **no more leaked Mendix sessions from the toolkit's own browser helpers**: all three logged in and closed the browser without the xas logout, so each run burned a seat of a trial licence's cap and later journeys failed login as "Sign in failed"; they now log out first, and `ui-loop.md` says a hand-written screenshot script must too. Field: System.Session count +1 per run on master (page-audit 0 -> 1, design-audit 1 -> 2, monkey 2 -> 3), +0 with the fix — speed experiment E2
23+
- fix(page-audit, design-audit): **the audits produce their own `.claude/loop/page-scope.json`**: when it is missing or older than the model they run `bin/page-scope.sh` (`scope-ensure.js`, 2 s) instead of FAULTing on 0 pages; a failed regeneration never blanks an existing file and names the failure. Field: scope deleted -> page-audit ran on 9 pages, `touch` of the .mpr -> regenerated once — speed experiment E2
24+
- new(look-ledger, journey-runner, module-review): **a journey run's screenshots are LOOK evidence**: `bin/look-ledger.sh shots` (run by journey-runner after every run) maps each reached step's PNG to the one page whose widgets (`DESCRIBE PAGE`) it landed on, a URL pattern breaking ties, and lists every owed page as `OPEN` (open that PNG; the Read hook then marks the page SEEN) or `UNREACHED` (short gap-fill journey, or `page-audit.js` as before) with the line `owed N = A OPEN + B UNREACHED`; a shot older than the page's last build is not evidence and nothing is claimed when unsure. Field: 8 owed pages = 5 OPEN + 3 UNREACHED from two journey runs, no separate screenshot pass; baseline words 64,004 -> 64,193 — speed experiment E2
25+
- fix(affected-tests): **a page, flow or navigation change no longer selects 0 journeys because the journeys name widgets, not `Module.Name`**: the affected scope now reads the widget names of every changed page/snippet and of each calling page (`DESCRIBE PAGE`, one extra warm-catalog `mxcli` call, 0.07 s) and selects each journey whose JSON holds one; a navigation or layout change selects every journey; when callers or widgets cannot be measured (`widgets NOT MEASURED` row) it selects every journey (unsure means include). Field: 067 (Request_Edit page) 0 -> J-REQ-01, 090 (navigation) 0 -> all 5 journeys — speed experiment E2
2226
- process(design-artifacts, conversion-runbook): **Stage 3 runs in parallel by default, from one brief and a mechanical task list**: the lead fixes modules, canonical names and design tokens, writes `docs/progress/stage3-brief.md`, and takes the tasks from the stage-3 lines of `bin/lib/artifact-check.sh` (so no artifact is forgotten); one subagent per 1–2 screens or artifact group, a shared opening line so the start is cached, mechanical tasks on the cheap tier, 5-line replies, the lead the only writer of `PROJECT.md`. The runbook's Stage 3 fan-out row points there, and its Stage 2 row names `facts-lock.sh resolve`. Measured on one project (single runs, Sonnet, 250k auto-compact): serial 13 min / $3.60, parallel 10 min / $4.44, parallel with a brief 6 min / $4.32; 0 name mismatches in the parallel runs (serial dropped 3 entities); both parallel runs forgot the process map and swimlanes, which the task list now covers. Baseline words 64,004 before and after — Stage 3 experiment 2026-10-28
2327
- fix(facts-lock, gate-check): **parallel BRDs keep one spelling per identifier**: `bin/facts-lock.sh <project> resolve` freezes each casing conflict (`WIPStatus`/`WipStatus`) whose majority spelling is used by strictly more BRD sources than any other and records the count in the lock (`resolvedBy`); a tie stays contested, and `build` still never picks on its own. Stage 2 of `gate-check.sh` now reads the lock once there are 2+ BRDs: a BRD that contradicts a frozen identifier FAILs and is quoted; no lock yet is a note, never a FAIL. Measured cause: 20 parallel BRDs on PROJECT-C carried 10 casing conflicts into Stage 3 as open questions, and facts-lock.sh, documented only as advice, reached 1 of 6 projects. Fixtures: `test-facts-lock.sh` resolve cases (4), `test-bug03-gates.sh` T19 (2 of 4 fail on the previous gate-check) — speed/parallel experiments 2026-10-28
2428
- fix(row-groups): **a build plan whose number and Kind cells are formatted as code (`` `BUILD` ``, `` `12` ``) is read like a plain one**: before, it read 0 BUILD rows, so with `Row groups: on` grouping silently did nothing (the fast-lane agent in the 2026-10-28 speed experiment had to strip the backticks by hand). Fixture: `test-row-groups.sh` G5 (both cases fail on the previous row-groups.sh) — speed experiment 2026-10-28

‎bin/gate-check.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1423,7 +1423,7 @@ EOF
14231423
[ "$n_proof" -gt 0 ] && how="$how, $n_proof by PROOF-OF-LOOK"
14241424
[ "$n_waived" -gt 0 ] && how="$how, $n_waived waived"
14251425
if [ "$n_unseen" -gt 0 ]; then
1426-
echo "FAIL|look: $n_unseen of $n built page(s) never looked at since their last build: $unseen — screenshot each, open the PNG (a file name containing the page name), compare it to its wireframe (skills/ui-loop.md); or --waive look/<Module> --reason \"...\""
1426+
echo "FAIL|look: $n_unseen of $n built page(s) never looked at since their last build: $unseen — screenshot each, open the PNG (a file name containing the page name, or the journey screenshot bin/look-ledger.sh shots lists), compare it to its wireframe (skills/ui-loop.md); or --waive look/<Module> --reason \"...\""
14271427
else
14281428
echo "MANUAL|look: $n of $n built page(s) accounted for ($how); the rest of Stage 5 is manual"
14291429
fi

‎bin/lib/install-manifest.sh‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ MXTK_AGENTS_STAGE_BUILD="mdl-agent.md gate-agent.md test-agent.md review-agent.m
107107
# directory. prototype-route.js is the one reader and writer of design/prototype.html's section
108108
# format, so page-fidelity.js, check-page-shell.sh and check-prototype-links.js resolve it as a
109109
# sibling too.
110+
# look-shots.js joined on 2026-10-29 (speed experiment E2): look-ledger.sh shots runs it to map journey screenshots to pages.
110111
# look-ledger.sh joined on 2026-10-02 with exec.sh's LOOK-owed call: exec.sh resolves it as its
111112
# own sibling, and the PostToolUse(Read) hook install-claude-permissions.sh writes runs
112113
# `bash bin/look-ledger.sh seen`. Two unattended builds built every page and opened no
@@ -128,7 +129,7 @@ MXTK_AGENTS_STAGE_BUILD="mdl-agent.md gate-agent.md test-agent.md review-agent.m
128129
# runs it per module (informational; see project-bin/mf-canvas-facts.py header).
129130
# model-scope.py joined on 2026-10-21: it imports mxpatch.py (and through it mxunit_bson.py) from
130131
# its own directory. It puts back units a write changed only in XPath whitespace (mxcli#1596).
131-
MXTK_PROJECT_BIN="reload.sh module-close.sh row-groups.sh process-map.js process-map.template.html app-restart.sh test-scratch.sh script-owners.sh describe-baseline.sh assemble-prototype.js prototype-route.js check-prototype-links.js wf-add-path-terminators.py wf-set-call-captions.py model-scope.py mf-canvas-facts.py mxunit_bson.py mxpatch.py _common.sh app-facts.sh _claims.sh snapshot-mpr.sh restore-mpr.sh exec.sh affected-tests.sh look-ledger.sh save-sp.sh restart-sp.sh check-sp-health.sh verify-module.sh harness-scan.sh test-stack-up.sh fixture-manifest.sh check-root-clean.sh lint-gate.sh lint-trend.sh metrics-ledger.sh metrics-trend.sh close-task.sh conformance-check.sh coverage-preflight.sh graph-sweep.sh review-module.sh coherence-cadence.sh build-plan-status.sh progress-report.py done-drift-check.sh page-scope.sh render-improvement-register.sh check-design-portability.sh check-design-reaches-app.sh check-page-shell.sh page-fidelity.js model-stamp.sh verify-model.sh install-project-hooks.sh mx-metadata-note.py session-check.sh constants-audit.sh context-pack.sh check-staged-secrets.sh"
132+
MXTK_PROJECT_BIN="reload.sh module-close.sh row-groups.sh process-map.js process-map.template.html app-restart.sh test-scratch.sh script-owners.sh describe-baseline.sh assemble-prototype.js prototype-route.js check-prototype-links.js wf-add-path-terminators.py wf-set-call-captions.py model-scope.py mf-canvas-facts.py mxunit_bson.py mxpatch.py _common.sh app-facts.sh _claims.sh snapshot-mpr.sh restore-mpr.sh exec.sh affected-tests.sh look-ledger.sh look-shots.js save-sp.sh restart-sp.sh check-sp-health.sh verify-module.sh harness-scan.sh test-stack-up.sh fixture-manifest.sh check-root-clean.sh lint-gate.sh lint-trend.sh metrics-ledger.sh metrics-trend.sh close-task.sh conformance-check.sh coverage-preflight.sh graph-sweep.sh review-module.sh coherence-cadence.sh build-plan-status.sh progress-report.py done-drift-check.sh page-scope.sh render-improvement-register.sh check-design-portability.sh check-design-reaches-app.sh check-page-shell.sh page-fidelity.js model-stamp.sh verify-model.sh install-project-hooks.sh mx-metadata-note.py session-check.sh constants-audit.sh context-pack.sh check-staged-secrets.sh"
132133

133134
# Files in project-bin/ that are deliberately NOT installed into projects. The reverse check
134135
# below flags anything named by NEITHER list, so a new file in project-bin/ has to be either
@@ -216,7 +217,7 @@ MXTK_LINT_RULES_NOINSTALL="README.md STOCK-HASHES.txt STOCK-HASHES-BUNDLED.txt"
216217
#
217218
# project.config.template.js is listed separately because it is the one file a project is
218219
# EXPECTED to edit — installing it over an edited copy would silently revert the port.
219-
MXTK_PROJECT_TESTS="config.js helpers.js settle.js otel.js journey-runner.js named-controls.js journey-runner.selftest.js journey-rung4-scope.test.js monkey.js monkey.selftest.js page-audit.js page-audit-rules.js design-audit.js full-app-walkthrough.js report-normalize.js report-render.js review-report.js demo-polish.js demo-polish.selftest.js example.journey.json"
220+
MXTK_PROJECT_TESTS="config.js helpers.js settle.js scope-ensure.js otel.js journey-runner.js named-controls.js journey-runner.selftest.js journey-rung4-scope.test.js monkey.js monkey.selftest.js page-audit.js page-audit-rules.js design-audit.js full-app-walkthrough.js report-normalize.js report-render.js review-report.js demo-polish.js demo-polish.selftest.js example.journey.json"
220221
MXTK_PROJECT_TESTS_TEMPLATE="project.config.template.js"
221222

222223
# Deliberately not installed. Same contract as the lists above.

‎project-bin/affected-tests.sh‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,14 @@
8989
# journey file (journeys/ or tests/e2e/) that names a subject OR a transitive caller literally.
9090
# A journey file that is not named <Module>.journey.json and holds no qualified Module.Name
9191
# cannot be selected by name at all; the "journeys unmapped" row counts them, so an empty
92-
# selection is never read as "nothing owed".
92+
# selection is never read as "nothing owed". Journeys name what they walk by WIDGET and by
93+
# menu caption far more often than by Module.Name, so the affected scope also reads the
94+
# widget names of every changed page/snippet and of every caller that is a page (DESCRIBE
95+
# PAGE, one cheap mxcli call) and selects each journey whose JSON holds one of them
96+
# ("widgets measured" row). A navigation or layout change selects EVERY journey (each walks
97+
# the menu). When the widgets or the callers cannot be measured ("widgets NOT MEASURED"),
98+
# the selection is EVERY journey: unsure means include. (speed experiment E2: 067 changed
99+
# a page J-REQ-01 walks and 090 changed navigation; both selected 0 journeys.)
93100
# 5. module: every test file naming the module, journeys/<M>.journey.json; the modules are the
94101
# subjects' modules plus every --module. full: every test file, every journey file.
95102
# 6. A grant/revoke whose object (microflow, page, ...) the SAME script creates or modifies is
@@ -324,6 +331,51 @@ select_unit() { # $1 = scope; prints "<file>\t<why>"
324331
esac | awk -F'\t' '!seen[$1]++'
325332
}
326333

334+
# ── Widgets (which journeys walk a changed page) ─────────────────────────────
335+
# Journeys are written against widget names and menu captions, not Module.Name, so name matching
336+
# alone selects nothing for a page change. ALLWHY non-empty = select every journey (navigation or
337+
# layout change, or the measurement failed: unsure means include).
338+
: > "$WORK/widgets"; : > "$WORK/widget-pages"
339+
WIDGETS="not needed"; ALLWHY=""
340+
if [ "$J" = affected ] && [ -n "$SCRIPTS" ]; then
341+
if printf '%s\n' "$SCRIPTS" | while IFS= read -r _s; do cat "$_s"; done |
342+
grep -qiE '^[[:space:]]*(create|alter|drop|move)[[:space:]]([^;]*[[:space:]])?navigation([[:space:]]|$)'; then
343+
ALLWHY="navigation change: every journey walks the menu"; WIDGETS="not needed ($ALLWHY)"
344+
elif awk -F'\t' '$1 == "layout" { f = 1 } END { exit !f }' "$WORK/subjects"; then
345+
ALLWHY="layout change: every page, so every journey, uses it"; WIDGETS="not needed ($ALLWHY)"
346+
elif [ -s "$WORK/flowish" ]; then
347+
if [ "$NO_MODEL" = 1 ]; then WIDGETS="NOT MEASURED (--no-model)"
348+
elif case "$CALLERS" in "NOT MEASURED"*) true ;; *) false ;; esac; then WIDGETS="NOT MEASURED (callers $CALLERS)"
349+
else
350+
_MX="${MXCLI:-$(find_project_mxcli 2>/dev/null || true)}"; _MPR="$(find_mpr 2>/dev/null || true)"
351+
if [ -z "$_MX" ] || [ ! -x "$_MX" ] || [ -z "$_MPR" ]; then WIDGETS="NOT MEASURED (no project mxcli or .mpr)"
352+
else
353+
"$_MX" -p "$_MPR" -c "SHOW PAGES; SHOW SNIPPETS" > "$WORK/pg.out" 2> "$WORK/pg.err"; _rc=$?
354+
# the Qualified Name column of the two tables
355+
awk -F'|' '$2 ~ /^ *[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]* *$/ { n = $2; gsub(/ /, "", n); print n }' "$WORK/pg.out" | sort -u > "$WORK/allpages"
356+
{ cat "$WORK/flowish"; cut -f1 "$WORK/callers"; } | sort -u > "$WORK/cand"
357+
comm -12 "$WORK/allpages" "$WORK/cand" > "$WORK/widget-pages"
358+
if [ "$_rc" -ne 0 ] || [ ! -s "$WORK/allpages" ]; then WIDGETS="NOT MEASURED (SHOW PAGES exit $_rc, $(wc -l < "$WORK/allpages" | tr -d ' ') page(s) listed)"
359+
elif [ ! -s "$WORK/widget-pages" ]; then WIDGETS="measured (0 of $(wc -l < "$WORK/cand" | tr -d ' ') changed or calling element(s) is a page or snippet)"
360+
else
361+
# second table in the output = SHOW SNIPPETS (its header has no Title column)
362+
_snip="$(awk '/^\| Qualified Name/ { h++ } h >= 2 && /^\| *[A-Za-z_]/ { split($0, c, "|"); n = c[2]; gsub(/ /, "", n); print n }' "$WORK/pg.out")"
363+
_q=""; while IFS= read -r _p; do
364+
if printf '%s\n' "$_snip" | grep -qxF "$_p"; then _q="${_q:+$_q; }DESCRIBE SNIPPET $_p"; else _q="${_q:+$_q; }DESCRIBE PAGE $_p"; fi
365+
done < "$WORK/widget-pages"
366+
"$_MX" -p "$_MPR" -c "$_q" > "$WORK/desc.out" 2> "$WORK/desc.err"; _rc=$?
367+
# widget heads: "<widgettype> <name> (" or "{" at the start of a line
368+
awk '/^[ \t]*[a-z][a-z0-9]*[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]*[({]/ && $1 != "create" && $1 != "alter" { print $2 }' "$WORK/desc.out" |
369+
sed 's/[({].*//' | sort -u > "$WORK/widgets"
370+
if [ "$_rc" -ne 0 ] || [ ! -s "$WORK/widgets" ]; then WIDGETS="NOT MEASURED (DESCRIBE exit $_rc, $(wc -l < "$WORK/widgets" | tr -d ' ') widget name(s) read)"
371+
else WIDGETS="measured ($(wc -l < "$WORK/widgets" | tr -d ' ') widget name(s) in $(wc -l < "$WORK/widget-pages" | tr -d ' ') page/snippet(s))"; fi
372+
fi
373+
fi
374+
fi
375+
case "$WIDGETS" in "NOT MEASURED"*) ALLWHY="unsure, so every journey ($WIDGETS)" ;; esac
376+
fi
377+
fi
378+
327379
select_journeys() { # $1 = scope; prints "<file>\t<why>"
328380
case "$1" in
329381
none) ;;
@@ -339,6 +391,13 @@ select_journeys() { # $1 = scope; prints "<file>\t<why>"
339391
_hit="$(grep -o -w -F -f "$WORK/jnames" "$f" 2>/dev/null | head -1)"
340392
[ -n "$_hit" ] && echo "$f names $_hit"
341393
done < "$WORK/journeys"
394+
if [ -n "$ALLWHY" ]; then awk -v w="$ALLWHY" '{ print $0 "\t" w }' "$WORK/journeys"
395+
elif [ -s "$WORK/widgets" ]; then
396+
while IFS= read -r f; do
397+
_hit="$(grep -o -w -F -f "$WORK/widgets" "$f" 2>/dev/null | head -1)"
398+
[ -n "$_hit" ] && echo "$f walks widget $_hit of a changed or calling page"
399+
done < "$WORK/journeys"
400+
fi
342401
fi ;;
343402
esac | awk -F'\t' '!seen[$1]++'
344403
}
@@ -444,6 +503,7 @@ select_journeys "$J" > "$WORK/journey.sel"
444503
if [ -n "$UNIT_OFF_WHY" ]; then out "unit off $UNIT_OFF_WHY"
445504
elif [ -s "$WORK/unit.sel" ]; then sed 's/^/unit /' "$WORK/unit.sel"
446505
else out "unit none scope $U: $( [ "$U" = none ] && echo 'strategy says none' || echo "0 of $(wc -l < "$WORK/tests" | tr -d ' ') .test.mdl file(s) match${FLIPPED:+ (unit on only for:$FLIPPED${LISTMODS:+; per listed flow in:$LISTMODS})}")"; fi
506+
[ "$J" = affected ] && [ -n "$SCRIPTS" ] && out "widgets $WIDGETS"
447507
if [ -s "$WORK/journey.sel" ]; then sed 's/^/journey /' "$WORK/journey.sel"
448508
else out "journey none scope $J: $( [ "$J" = none ] && echo 'strategy says none' || echo "0 of $(wc -l < "$WORK/journeys" | tr -d ' ') journey file(s) in $JDIR/ and tests/e2e/ match")"; fi
449509

‎project-bin/harness-scan.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ ROOT="${PROJECT_ROOT:-$(pwd)}"
4343

4444
# The engine files the toolkit installs into tests/e2e/ — kept in step with install-manifest.sh's
4545
# MXTK_PROJECT_TESTS. A name missing here is only a false NOTE on the toolkit's own file.
46-
ENGINE=" config.js helpers.js settle.js otel.js journey-runner.js named-controls.js journey-runner.selftest.js journey-rung4-scope.test.js monkey.js monkey.selftest.js page-audit.js page-audit-rules.js design-audit.js full-app-walkthrough.js report-normalize.js report-render.js review-report.js demo-polish.js demo-polish.selftest.js project.config.js project.config.template.js "
46+
ENGINE=" config.js helpers.js settle.js otel.js journey-runner.js named-controls.js journey-runner.selftest.js journey-rung4-scope.test.js monkey.js monkey.selftest.js page-audit.js page-audit-rules.js design-audit.js full-app-walkthrough.js report-normalize.js report-render.js review-report.js demo-polish.js demo-polish.selftest.js scope-ensure.js project.config.js project.config.template.js "
4747

4848
files=()
4949
for d in tests e2e acceptance; do

0 commit comments

Comments
 (0)