fix(seo,geo): C1a — find sees build output, grep does not; the two disagreed
Surfaced by dogfooding on a real Astro repo instead of reading the spec. Claude Code installs a shell function routing `grep` to ugrep with `--ignore-files`, so grep honours .gitignore and never descends into a gitignored dist/. `find` honours nothing. seo-analyzer uses both. Measured on zenquality (Astro, dist/ gitignored but built locally), seo-analyzer.md:497 returned 92 images, 45 of them under dist/ — every asset listed twice, source and generated copy, byte-identical. Two live consequences: - "top 20 by size" was ~10 real images dressed as 20. - Batch C (`cwebp -q 80 <img> -o <img>.webp`) could target dist/og-image.png; the .webp lands in dist/ and the `npm run build` the dispatcher runs to VERIFY the fix erases it. Fix lands, verification passes, nothing survives, report says applied. lib/source-scope.sh separates source from build output, framework-aware. `public/` is deliberately NOT excluded by default: it is Astro/Vite/Next SOURCE and holds favicon.ico, apple-touch-icon.png and robots.txt — the very files STEP 4 curls. It is build output only for Hugo/Gatsby, detected from config (legacy config.toml alone is ambiguous, so it needs archetypes/ too). Blanket-excluding it would blind the audit to its own resource checks. findargs emits one token per line and MUST be consumed via a quoted array. I shipped a flat-string version first and the dogfood caught it: the shell globs */dist/* against the CWD and passes the matches to find as search paths, which turned 90 hits into 135 and kept every dist/ file. Both the header and a functional test now pin that. Also: no bundle item may target build output, in either agent. That is the real safety net — even if some future find leaks a dist path, the fix cannot land there. Fix the source that generates the artifact; if the source cannot be found, that is a finding, not a reason to patch the artifact. SCOPE CORRECTION: the proposal claimed grep was auditing 86 generated files instead of 9 templates. That was FALSE — the ugrep shim already skips them. Killed my own premise before coding it; the real bug is narrower and lives in find only. Do NOT "fix" the grep lines to match: they are already correct, and adding these exclusions there would drop public/. Verified: 90 -> 45 images on the real repo, 0 dist survivors, public/ preserved (favicon.ico still visible); 34 new assertions PASS / 0 FAIL; full suite green; shellcheck clean. Test file addition used the documented one-shot config-edit sentinel.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/source-scope.test.sh
|
||||
set -u
|
||||
S="$(cd "$(dirname "$0")/../.." && pwd)/lib/source-scope.sh"
|
||||
pass=0; fail=0
|
||||
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
|
||||
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
|
||||
# does `list` (run inside dir $1) contain the name $2?
|
||||
listed() { ( cd "$1" && bash "$S" list 2>/dev/null | grep -qxF "$2" ) \
|
||||
&& echo yes || echo no; }
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
# --- always-excluded build output + caches ---
|
||||
mkdir -p "$TMP/plain"
|
||||
for d in node_modules .git dist build .next .nuxt .output _site .astro \
|
||||
.svelte-kit .cache out coverage .vercel .netlify .turbo; do
|
||||
check "A-$d-listed" "$(listed "$TMP/plain" "$d")" yes
|
||||
done
|
||||
|
||||
# --- public/ is SOURCE by default: Astro/Vite/Next keep favicon.ico,
|
||||
# apple-touch-icon.png and robots.txt there, and the audit checks them ---
|
||||
check B1-public-kept-by-default "$(listed "$TMP/plain" public)" no
|
||||
|
||||
# --- public/ is OUTPUT for Gatsby and Hugo only ---
|
||||
mkdir -p "$TMP/gatsby"; : > "$TMP/gatsby/gatsby-config.js"
|
||||
check B2-gatsby-js "$(listed "$TMP/gatsby" public)" yes
|
||||
mkdir -p "$TMP/gatsby2"; : > "$TMP/gatsby2/gatsby-config.ts"
|
||||
check B3-gatsby-ts "$(listed "$TMP/gatsby2" public)" yes
|
||||
mkdir -p "$TMP/hugo"; : > "$TMP/hugo/hugo.toml"
|
||||
check B4-hugo-toml "$(listed "$TMP/hugo" public)" yes
|
||||
mkdir -p "$TMP/hugo2"; : > "$TMP/hugo2/hugo.yaml"
|
||||
check B5-hugo-yaml "$(listed "$TMP/hugo2" public)" yes
|
||||
# legacy config.toml alone is ambiguous (many tools use it) — needs archetypes/
|
||||
mkdir -p "$TMP/amb"; : > "$TMP/amb/config.toml"
|
||||
check B6-config-toml-alone-is-ambiguous "$(listed "$TMP/amb" public)" no
|
||||
mkdir -p "$TMP/hugo3/archetypes"; : > "$TMP/hugo3/config.toml"
|
||||
check B7-config-toml-plus-archetypes "$(listed "$TMP/hugo3" public)" yes
|
||||
|
||||
# --- grep mode: flags, and no glob character (safe unquoted) ---
|
||||
G="$(cd "$TMP/plain" && bash "$S" grep)"
|
||||
case "$G" in *--exclude-dir=dist*) check C1-grep-has-dist ok ok ;;
|
||||
*) check C1-grep-has-dist "missing" ok ;; esac
|
||||
case "$G" in *"*"*) check C2-grep-has-no-glob "has-glob" ok ;;
|
||||
*) check C2-grep-has-no-glob ok ok ;; esac
|
||||
|
||||
# --- findargs: one token per line, 3 tokens per dir ---
|
||||
N="$(cd "$TMP/plain" && bash "$S" findargs | wc -l)"
|
||||
D="$(cd "$TMP/plain" && bash "$S" list | wc -l)"
|
||||
check D1-findargs-3-tokens-per-dir "$N" "$((D * 3))"
|
||||
check D2-findargs-first-token "$(cd "$TMP/plain" && bash "$S" findargs | head -1)" '!'
|
||||
|
||||
# --- FUNCTIONAL: the array form actually excludes build output ---
|
||||
# A flat unquoted string does NOT work here: the shell globs */dist/* against
|
||||
# the CWD and passes the matches to find as search paths. Measured on a real
|
||||
# repo, that turned 90 hits into 135 and kept every dist/ file.
|
||||
W="$TMP/work"; mkdir -p "$W/src" "$W/dist" "$W/public" "$W/node_modules"
|
||||
: > "$W/src/a.png"; : > "$W/dist/a.png"; : > "$W/public/favicon.ico"
|
||||
: > "$W/node_modules/dep.png"
|
||||
cd "$W" || exit 1
|
||||
mapfile -t FEXCL < <(bash "$S" findargs)
|
||||
check E1-excludes-dist "$(find . "${FEXCL[@]}" -name 'a.png' | grep -c '/dist/')" 0
|
||||
check E2-keeps-src "$(find . "${FEXCL[@]}" -name 'a.png' | grep -c '/src/')" 1
|
||||
check E3-excludes-nodem "$(find . "${FEXCL[@]}" -name '*.png' | grep -c 'node_modules')" 0
|
||||
# public/ survives: the audit's own resource checks live there
|
||||
check E4-keeps-public "$(find . "${FEXCL[@]}" -name 'favicon.ico' | wc -l)" 1
|
||||
cd / || exit 1
|
||||
|
||||
# --- usage ---
|
||||
bash "$S" >/dev/null 2>&1; check X1-no-args "$?" 2
|
||||
bash "$S" bogus >/dev/null 2>&1; check X2-bad-verb "$?" 2
|
||||
# `find` was renamed to `findargs` when the flat-string form proved unsafe
|
||||
bash "$S" find >/dev/null 2>&1; check X3-old-find-verb-gone "$?" 2
|
||||
|
||||
rm -rf "$TMP"
|
||||
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
||||
Reference in New Issue
Block a user