Files
jason.woltje 6bad02612a lead: commit dewey's untracked evidence (721 files: queue-124-review queue-143 queue-149 queue-165-review queue-170 queue-175 queue-177 queue-184 queue-191)
Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
2026-11-02 15:05:14 -06:00

100 lines
4.6 KiB
Bash

#!/usr/bin/env bash
# Dewey's row 149 round 3 mutants (sage-0684, sage-0691): one per new contract-line assertion in
# packages/business/tests/workflow-roles.test.mjs. Each mutant edits one role file once; the test
# file must then fail. Usage: mutants.sh <r149-r2 worktree> [--dry-run]
# --dry-run applies and restores each edit (exactly one match, file hash back) and runs no test.
set -uo pipefail
WT=$(cd "$1" && pwd) || exit 2
DRY=${2:-}
OUT=$(cd "$(dirname "$0")" && pwd) || exit 2
mkdir -p "$OUT/mutants" "$OUT/backup" || exit 2
export TMPDIR=$HOME/dewey-scratch/tmp
TEST=packages/business/tests/workflow-roles.test.mjs
(cd "$WT" && sha256sum -c --quiet "$OUT/../candidate-manifest.sha256") || { echo "manifest check failed"; exit 2; }
# id|file|old|new (old must occur exactly once; new may be empty)
MUTANTS=$(cat <<'EOF'
m1|roles/merge-gate.md|The run that raises the decision ends its
turn there. |
m2|roles/merge-gate.md|don't try to land and don't raise again.|don't try to land; raise it again.
m3|roles/merge-gate.md|A run launched after the approval has `land_work`|The raising run has `land_work`
m4|roles/merge-gate.md|- If the decision is declined,|- Once the decision is resolved with its approval option, call
`land_work`.
- If the decision is declined,
m5|roles/cto.md|send the chief of staff one message|send the pm one message
m6|roles/cto.md|and the decision
id.|and the task.
m7|roles/chief-of-staff.md|launch that instance with
`launch`.|tell the pm.
m8|roles/chief-of-staff.md|Then
end it with `end_run`, with a reason naming the decision, and launch
once it has exited.|Then wait for it to end.
m9|roles/pm.md|A `role.revoke` raise whose target is the assistant is a hard wait too:
it goes to the human, cited or not.
|
m10|roles/chief-of-staff.md|A `role.revoke` raise whose target is the assistant is a hard wait too:
it goes to the human, cited or not.
|
m11|roles/chief-of-staff.md|named the instance and the decision, `launch` for that instance was
refused `instance-running`, and the run belongs|named the instance and the decision, and the run belongs
m12|roles/chief-of-staff.md| End no other run.|
m13|roles/chief-of-staff.md|with `mosaic stop`, apart from the merge
relaunch under Duties.|with `mosaic stop`.
m14|roles/merge-gate.md|then pass `project`, `spec_path` `spec/north-star.md` and
`spec_commit`.|then pass `project`.
m15|roles/merge-gate.md|If the raise is refused `stale-spec`, read the spec
again and raise once more with the current commit. |
m16|roles/merge-gate.md|raise uncited and say so in the question.|don't raise.
EOF
)
status=0
echo "start $(date -u +%FT%TZ) head $(git -C "$WT" rev-parse --short HEAD) mode=${DRY:-run}"
if [ -z "$DRY" ]; then
(cd "$WT" && setsid -w timeout -k 10 300 node --test "$TEST") > "$OUT/mutants/control.log" 2>&1
rc=$?; echo "control rc=$rc"; [ $rc -eq 0 ] || { echo "control must pass"; exit 1; }
fi
# Records are split on a line holding only the next id: python does the parsing and the edit.
ids=$(python3 - "$MUTANTS" <<'PY'
import re, sys
for m in re.finditer(r'^(m\d+)\|', sys.argv[1], re.M): print(m.group(1))
PY
)
for id in $ids; do
file=$(python3 - "$MUTANTS" "$id" <<'PY'
import re, sys
recs = re.split(r'^(?=m\d+\|)', sys.argv[1], flags=re.M)
r = next(x for x in recs if x.startswith(sys.argv[2] + '|'))
print(r.split('|', 3)[1])
PY
) || { echo "$id: parse failed"; status=1; continue; }
cp "$WT/$file" "$OUT/backup/$id" || exit 1
before=$(sha256sum < "$WT/$file")
python3 - "$MUTANTS" "$id" "$WT/$file" <<'PY' || { echo "$id: edit refused"; status=1; continue; }
import re, sys, pathlib
recs = re.split(r'^(?=m\d+\|)', sys.argv[1], flags=re.M)
r = next(x for x in recs if x.startswith(sys.argv[2] + '|'))
_, _, old, new = r.split('|', 3)
new = new[:-1] if new.endswith('\n') else new # the record's own line end
p = pathlib.Path(sys.argv[3]); t = p.read_text()
n = t.count(old)
if n != 1: sys.exit(f"{sys.argv[2]}: {n} matches")
p.write_text(t.replace(old, new))
PY
if [ -n "$DRY" ]; then
echo "$id $file applies once"
else
(cd "$WT" && setsid -w timeout -k 10 300 node --test "$TEST") > "$OUT/mutants/$id.log" 2>&1; rc=$?
if [ $rc -ne 0 ]; then
echo "$id $file caught: $(grep -m1 -E '^not ok|✖' "$OUT/mutants/$id.log" | cut -c1-140)"
else
echo "$id $file SURVIVED"; status=1
fi
fi
cp "$OUT/backup/$id" "$WT/$file" || exit 1
[ "$(sha256sum < "$WT/$file")" = "$before" ] || { echo "$id: restore failed"; exit 1; }
done
(cd "$WT" && sha256sum -c --quiet "$OUT/../candidate-manifest.sha256") || { echo "manifest check failed after"; status=1; }
echo "end $(date -u +%FT%TZ) status=$status"
exit $status