diff --git a/CHANGELOG.md b/CHANGELOG.md index ee699ada..ee102a06 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(sync): **the exec-approval item-3 rewrite now works on BSD awk (stock macOS) and `sync-project.sh` no longer prints "Rewrote" for a rewrite that failed.** `_ea_rewrite_item3` passed the multi-line replacement through `awk -v`, which BSD awk rejects ("newline in string", exit 2); the file stayed unchanged and sync still reported the rewrite on every run. The replacement is now streamed from a temp file (as the Q9 repair does), the helper returns 2 when awk fails, and the caller prints a `WARN:` instead of "Rewrote" and does not count a change. `test-exec-approval.sh` gains 3 assertions (failing-awk shim: no "Rewrote", a WARN, file unchanged). No other multi-line `awk -v` call site exists (the rest are single-line values) ([#426](https://github.com/MendixMau/mxcli-project-toolkit/issues/426)) — MendixMau - process(testing): **unit tests are now off by default and `affected-tests.sh` enforces it; each project picks its test strategy with the developer at CAC-5.** Fast-tracked by the maintainer on 2026-10-16 because users report tests firing on every change (the baseline-text A/B is waived; E3 follows as a check). Moments: every change = gate plus what it touched (unit only for affected modules with unit on); module done = that module's journeys (+ its unit tests if on); build complete and before every later deploy = everything once; "run everything" any time. The CAC-5 Offer records it in PROJECT.md `## Test strategy` (Status `ASSUMED` in auto mode). `affected-tests.sh` reads `Unit tests default:` (`no` or absent means unit off, one `unit off` line, never a `.test.mdl` run row); turn unit on per project with `Unit tests default: yes`, per module with `Unit modules: Shop, Admin` in PROJECT.md or `Unit tests: yes` in the module brief; the strategy row's Unit cell is a scope only and never turns unit on. A grant on a microflow or page the same script changes is part of that subject and no longer selects the owning module's tests. `module-review.md` PROVE now runs `--moment module --module `. `testing-shape.md`, `checkpoint-build.md`, `iterative-build-loop.md`, `conversion-runbook.md` and the exec.sh logic lane text updated. `tests/wave2/test-test-strategy-docs.sh` and the extended `test-affected-tests.sh` pin it. Baseline words stay at or below master (75,219 vs 75,225). Split out of #409; found by the field test of the release candidate — the maintainer's decision, 2026-10-16 - learn(skills): **five lab findings land as compact rows, each paid for by trimming prose in the same files, so always-loaded words do not grow (baseline 75,225 to 75,204, budget 80,000).** Enum value `New` and `AutoCreatedDate`/`AutoOwner` (detection gaps), `[%CurrentDateTime%]` not `currentDateTime()`, expression return types (Decimal from date diffs and `div`, absolute `secondsBetween`, `formatDecimal` on pages), CE2729 from the default home page and page microflow grants, Atlas selector carriers for list views, page body and timelines, `-small` spacing suffixes do not exist, `display: contents` rows paint nothing, CSS pseudo text is invisible to `innerText` — lab field runs, Refs #276 (TK-020, TK-021, TK-022, TK-024) - fix(design-check): **a design system whose palette lives only in scoped `:root.` blocks no longer reads as "0 of N tokens carry the design's value".** Pass 2b compared every token against a bare `:root` of the built sheet, so a themed-only palette always warned, a false warning that teaches agents to ignore the check. A token the design declares only in scoped blocks is now compared block for block against the same-named block in the built sheet, and a wrong value is still named with its block (knob binding stays bare-only: a knob bound inside a theme the app is not wearing is not bound). Synthetic fixtures `ds-themed.css`, `built-themed.css`, `built-themed-wrong.css`; 5 new cases in `test-design-reaches-app.sh` (50 total) — TK-015 part (b) diff --git a/bin/sync-project.sh b/bin/sync-project.sh index 5a8fa567..10e92bf3 100755 --- a/bin/sync-project.sh +++ b/bin/sync-project.sh @@ -1511,15 +1511,46 @@ _ea_rewrite_item3() { # $1=file $2=start-prefix $3=end-prefix $4=replacemen grep -qF -- "$needle" <<<"$_blk" || return 1 fi [ "$DRY_RUN" -eq 1 ] && return 0 - _tmp="$(mktemp "${TMPDIR:-/tmp}/eaitem3.XXXXXX")" || return 1 - awk -v start="$start" -v endp="$endp" -v repl="$repl" ' + _tmp="$(mktemp "${TMPDIR:-/tmp}/eaitem3.XXXXXX")" || return 2 + _rf="$(mktemp "${TMPDIR:-/tmp}/eaitem3r.XXXXXX")" || { rm -f "$_tmp"; return 2; } + # BSD awk (stock macOS) rejects a newline inside -v ("awk: newline in string"), so the + # replacement text is streamed from a file, as the Q9 repair does. Return 2 (not 0) when awk + # fails so the caller warns instead of reporting a rewrite that never happened. + printf '%s\n' "$repl" > "$_rf" || { rm -f "$_tmp" "$_rf"; return 2; } + if awk -v start="$start" -v endp="$endp" -v rf="$_rf" ' BEGIN { skip = 0 } { if (skip) { if (index($0, endp) == 1) { skip = 0 } else { next } } - if (!skip && index($0, start) == 1) { print repl; skip = 1; next } + if (!skip && index($0, start) == 1) { + while ((getline l < rf) > 0) print l + close(rf); skip = 1; next } print } - ' "$f" > "$_tmp" && cat "$_tmp" > "$f" - rm -f "$_tmp" - return 0 + ' "$f" > "$_tmp" && [ -s "$_tmp" ]; then + cat "$_tmp" > "$f" + rm -f "$_tmp" "$_rf" + return 0 + fi + rm -f "$_tmp" "$_rf" + return 2 +} +# Report one item-3 rewrite attempt: $1=rc of _ea_rewrite_item3 $2=label $3=dry text $4=done text +_ea_report_item3() { + case "$1" in + 0) if [ "$DRY_RUN" -eq 1 ]; then echo "Would rewrite: $2 item 3 — $3" + else echo "Rewrote: $2 item 3 — $4"; fi + CHANGES=$((CHANGES + 1)) ;; + 2) warn "$2 item 3 rewrite FAILED (awk error or temp file); file left unchanged." "Re-run sync, or edit item 3 by hand." ;; + esac +} +_ea_try_item3() { # $1=file $2=label $3=md/hash text $4=prefix of "3. **" — tries both wordings + _p="$4"; _rc=0 + _ea_rewrite_item3 "$1" "${_p}3. **Never run" "${_p}4. **" "$3" || _rc=$? + if [ "$_rc" -ne 1 ]; then + _ea_report_item3 "$_rc" "$2" "carries the old \"without asking the user first — every time\" wording" "the old \"without asking the user first — every time\" wording is now the exec-approval knob (bin/exec-approval.sh)." + return + fi + _rc=0 + _ea_rewrite_item3 "$1" "${_p}3. **Before any" "${_p}4. **" "$3" '`ask` (the default)' || _rc=$? + _ea_report_item3 "$_rc" "$2" "still says \`ask\` is the default; exec approval now defaults to \`auto\`" "flipped from \"\`ask\` (the default)\" wording to auto-default (bin/exec-approval.sh)." } if [ "$WIRED" -eq 1 ]; then _EA_NEW3_MD="$(wiring_item3_md "$TOOLKIT_ROOT")" @@ -1527,38 +1558,10 @@ if [ "$WIRED" -eq 1 ]; then for rel in AGENTS.md CLAUDE.md .github/copilot-instructions.md .cursorrules .windsurfrules; do f="$PROJECT_DIR/$rel" [ -f "$f" ] || continue - if _ea_rewrite_item3 "$f" "3. **Never run" "4. **" "$_EA_NEW3_MD"; then - if [ "$DRY_RUN" -eq 1 ]; then - echo "Would rewrite: $rel item 3 — carries the old \"without asking the user first — every time\" wording" - else - echo "Rewrote: $rel item 3 — the old \"without asking the user first — every time\" wording is now the exec-approval knob (bin/exec-approval.sh)." - fi - CHANGES=$((CHANGES + 1)) - elif _ea_rewrite_item3 "$f" "3. **Before any" "4. **" "$_EA_NEW3_MD" '`ask` (the default)'; then - if [ "$DRY_RUN" -eq 1 ]; then - echo "Would rewrite: $rel item 3 — still says \`ask\` is the default; exec approval now defaults to \`auto\`" - else - echo "Rewrote: $rel item 3 — flipped from \"\`ask\` (the default)\" wording to auto-default (bin/exec-approval.sh)." - fi - CHANGES=$((CHANGES + 1)) - fi + _ea_try_item3 "$f" "$rel" "$_EA_NEW3_MD" "" done f="$PROJECT_DIR/.aider.conf.yml" - if [ -f "$f" ] && _ea_rewrite_item3 "$f" "# 3. **Never run" "# 4. **" "$_EA_NEW3_HASH"; then - if [ "$DRY_RUN" -eq 1 ]; then - echo "Would rewrite: .aider.conf.yml item 3 — carries the old \"without asking the user first — every time\" wording" - else - echo "Rewrote: .aider.conf.yml item 3 — the old \"without asking the user first — every time\" wording is now the exec-approval knob (bin/exec-approval.sh)." - fi - CHANGES=$((CHANGES + 1)) - elif [ -f "$f" ] && _ea_rewrite_item3 "$f" "# 3. **Before any" "# 4. **" "$_EA_NEW3_HASH" '`ask` (the default)'; then - if [ "$DRY_RUN" -eq 1 ]; then - echo "Would rewrite: .aider.conf.yml item 3 — still says \`ask\` is the default; exec approval now defaults to \`auto\`" - else - echo "Rewrote: .aider.conf.yml item 3 — flipped from \"\`ask\` (the default)\" wording to auto-default (bin/exec-approval.sh)." - fi - CHANGES=$((CHANGES + 1)) - fi + [ -f "$f" ] && _ea_try_item3 "$f" ".aider.conf.yml" "$_EA_NEW3_HASH" "# " fi # ── Per-harness auto-run allow-list: report only, never write ──────────────────────────── diff --git a/tests/wave2/test-exec-approval.sh b/tests/wave2/test-exec-approval.sh index 271eb957..2ba0c887 100755 --- a/tests/wave2/test-exec-approval.sh +++ b/tests/wave2/test-exec-approval.sh @@ -315,6 +315,45 @@ else bad "sync-project.sh not found/executable at $SYNC — cannot check the 2026-09-15 repair" fi +# --- sync-project.sh: a failed item-3 rewrite is reported as a failure, never as "Rewrote" ---- +# (#426) A shim awk that always exits 2 stands in for BSD awk rejecting the replacement. +if [ -x "$SYNC" ]; then + SP3="$WORK/syncproj3"; mkdir -p "$SP3/bin" "$WORK/failawk" + : > "$SP3/Fixture.mpr" + printf 'placeholder\n' > "$SP3/PROJECT.md" + printf 'placeholder\n' > "$SP3/CLAUDE.local.md" + printf 'placeholder\n' > "$SP3/intake.md" + mkdir -p "$SP3/.github" + for f in AGENTS.md CLAUDE.md .github/copilot-instructions.md .cursorrules .windsurfrules; do + printf '%s\n' "$OLDBLOCK" > "$SP3/$f" + done + # all six entry points present, so wire-agents --check passes and only the item-3 repair runs + printf '%s\n' "$OLDBLOCK" | sed 's/^/# /' | sed 's/[[:space:]]*$//' > "$SP3/.aider.conf.yml" + # fails only the replacement-streaming call (the one that passes rf=), like BSD awk would + printf '#!/bin/sh\nfor a in "$@"; do case "$a" in rf=*) echo "awk: simulated failure" >&2; exit 2;; esac; done\nexec /usr/bin/awk "$@"\n' > "$WORK/failawk/awk" + chmod +x "$WORK/failawk/awk" + BEFORE3="$(cat "$SP3/CLAUDE.md")" + cp -R "$SP3" "$SP3.base" + OUT4B="$(bash "$SYNC" "$SP3.base" 2>&1)" + OUT4="$(PATH="$WORK/failawk:$PATH" bash "$SYNC" "$SP3" 2>&1)" + printf '%s' "$OUT4" | grep -q 'Rewrote: CLAUDE.md item 3' \ + && bad "sync-project.sh: prints 'Rewrote' for an item-3 rewrite that failed" \ + || ok "sync-project.sh: no 'Rewrote' message when the item-3 rewrite fails" + printf '%s' "$OUT4" | grep -q '⚠️ CLAUDE.md item 3 rewrite FAILED' \ + && ok "sync-project.sh: failed item-3 rewrite is reported as a WARN" \ + || bad "sync-project.sh: failed item-3 rewrite gave no WARN" + # the failures must reach the WARNINGS counter: the same fixture synced with a working awk + # gives the baseline count, and the six failed rewrites must add to it + N0="$(printf '%s' "$OUT4B" | sed -n 's/^\([0-9][0-9]*\) warning(s).*/\1/p')" + N1="$(printf '%s' "$OUT4" | sed -n 's/^\([0-9][0-9]*\) warning(s).*/\1/p')" + [ "${N1:-0}" -ge $(( ${N0:-0} + 6 )) ] \ + && ok "sync-project.sh: failed item-3 rewrites are counted as warnings (${N0:-0} -> ${N1:-0})" \ + || bad "sync-project.sh: failed item-3 rewrites not counted as warnings (${N0:-0} -> ${N1:-0})" + [ "$BEFORE3" = "$(cat "$SP3/CLAUDE.md")" ] \ + && ok "sync-project.sh: file left unchanged when the rewrite fails" \ + || bad "sync-project.sh: file changed although the rewrite failed" +fi + echo "" echo "PASS=$PASS FAIL=$FAIL" [ "$FAIL" -eq 0 ]