diff --git a/CHANGELOG.md b/CHANGELOG.md index df3d67a9..db9fc6c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. ## Unreleased +- fix(obligations): **LOOK staleness reads the newest `VALID AT` stamp, anywhere in the report.** Both pickers took the first stamp inside the first 8000 bytes, so a review file with dated per-build addenda went false-STALE; now `tail -1` over the whole file. Field run 2026-09-26, requirements-driven build (#155) — MendixMau - perf(routing): **CLAUDE.local.md routing carries triggers only.** The `baseline` view (the copy loaded into every session of every project) now cuts each row's "when" cell at its first " — " and replaces the three-paragraph preamble with two lines pointing at `ROUTING.md` for the full text; `readme-baseline` (README, runbook) is unchanged. Fresh scaffold: CLAUDE.local.md 17.5 KB → 11.7 KB (−33%). Field run 2026-10-07 (existing app, one main session per run, 2 runs per side, an entity + 2 microflows + 2 pages): 10/10 steps applied each side, new mx errors 5 vs 5 (the same CE2421 page-binding slip in both), cost $1.39 vs $1.38; the slim lanes opened the MDL pre-flight skill as often or more. Also drops two stray merge-artifact lines from this section. — MendixMau - perf(ui-loop): **fewer mxbuilds, no fixed sleeps, legible screenshots.** Field run 2026-10-07 (a requirements-driven build): `exec.sh` was 22% of tool time at two mxbuilds per exec, journeys and page waits slept a fixed time, and screenshots were 11.5% of re-read tokens. `exec.sh` now skips the pre-flight baseline mxbuild when the model is byte-identical to its last *clean* pass: the verification stamp gains an `errors:` field (`model-stamp.sh check --clean` accepts only `errors: 0`, so a delta-gate pass over pre-existing errors never counts; `SKIP_BASELINE=0` is unchanged). New `tests/e2e/settle.js` waits for the page's own state (no visible Mendix progress/loading indicator, DOM size stable) instead of a fixed time; `journey-runner.js` uses it after each action (an explicit `settleMs` still wins) and `page-audit.js` per page. Probed in a real browser: on a page that loads in 1.8 s the old 1200 ms sleep and networkidle+600 both captured it half-loaded, settle waited for it; on a fast page settle took 775 ms vs 1200. `SETTLE_MODE=fixed` restores the old waits for an A/B run. `page-audit.js` also saves a viewport shot `page-audit-.top.png` beside the full-page one, and `ui-loop.md` says to look at viewport shots: a tall full-page shot reaches the model at ~1024 px high, text unreadable. `shrink-image-read.sh` now works off macOS (ImageMagick or Pillow, python when jq is absent; it was a silent no-op without `sips`). — MendixMau - perf(ui-loop): **a stale app reloads instead of restarting, journeys stop sleeping, worst pages are looked at first.** Field run 2026-10-07, existing app. `test-stack-up.sh` records which model the running app was built from (`.claude/loop/served-model`); when the model has changed since, it hot-reloads with `mxcli docker reload` (adding `--skip-check` when the exec.sh gate already passed this model) and restarts only if that fails: 93 s instead of 179 s. `ui-loop.md` and the script header carry the measured table and point at `mxcli run --local --watch` as the fast loop (~18 s per page change). `journey-runner.js` waits on page state after a combobox action instead of fixed pauses, and a combobox step now opens the widget it names: it used to open the page's first combobox, so on a form with three every step but the first failed. `page-audit.js` clicks the first copy of a nav item a user could actually click, not just the first match: with the sidebar collapsed, every in-nav page faulted while the same item sat in the top menu bar. It also writes `page-audit-look-order.txt`, all pages worst first (`module-review.md` §4a: order, not selection, every page is still looked at). `design-audit.js` drops its fixed 1.2 s wait. Same 7-step journey on the same app: 56 s with `SETTLE_MODE=fixed`, 24 s with settle, 11/11 pass both ways, 3 runs each. — MendixMau diff --git a/bin/lib/obligation-check.sh b/bin/lib/obligation-check.sh index 7fde53e1..af7b25ac 100755 --- a/bin/lib/obligation-check.sh +++ b/bin/lib/obligation-check.sh @@ -246,9 +246,9 @@ _ob_has_denominator() { # "expired". _ob_stale() { local root="$1" hit="$2" hash n - hash="$(head -c 8000 "$hit" 2>/dev/null | tr -d '\r' \ + hash="$(cat "$hit" 2>/dev/null | tr -d '\r' \ | grep -oiE 'VALID[[:space:]]+AT:?[[:space:]]*[0-9a-f]{7,40}' \ - | grep -oiE '[0-9a-f]{7,40}$' | head -1)" + | grep -oiE '[0-9a-f]{7,40}$' | tail -1)" [ -n "$hash" ] || return 1 git -C "$root" cat-file -e "${hash}^{commit}" 2>/dev/null || return 1 n="$(git -C "$root" log --oneline "${hash}..HEAD" -- '*.mpr' mdlsource 2>/dev/null | wc -l | tr -d ' ')" @@ -303,9 +303,9 @@ _ob_look_proof_broken() { # VALID AT commit time, for freshness. Unresolvable → freshness unmeasured, not failed. local vhash vtime="" - vhash="$(head -c 8000 "$hit" 2>/dev/null | tr -d '\r' \ + vhash="$(cat "$hit" 2>/dev/null | tr -d '\r' \ | grep -oiE 'VALID[[:space:]]+AT:?[[:space:]]*[0-9a-f]{7,40}' \ - | grep -oiE '[0-9a-f]{7,40}$' | head -1)" + | grep -oiE '[0-9a-f]{7,40}$' | tail -1)" [ -n "$vhash" ] && vtime="$(git -C "$root" show -s --format=%ct "${vhash}^{commit}" 2>/dev/null || true)" reldir="$(dirname "$hit")" diff --git a/tests/wave2/test-obligation-valid-at.sh b/tests/wave2/test-obligation-valid-at.sh new file mode 100755 index 00000000..c949bc4a --- /dev/null +++ b/tests/wave2/test-obligation-valid-at.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Usage: bash tests/wave2/test-obligation-valid-at.sh [path-to-gate-check.sh | path-to-obligation-check.sh] +# +# test-obligation-valid-at.sh — the LOOK obligation's staleness check must judge a review report +# by its NEWEST "VALID AT: " stamp, wherever in the file it sits. +# +# THE BUG (#155): _ob_stale and the proof check picked `head -c 8000 | grep ... | head -1` — the +# FIRST stamp in the first 8000 bytes. A report that keeps one dated headline and appends an +# addendum per build row (each with its own VALID AT) was judged by the headline's original +# stamp and went STALE after later model commits, though every page was re-looked-at. +# Subject: obligation-check.sh. Producer of the stamp: module-review.md stage 5 (the reviewer). +set -uo pipefail +ARG="${1:?usage: test-obligation-valid-at.sh /path/to/bin/gate-check.sh (or lib/obligation-check.sh)}" +case "$(basename "$ARG")" in + obligation-check.sh) OBCHECK="$ARG" ;; + *) OBCHECK="$(cd "$(dirname "$ARG")" && pwd)/lib/obligation-check.sh" ;; +esac +[ -r "$OBCHECK" ] || { echo "no obligation-check.sh at $OBCHECK"; exit 2; } +WORK="$(mktemp -d "${TMPDIR:-/tmp}/ob-validat.XXXXXX")" +trap '[ -n "${KEEP:-}" ] || rm -rf "$WORK"' EXIT +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; } +bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; } + +P="$WORK/p"; mkdir -p "$P/architecture/modules/ModuleX" "$P/mdlsource" "$P/design/ui-reviews" +git -C "$P" init -q . 2>/dev/null +git -C "$P" config user.email t@t; git -C "$P" config user.name t +echo a > "$P/mdlsource/a.mdl"; git -C "$P" add -A; git -C "$P" commit -qm one +OLD="$(git -C "$P" rev-parse --short HEAD)" +echo b >> "$P/mdlsource/a.mdl"; git -C "$P" commit -qam two +NEW="$(git -C "$P" rev-parse --short HEAD)" +REP="$P/design/ui-reviews/ui-review-2026-01-01.html" +stale() { "$OBCHECK" "$P" 2>&1 | grep -c 'STALE'; } + +echo "== T1: only the old stamp, model built past it -> STALE (control) ==" +printf '

ModuleX: 1 of 1 pages reviewed

\nVALID AT: %s\n' "$OLD" > "$REP" +[ "$(stale)" -gt 0 ] && ok "old-only stamp reports STALE" || bad "control: expected STALE" + +echo "== T2: old stamp, then an addendum stamped at HEAD -> newest wins, not STALE ==" +printf '

ModuleX: 1 of 1 pages reviewed

\nVALID AT: %s\n

Addendum

\nVALID AT: %s\n' "$OLD" "$NEW" > "$REP" +[ "$(stale)" -eq 0 ] && ok "later stamp wins" || bad "first stamp was used (STALE)" + +echo "== T3: newest stamp sits past byte 8000 -> still seen ==" +{ printf '

ModuleX: 1 of 1 pages reviewed

\nVALID AT: %s\n' "$OLD"; head -c 9000 /dev/zero | tr '\0' 'x'; printf '\nVALID AT: %s\n' "$NEW"; } > "$REP" +[ "$(stale)" -eq 0 ] && ok "stamp past 8000 bytes seen" || bad "stamp past 8000 bytes ignored (STALE)" + +echo "pass=$PASS fail=$FAIL" +[ "$FAIL" -eq 0 ]