Files
stack/packages/mosaic/framework/tools/git/mutate-push-guard.sh
T
installer-7andClaude Opus 5 8310075d33
ci/woodpecker/pr/ci Pipeline was successful
fix(git): push-guard — resolve re-review blockers; mutation table now generated
Authored by installer-7; committed by mos-claude (no credential for this remote).

BLOCKER 1 — verify-clean-clone.sh had B1's own defect: it copied WORKING-TREE files into a
  scratch repo and asserted the SCRATCH index, so a stale local exec bit was laundered in and it
  reported success while the committed artifact was still 100644. v2 reads mode from 'git ls-tree'
  of the SOURCE COMMIT and runs from 'git clone --no-local --no-hardlinks' of that commit; there is
  no 'cp' in the file. Proven by A/B against one laundered repo: v1 EXIT=0, v2 EXIT=1 (both observed,
  not asserted). New test-verify-clean-clone.sh 6/6, incl. a fixture that first proves it really is
  git=100644 disk=755 before testing it.
BLOCKER 2 — empty-merge needle + non-empty-merge control, both asserting on OUTPUT; the fixture
  first proves it IS a merge with a tree identical to both parents.

The 'non-blocking' README item was not documentation: regenerating its mutation table (now generated
by mutate-push-guard.sh, not hand-numbered) found TWO genuinely surviving mutants against a 46/46
green suite — staged-but-uncommitted opt-out honoured, and unparseable committed config ignored.
Both still exit 6 under mutation because control falls to a SIBLING refusal, so an exit-code-only
assertion would have been satisfied by the wrong branch. It also found a live instance of the
substring-anchor defect already in the suite: three branches print 'OPT-OUT IS NOT REVIEWABLE', so
deleting one let the needle RE-POINT to a sibling instead of failing. Re-anchored.
Suite 44 -> 46. Verifier 6/6. 13 mutants, 0 survived.

Also: README reformatted to satisfy 'pnpm format:check' (prettier), which was failing CI at both
prior heads — a gate neither author nor reviewer had exercised.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YKj59Qadrb2WBLaePvkM7H
2026-07-30 21:06:55 -05:00

