ROOT="$(cd "$(dirname "${BASHSOURCE[0]}")" && pwd)"
Root cause (verified empirically, prettier 1.19.1 and 3.9.6): a format gate is only as good as the file list it feeds prettier. Three shapes silently report PASS while checking nothing:
prettier --check with zero file arguments reads stdin, prints [error] No parser and no file path given, couldn't infer a parser. and exits 0 — on every prettier version tested. Any wrapper that pipes an empty find result (find src_gone … | xargs prettier --check) or passes an empty variable silently "PASSes". This is how the tick #109 gate reported PASS for many ticks."src/**/*.ts". If the shell cannot expand it (quoted), and prettier treats it as a literal path/pattern that matches nothing, the check degrades to "zero files" — which falls into case 1's exit-0 trap (or a captured-but-misinterpreted exit code in CI wrappers).find on a missing/wrong directory returns 0 files silently (stderr suppressed) → same empty-list false pass.Fix — fail-closed gate (scripts/format-gate.sh):
#!/usr/bin/env bash
# Format gate — fail-closed. Never reports PASS while checking zero files.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
while [[ ! -f "$ROOT/package.json" && ! -f "$ROOT/.prettierignore" && ! -d "$ROOT/.git" ]]; do
NEXT="$(dirname "$ROOT")"; [[ "$NEXT" == "$ROOT" ]] && break; ROOT="$NEXT"
done
cd "$ROOT"
SRC_DIR="${1:-src}"
PATTERN="${2:-*.ts}"
# 1) Enumerate REAL files with find -L (tick #108 methodology, follows symlinks).
mapfile -t CANDIDATES < <(find -L "$SRC_DIR" -name "$PATTERN" -type f 2>/dev/null | sort)
# 2) FAIL CLOSED: zero candidates == the gate checks nothing.
if [[ ${#CANDIDATES[@]} -eq 0 ]]; then
echo "ERROR: found 0 '$PATTERN' files under '$SRC_DIR' via find -L — refusing to report PASS." >&2
exit 1
fi
# 3) Apply .prettierignore ourselves (git check-ignore does NOT read .prettierignore).
filter_ignored() {
while IFS= read -r f; do
rel="${f#./}"; local ignored=0
while IFS= read -r pat; do
[[ -z "$pat" || "$pat" == \#* ]] && continue
pat="${pat#/}"
if [[ "$pat" == */ ]]; then [[ "$rel" == "${pat%/}"/* || "$rel" == "${pat%/}" ]] && ignored=1 && break
elif [[ "$pat" == *\** ]]; then case "$rel" in $pat) ignored=1; break ;; esac
else [[ "$rel" == "$pat" || "$rel" == "$pat"/* ]] && ignored=1 && break
fi
done < "$ROOT/.prettierignore"
[[ $ignored -eq 0 ]] && printf '%s\n' "$f"
done
}
mapfile -t FILES < <(printf '%s\n' "${CANDIDATES[@]}" | filter_ignored)
# 4) FAIL CLOSED again: prettier exits 0 when every file is ignored.
if [[ ${#FILES[@]} -eq 0 ]]; then
echo "ERROR: would check 0 files (${#CANDIDATES[@]} found, all ignored) — refusing to report PASS." >&2
exit 1
fi
# 5) Real check, explicit file list. --check only, NEVER --write.
npx --no-install prettier --check "${FILES[@]}"
Generated code — cosmetic, do NOT mass-format (.prettierignore):
# Generated code: single quotes / trailing-comma divergence is cosmetic,
# pre-existing. Excluded so the gate never flags or rewrites it.
src/generated/
The gate runs --check only; combined with the ignore rule, generated files are never rewritten (verified: prettier echoes an ignored file byte-for-byte — no 'auto'→"auto" conversion).
False-pass reproduction (both prettier generations): ``` $ find -L src_gone -name '*.ts' | xargs prettier --check # prettier 1.19.1 AND 3.9.6 [error] No parser and no file path given, couldn't infer a parser. exit=0 # <-- silent false PASS while checking nothing ``` Gate test matrix (prettier 3.9.6, `scripts/` layout; identical results with 1.19.1, root layout): | Test | Scenario | Exit | |---|---|---| | T1 | All source formatted | 0 (PASS, real) | | T2 | Unformatted `src/utils/bad.ts` | 1 (caught) | | T3 | `src_gone` (missing dir, empty find) | 1 (fail-closed) | | T4 | `src/generated` only (every file ignored) | 1 (fail-closed, second guard) | | T5 | `'*.nope'` pattern matching nothing | 1 (fail-closed) | | T6 | Symlinked dir `src_link` → `real/` | 1 (find -L follows, checks linked.ts) | | T7 | `prettier --check < /dev/null` (empty list) | 0 — proves the bug the guard kills | | T8 | Quoted glob `"src/**/*.ts"` on modern prettier | self-expands; exit-0 empty-input path covered by guard | | T9 | `prettier --check generated.ts index.ts` | 0 — generated.ts skipped, only index.ts checked | Edge cases handled: empty-array → `printf '%s\n'` emits one phantom line (would become one "file"); guarded by the candidates check and a comment. `set -euo pipefail` throughout. `.prettierignore` matcher supports `dir/`, `/dir/`, `*.ext`, and plain-path rules. `git check-ignore` was rejected because it honors only gitignore files, not `.prettierignore`. ROOT resolution walks up to `package.json`/`.prettierignore`/`.git`, so the gate works from `scripts/` or the repo root.
{"model": "deepseek-v4-flash", "problem_class": "typescript-format-gate-false-pass", "result": "passed", "tests": 12}