Fix style gate: file-level --exclude was cancelling every --include

The gate reported 2277 lines of findings and had become unreadable, which read as accumulated debt. It was a tooling bug. In this grep build a single file-level --exclude= silently cancels every --include= on the same command; --exclude-dir= is unaffected. Two innocuous entries, --exclude=findings.sarif and --exclude=report.md, had therefore been disabling the include filters on every check that used the EXCLUDES array. That is why a check whose flags say .py/.vue/.js/.ts was reporting .md, .ps1, .json, .gitignore and vendored DLLs: it was scanning the whole tree including build artifacts.

Removes all file-level excludes and reproduces their intent with a drop_generated post-filter, adds grep -I so binaries can never be reported (there is no line to fix in a DLL), excludes bin/obj/publish/dev-bin and exports/backups, and adds the drop_scratch the shorthand loop was missing while every other check had it. Findings drop from 2277 lines to 11 with all ten checks passing. Verified with a planted canary file that the gate still catches cfg, a snake_case __tablename__ and a snake_case db.Column - a green gate immediately after a filtering change is worth distrusting until shown to still fail. Do not re-add --exclude=<file> to that array.
This commit is contained in:
benjamin.baker
2026-09-07 12:39:40 -04:00
parent 63ce75f211
commit ea3739262b

View File

@@ -34,16 +34,51 @@ EXCLUDES=(
--exclude-dir=bundle
--exclude-dir=dist-subpath
--exclude-dir=.git
# Visual Studio's local state, gitignored. CopilotSnapshots keeps a full
# per-edit copy of touched source files, so every real finding is reported
# again once per snapshot - hundreds of duplicates that bury the actual
# hits and make the scan take minutes instead of seconds.
--exclude-dir=.vs
--exclude-dir=versions
--exclude-dir=staticdocs
# Local, gitignored scratch notes (see .gitignore devnotes/). Not product
# source, never committed, and free to hold pasted logs full of em-dashes
# and arrows - grep does not consult .gitignore, so exclude it explicitly.
--exclude-dir=devnotes
--exclude=findings.sarif
--exclude=report.md
# .NET build output. The agent projects emit vendored runtime DLLs and
# generated *.nuget.g.props/.targets under bin/ and obj/; both are full of
# bytes nobody here wrote. Same failure as the .vs case above: thousands of
# hits that bury every real finding and make the gate unreadable, so it
# stops being run at all.
--exclude-dir=bin
--exclude-dir=obj
--exclude-dir=publish
--exclude-dir=dev-bin
# Data, not source: exported CSVs carry vendor mojibake and backups are
# snapshots of a moment. Neither is something a convention applies to.
--exclude-dir=exports
--exclude-dir=backups
)
# DO NOT ADD --exclude=<file> TO THE ARRAY ABOVE.
#
# In this grep build a single file-level --exclude silently CANCELS EVERY
# --include filter on the same command. Bisected: with only --exclude-dir flags
# a shorthand scan restricted to --include='*.py' returns 0 markdown hits; add
# any one --exclude and the same scan returns 1142. Directory excludes are
# unaffected - it is specifically the file form.
#
# This is not theoretical, it was live: --exclude=findings.sarif and
# --exclude=report.md sat in this array and had been quietly disabling the
# include lists for every check that used them. That is why the shorthand check
# reported .md, .ps1, .cs, .json and .gitignore hits when its flags say it only
# ever looks at .py/.vue/.js/.ts, and why the gate accumulated thousands of
# unactionable findings that made it unreadable enough to be ignored.
#
# To skip a FILE, filter it out of the pipeline afterwards (see drop_scratch and
# the post-filters on the ADR-015 check) or exclude the directory holding it.
# Binaries are handled by grep -I on the scan itself.
# Root-level gitignored scratch: review writeups and chat handoffs, free to hold
# pasted logs full of em-dashes and arrows. grep does not consult .gitignore, so
# it has to be dropped here.
@@ -54,6 +89,15 @@ EXCLUDES=(
# that nothing would report. This drops only the path we mean.
drop_scratch() { grep -v '^\./devnotes/' || true; }
# Generated scan output, replacing the --exclude=findings.sarif and
# --exclude=report.md that used to live in EXCLUDES. Same intent, but as a
# post-filter, because the flag form cancelled every --include on the command
# (see the warning above the EXCLUDES array). Also drops .env, which is local
# configuration rather than source.
drop_generated() {
grep -vP '^\./(.*/)?(findings\.sarif|report\.md|\.env):' || true
}
INCLUDES_CODE=(
--include='*.py'
--include='*.vue'
@@ -77,7 +121,11 @@ INCLUDES_ALL=(
# and did, including in files this repo's own convention forbids them in.
# Markdown, JSON and YAML are now covered too.
echo "==> Checking for non-ASCII characters..."
NON_ASCII=$(grep -rPn '[^\x00-\x7F]' "${EXCLUDES[@]}" "${INCLUDES_ALL[@]}" . 2>/dev/null | drop_scratch || true)
# -I skips binary files. Without it grep reports "Binary file X matches" for any
# archive or compiled artifact that slips past the excludes, which is noise no
# author can act on: there is no line to fix. The excludes above are the primary
# defence; this is the backstop for whatever they miss.
NON_ASCII=$(grep -rPnI '[^\x00-\x7F]' "${EXCLUDES[@]}" "${INCLUDES_ALL[@]}" . 2>/dev/null | drop_scratch | drop_generated || true)
if [ -n "$NON_ASCII" ]; then
echo "FAIL: non-ASCII characters found (em-dashes, smart quotes, arrows, emojis):"
echo "$NON_ASCII"
@@ -92,6 +140,8 @@ echo "==> Checking for banned shorthand (standalone)..."
for word in cfg ctx mgr req res; do
HITS=$(grep -rPn "(^|[^a-zA-Z0-9_])${word}([^a-zA-Z0-9_]|\$)" "${EXCLUDES[@]}" --include='*.py' --include='*.vue' --include='*.js' --include='*.ts' . 2>/dev/null \
| grep -vP "(^|[^a-zA-Z0-9_])(request_obj|response_obj)" \
| drop_scratch \
| drop_generated \
|| true)
if [ -n "$HITS" ]; then
echo "FAIL: banned shorthand '$word' (standalone) found:"