diff --git a/plugins/ai-slop/.claude-plugin/plugin.json b/plugins/ai-slop/.claude-plugin/plugin.json index 7c6f435b05..87225f0dcb 100644 --- a/plugins/ai-slop/.claude-plugin/plugin.json +++ b/plugins/ai-slop/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "ai-slop", - "version": "0.3.8", + "version": "0.3.9", "description": "Detects and removes AI-writing tells (slop) in checked-in markdown prose: em dashes, emoji formatting, AI vocabulary, negative parallelisms, chatbot phrases, filler, stacked hedging, citation artifacts, and the rest of a catalog distilled from Wikipedia's Signs of AI writing. Read-only audit by default with a deterministic detector plus a judgment rubric; an explicit fix action rewrites findings behind a semantic-diff guard. Findings conform to the detector-findings convention so the review fanout fix relay can consume them.", "author": { "name": "Melodic Software", diff --git a/plugins/ai-slop/CHANGELOG.md b/plugins/ai-slop/CHANGELOG.md index d99231567d..170a0e58d6 100644 --- a/plugins/ai-slop/CHANGELOG.md +++ b/plugins/ai-slop/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [0.3.9] + +- **Three directory-expansion defects survived 0.3.8.** A tracked file + whose name held a non-ASCII byte was dropped with no trace, because + `git ls-files` C-quotes those paths unless told otherwise; the listing + now sets `core.quotePath=false` and stays newline-delimited. A + filesystem walk still ran when git was missing or could not confirm a + work tree, while the comment claimed that case was gone; the walk + remains the fallback when tracked-files-only is not achievable, and + that fallback is now reported on stderr (`git is not on PATH`, or + `git could not confirm a work tree`). The new dir-target tests now + assert the emitted `file=` path, not only the scan count, so rebuilding + paths from `git rev-parse --show-toplevel` fails the suite; they also + pin subtree restriction and a non-ASCII filename. Drive-root slash + preservation from 0.3.8 is unchanged. + ## [0.3.8] - **Directory targets silently fell back to an untracked-inclusive filesystem diff --git a/plugins/ai-slop/skills/audit/scripts/detect.sh b/plugins/ai-slop/skills/audit/scripts/detect.sh index 4a16cc6e33..a45a83fcdb 100755 --- a/plugins/ai-slop/skills/audit/scripts/detect.sh +++ b/plugins/ai-slop/skills/audit/scripts/detect.sh @@ -266,14 +266,23 @@ fi # prefix from `git rev-parse --show-toplevel` and its filter from `pwd`, so on # any host where those disagree no candidate survived the filter and the walk # below silently replaced the tracked-files listing it was meant to back up. +# `ls-files` C-quotes any path holding a non-ASCII byte unless +# `core.quotePath=false`, so a tracked `café.md` (or a filename that itself +# holds an em dash) would arrive as a literal quoted escape, fail the scan +# loop's existence test, and produce neither a finding nor a declined row. +# # Running `ls-files` with `-C ` needs neither: it is already # restricted to that directory's subtree and answers in paths relative to it, # so `` is the only anchor and cannot disagree with itself. # # The branch is chosen up front from `--is-inside-work-tree`, never from an -# empty pipeline, so a walk is only ever the answer for a directory that is -# genuinely outside a checkout. Inside one, a listing that fails says so on -# stderr rather than degrading into a different set of files. +# empty pipeline. A silent walk is only the answer when git is present and +# reports the directory is genuinely outside a checkout. If git is missing, +# or git cannot confirm a work tree (safe.directory refusal, unreadable +# .git, nonzero rev-parse), the walk still runs because tracked-files-only +# is not achievable, and that fallback is reported on stderr. Inside a +# confirmed checkout, a listing that fails says so on stderr rather than +# degrading into a different set of files. # # Strip one trailing slash, except when that would turn a Windows drive root # (`C:/`) into a drive-relative path (`C:`). Windows then treats the target as @@ -293,13 +302,25 @@ expand_dir_target() { local dir inside listing status dir="$(normalize_dir_target "$1")" - inside="$(git -C "$dir" rev-parse --is-inside-work-tree 2>/dev/null || true)" + if ! command -v git >/dev/null 2>&1; then + echo "detect.sh: git is not on PATH; directory $dir expanded via filesystem walk (tracked-files-only is not achievable)" >&2 + find "$dir" -name '*.md' -type f 2>/dev/null + return 0 + fi + + inside="$(git -C "$dir" rev-parse --is-inside-work-tree 2>/dev/null)" + status=$? + if [[ "$status" -ne 0 ]]; then + echo "detect.sh: git could not confirm a work tree under $dir (exit $status); expanding via filesystem walk" >&2 + find "$dir" -name '*.md' -type f 2>/dev/null + return 0 + fi if [[ "$inside" != "true" ]]; then find "$dir" -name '*.md' -type f 2>/dev/null return 0 fi - listing="$(git -C "$dir" ls-files '*.md')" + listing="$(git -C "$dir" -c core.quotePath=false ls-files '*.md')" status=$? if [[ "$status" -ne 0 ]]; then echo "detect.sh: git ls-files failed under $dir (exit $status); that directory expanded to nothing" >&2 diff --git a/plugins/ai-slop/skills/audit/scripts/detect.test.sh b/plugins/ai-slop/skills/audit/scripts/detect.test.sh index 512bbe2194..2cb8d88ba7 100755 --- a/plugins/ai-slop/skills/audit/scripts/detect.test.sh +++ b/plugins/ai-slop/skills/audit/scripts/detect.test.sh @@ -403,9 +403,14 @@ cat >"$GITDIR/docs/untracked.md" <"$GITDIR/sibling.md" <&1)" -assert_contains "dir target in git repo: tracked file scanned via ls-files" "$out" "tracked.md" +assert_contains "dir target in git repo: tracked file scanned via ls-files" "$out" "file=$GITDIR/docs/tracked.md" assert_contains "dir target in git repo: only the tracked file counts" "$out" "1 files scanned" +assert_not_contains "dir target in git repo: sibling outside the subtree is not scanned" "$out" "sibling.md" # Path agreement. One directory has several SPELLINGS on Git Bash: git answers # the Windows form of the same checkout a shell reaches as "/tmp/...". An @@ -420,15 +425,19 @@ GITSPELL="$(git -C "$GITDIR/docs" rev-parse --show-toplevel)/docs" PWDSPELL="$(cd "$GITDIR/docs" && pwd)" out="$(bash "$DETECT" "$GITSPELL" 2>&1)" +assert_contains "dir target, git spelling: the emitted path keeps that spelling" "$out" "file=$GITSPELL/tracked.md" assert_contains "dir target, git spelling: only the tracked file counts" "$out" "1 files scanned" out="$(bash "$DETECT" "$PWDSPELL" 2>&1)" +assert_contains "dir target, shell spelling: the emitted path keeps that spelling" "$out" "file=$PWDSPELL/tracked.md" assert_contains "dir target, shell spelling: only the tracked file counts" "$out" "1 files scanned" # A trailing slash is the same directory and must expand identically; the # expansion builds paths by concatenation, so an unnormalized target would emit # a doubled separator and scan nothing. out="$(bash "$DETECT" "$GITDIR/docs/" 2>&1)" +assert_contains "dir target with a trailing slash: the emitted path is not doubled" "$out" "file=$GITDIR/docs/tracked.md" +assert_not_contains "dir target with a trailing slash: no doubled separator" "$out" "file=$GITDIR/docs//tracked.md" assert_contains "dir target with a trailing slash: only the tracked file counts" "$out" "1 files scanned" # Drive-root slash preservation is a string contract, not a host contract: the @@ -456,6 +465,28 @@ EOF out="$(bash "$DETECT" "$GITDIR/untrackedonly" 2>&1)" assert_contains "dir target in git repo, no tracked markdown: expands to nothing" "$out" "0 files scanned" +# git C-quotes non-ASCII path bytes unless core.quotePath=false. A tracked +# filename holding an em dash would then fail the scan loop's existence test +# and vanish from the report. The listing must emit the raw filename. +mkdir -p "$GITDIR/unicode" +printf 'A tracked em dash %s here.\n' "$EM" >"$GITDIR/unicode/dash${EM}name.md" +git -C "$GITDIR" add "unicode/dash${EM}name.md" +out="$(bash "$DETECT" "$GITDIR/unicode" 2>&1)" +assert_contains "dir target, non-ASCII filename: the raw path is scanned" "$out" "file=$GITDIR/unicode/dash${EM}name.md" +assert_contains "dir target, non-ASCII filename: the file is not dropped" "$out" "1 files scanned" + +# git absent: tracked-files-only is not achievable, so the walk still runs, +# but the fallback must be reported rather than silent. +NOGIT_BIN="$TEST_TMPDIR/bin-nogit" +mkdir -p "$NOGIT_BIN" +for name in bash find sort awk sed cat printf mkdir uname env dirname basename head tail wc tr; do + src="$(command -v "$name" 2>/dev/null)" || continue + ln -s "$src" "$NOGIT_BIN/$name" +done +out="$(PATH="$NOGIT_BIN" bash "$DETECT" "$GITDIR/docs" 2>&1)" +assert_contains "dir target, git absent: reports the walk" "$out" "git is not on PATH" +assert_contains "dir target, git absent: walk scans tracked and untracked markdown" "$out" "2 files scanned" + # --- Excerpt truncation at the byte boundary -------------------------------------- # 79 ASCII bytes then an em dash: a byte cut at 80 would keep only the first