144 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# mutate-push-guard.sh -- regenerate the README's mutation table from MEASUREMENT.
#
# WHY THIS EXISTS. The README carried a hand-written mutation table quoting
# "32/32" style results. Those numbers were true when typed and went stale in
# silence as the suite grew. A README is what a reader trusts when the tool
# misbehaves, so a confidently-wrong one is worse than none. Every number the
# README prints about mutation now comes out of this script.
#
# TWO WAYS A MUTATION RUN LIES, AND THE GUARD FOR EACH:
#
# 1. THE ANCHOR NO LONGER MATCHES. The mutant is never applied, the suite is
# green, and the report says SURVIVED -- which is the same word a real
# coverage gap gets. Guarded by grep-before-mutate (ANCHOR MISSING).
#
# 2. THE ANCHOR MATCHES PROSE. This one bit me on the first run: my
# "revert refuse-on-missing-config" mutant landed inside the usage()
# heredoc, edited a help string, changed no behaviour, and duly reported
# SURVIVED. I nearly recorded a documentation edit as an uncovered branch.
# A MUTATION THAT CANNOT CHANGE BEHAVIOUR IS NOT A SURVIVING MUTANT, IT IS
# A NON-MEASUREMENT -- and a non-measurement reported as a result is the
# same defect as the vacuous test it is supposed to be hunting.
# Guarded by prose_range(): anchors inside usage() are a loud refusal.
#
# 3. THE ANCHOR IS AMBIGUOUS. replace(...,1) silently picks the first match,
# which may not be the branch named in the label. Guarded by an exact
# occurrence count.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET="$HERE/push-guard.sh"
SUITE="$HERE/test-push-guard.sh"
BAK="$(mktemp)"; cp "$TARGET" "$BAK"
trap 'cp "$BAK" "$TARGET"; rm -f "$BAK"' EXIT
# --- where the prose lives: usage() { ... EOF ---------------------------------
PROSE_LO="$(grep -n '^usage() {' "$BAK" | head -1 | cut -d: -f1)"
PROSE_HI="$(awk -v lo="$PROSE_LO" 'NR > lo && /^EOF$/ { print NR; exit }' "$BAK")"
if [[ -z "$PROSE_LO" || -z "$PROSE_HI" ]]; then
echo "!! cannot locate the usage() heredoc -- the prose guard would be inert; refusing" >&2
exit 1
fi
printf 'prose (usage heredoc) is lines %s-%s -- anchors there are refused, not scored\n\n' \
"$PROSE_LO" "$PROSE_HI"
rc_all=0
KILLED=0; SURVIVED=0
declare -a ROWS=()
mutate() {
local name="$1" find="$2" repl="$3"
# Count and locate in python so MULTI-LINE anchors work. They are required:
# two unrelated branches in this file are both the single line
# `if (( status != 0 )); then`, and a one-line anchor cannot say which one a
# result belongs to. Guessing would attribute a kill to the wrong branch.
local loc; loc="$(python3 - "$BAK" "$find" <<'LOCPY'
import sys
s = open(sys.argv[1]).read(); find = sys.argv[2]
n = s.count(find)
print(n, (s[:s.index(find)].count("\n") + 1) if n else 0)
LOCPY
)"
local n="${loc%% *}" ln="${loc##* }"
if (( n == 0 )); then
printf ' !! ANCHOR MISSING %-46s NOT APPLIED -- result would be meaningless\n' "$name"
rc_all=1; return
fi
if (( n != 1 )); then
printf ' !! ANCHOR AMBIGUOUS %-46s %d matches -- refusing to guess which branch\n' "$name" "$n"
rc_all=1; return
fi
if (( ln >= PROSE_LO && ln <= PROSE_HI )); then
printf ' !! ANCHOR IS PROSE %-46s line %d is inside usage() -- not a branch\n' "$name" "$ln"
rc_all=1; return
fi
python3 - "$BAK" "$TARGET" "$find" "$repl" <<'PY'
import sys
src, dst, find, repl = sys.argv[1:5]
open(dst, "w").write(open(src).read().replace(find, repl, 1))
PY
local out line failed passed total
out="$("$SUITE" 2>&1)"
line="$(printf '%s\n' "$out" | grep -E 'needles: [0-9]+ passed' | tail -1)"
failed="$(printf '%s\n' "$line" | sed -n 's/.*, \([0-9]*\) failed.*/\1/p')"
passed="$(printf '%s\n' "$line" | sed -n 's/.*: \([0-9]*\) passed.*/\1/p')"
if [[ -z "$failed" || -z "$passed" ]]; then
printf ' !! NO TALLY %-46s suite produced no needle count\n' "$name"
rc_all=1; cp "$BAK" "$TARGET"; return
fi
total=$(( passed + failed ))
if (( failed > 0 )); then
printf ' KILLED L%-5s %-46s %s/%s fail\n' "$ln" "$name" "$failed" "$total"
ROWS+=("| \`$name\` (L$ln) | $failed/$total fail | killed |")
KILLED=$(( KILLED + 1 ))
else
printf ' SURVIVED L%-5s %-46s 0/%s fail <-- UNCOVERED BRANCH\n' "$ln" "$name" "$total"
ROWS+=("| \`$name\` (L$ln) | 0/$total fail | **SURVIVED** |")
SURVIVED=$(( SURVIVED + 1 )); rc_all=1
fi
cp "$BAK" "$TARGET"
}
echo "=== push-guard mutation run ==="
mutate "json decision requirement bypassed" \
' if (( ${#JSON_PATHS[@]} == 0 )); then' ' if false; then'
mutate "opt-out accepted with no written reason" \
'if not isinstance(reason, str) or not reason.strip():' 'if False:'
mutate "committed re-read of the opt-out skipped" \
' [[ "$CFG_MODE" == "none" ]] || return 0' ' return 0'
mutate "untracked config honoured as an opt-out" \
' if [[ -z "$rel" ]]; then' ' if false; then'
mutate "staged-but-uncommitted opt-out honoured" \
' if [[ -z "$cmode" ]]; then' ' if false; then'
mutate "committed SYMLINK config honoured" \
' if [[ "$cmode" == "120000" ]]; then' ' if false; then'
mutate "unparseable committed config ignored" \
' if (( cstatus != 0 )); then' ' if false; then'
mutate "local-only opt-out (HEAD says ON) honoured" \
' if [[ "$cmode_val" != "none" ]]; then' ' if false; then'
mutate "empty MERGE exempted" \
' if [[ "$all_same" == yes ]]; then' ' if false; then'
mutate "empty ROOT exempted" \
'if [[ -z "$(git diff-tree --root -r --name-only --no-commit-id HEAD)" ]]; then' \
'if false; then'
mutate "--since-head ancestry check removed" \
'if ! git merge-base --is-ancestor "$since_head" "$head"; then' 'if false; then'
mutate "staged-file enumeration ignores git failure" \
"$(printf 'if (( status != 0 )); then\n local msg')" \
"$(printf 'if false; then\n local msg')"
mutate "malformed config degrades to absent instead of refusing" \
"$(printf 'if (( status != 0 )); then\n fail "$EX_CONFIG"')" \
"$(printf 'if false; then\n fail "$EX_CONFIG"')"
BASE="$("$SUITE" 2>&1 | grep -E 'needles: [0-9]+ passed' | tail -1)"
printf '\nunmodified: %s\n' "$BASE"
printf '%d killed, %d survived\n' "$KILLED" "$SURVIVED"
printf '\n--- README TABLE (paste verbatim) ---\n'
printf '| mutation | suite result | verdict |\n|---|---|---|\n'
printf '| *unmodified* | %s | baseline |\n' \
"$(printf '%s' "$BASE" | sed 's/push-guard needles: //')"
printf '%s\n' "${ROWS[@]}"
exit "$rc_all"