The gap-only manifest stopped guarding a case once it was fixed, so later work silently regressed previously-fixed cases (caught only by an external full-suite run, not the gated sweep or full corpus). Replace it with a full manifest of all 402 official cases (Tests/Fixtures/yaml-test-suite-manifest.yaml), each carrying its conformant expectation and a passing/gap status, with every case's files vendored. The gated sweep now asserts every passing case still passes (the regression guard) and every gap still reproduces, so any regression fails the build. The honest gate-enforced count is 329/402 passing (the earlier 348 came from a more lenient one-time oracle). Repairs three regressions the new gate revealed: the bucket-6 tab rule over-rejected tabs in block-scalar content and as separation before a scalar value (root cause: the batch scanner and the incremental TokenCursor had divergent line-start loops; unified onto a single scanStep, fixing 96NN/00, 96NN/01, R4YG, Y79Y/001, DK95/00, DK95/03, DK95/07, 6CA3, Q5MG, J3BT); an anchor on a prior line before an alias key wrongly rejected 26DV; and a tag-only flow key mis-parsed WZ62. Genuine tab-indentation errors (4EJS, Y79Y/000) still reject.
234 lines
8.0 KiB
Bash
Executable File
234 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# pre-commit hook: reject style tells in staged file content.
|
|
#
|
|
# Companion to commit-msg-style-check (which scans the commit message).
|
|
# Scans the staged diff for the same forbidden patterns:
|
|
# - Em-dash (U+2014). github-discipline.md Rule 5.2.
|
|
# - AI-attribution literal phrases. github-discipline.md Rule 5.1.
|
|
# - AI-signature emojis used as flourish.
|
|
# - AI-tell preamble phrasings at line start.
|
|
#
|
|
# Scope: only ADDED lines (`^+` in the staged diff, excluding file headers).
|
|
# This means existing offending content stays put; only new content is gated.
|
|
# A future "sweep" task can clean up legacy content; this hook prevents new
|
|
# violations from entering staged commits.
|
|
#
|
|
# Install (per repo): git config core.hooksPath .githooks
|
|
#
|
|
# Skip mechanism (for legitimate meta-references that need to quote the
|
|
# forbidden tokens, like the rule documentation itself):
|
|
# Any line containing `# style-check: allow` is skipped. Use sparingly.
|
|
#
|
|
# Excluded paths (never scanned):
|
|
# - Generated lockfiles: Cargo.lock, Package.resolved, package-lock.json,
|
|
# pnpm-lock.yaml, yarn.lock, uv.lock, Pipfile.lock, poetry.lock, Gemfile.lock
|
|
# - Binary files (detected via `git diff --numstat` reporting `-` width)
|
|
# - Vendored / third-party directories: vendor/, third_party/, node_modules/,
|
|
# _archive/python-reference/ (frozen senko snapshot we don't author)
|
|
# - The rule files that DEFINE the prohibitions: github-discipline.md and
|
|
# similar (would otherwise self-flag for citing the forbidden strings)
|
|
#
|
|
# Portable: BSD-compatible grep, no PCRE.
|
|
|
|
set -e
|
|
|
|
# Skip when there's nothing staged (e.g. `git commit --amend` with no index).
|
|
if git diff --cached --name-only --diff-filter=ACMR | head -1 | grep -q .; then
|
|
:
|
|
else
|
|
exit 0
|
|
fi
|
|
|
|
# Build the list of staged text files that should be scanned.
|
|
# `--diff-filter=ACMR` picks Added/Copied/Modified/Renamed; we skip Deleted.
|
|
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR)
|
|
|
|
# Exclusion path patterns. A staged file path that matches any of these is
|
|
# skipped entirely. Match against the path-from-repo-root, no leading slash.
|
|
SKIP_PATTERNS=(
|
|
# Lockfiles (generated, not human-authored).
|
|
'Cargo\.lock$'
|
|
'Package\.resolved$'
|
|
'package-lock\.json$'
|
|
'pnpm-lock\.yaml$'
|
|
'yarn\.lock$'
|
|
'uv\.lock$'
|
|
'Pipfile\.lock$'
|
|
'poetry\.lock$'
|
|
'Gemfile\.lock$'
|
|
'composer\.lock$'
|
|
# Vendored / third-party.
|
|
'^vendor/'
|
|
'^third_party/'
|
|
'^node_modules/'
|
|
'^_archive/'
|
|
'^Tests/Fixtures/acme/'
|
|
'^Tests/Fixtures/real-yaml/'
|
|
'^Tests/Fixtures/yaml-test-suite/'
|
|
# Rule files that DEFINE the prohibitions; they MUST be able to cite
|
|
# the forbidden strings to document them.
|
|
'/github-discipline\.md$'
|
|
'/commit-msg-style-check$'
|
|
'/pre-commit-style-check$'
|
|
# Same hooks installed under the public-kit names (.githooks/commit-msg,
|
|
# .githooks/pre-commit) rather than the canonical *-style-check names.
|
|
'/commit-msg$'
|
|
'/pre-commit$'
|
|
'/pre-push$'
|
|
'/check-style\.sh$'
|
|
'/check-staged-style\.sh$'
|
|
# The CI workflow that enforces the same Cursor prohibition server-side; it
|
|
# must quote the forbidden tokens to scan for them.
|
|
'^\.github/workflows/no-cursor-attribution\.yml$'
|
|
)
|
|
|
|
# Filter STAGED_FILES through the skip list.
|
|
SCAN_FILES=""
|
|
while IFS= read -r f; do
|
|
[ -z "$f" ] && continue
|
|
skipped=0
|
|
for pat in "${SKIP_PATTERNS[@]}"; do
|
|
if printf '%s' "$f" | grep -qE -- "$pat"; then
|
|
skipped=1
|
|
break
|
|
fi
|
|
done
|
|
[ "$skipped" -eq 1 ] && continue
|
|
SCAN_FILES="${SCAN_FILES}${f}"$'\n'
|
|
done <<<"$STAGED_FILES"
|
|
|
|
# Exit clean if nothing to scan.
|
|
if [ -z "${SCAN_FILES//[$'\n\t ']}" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# Helper: get the ADDED lines (starting with `+` but not the `+++` file header)
|
|
# from the staged diff for a specific file, with their line numbers.
|
|
# Output format: `<line-number>:<content-after-the-plus>`.
|
|
added_lines_for() {
|
|
local file="$1"
|
|
# `git diff --cached -U0` minimises context; `--no-color` keeps output clean.
|
|
git diff --cached -U0 --no-color -- "$file" \
|
|
| awk '
|
|
/^\+\+\+ / { next } # skip the +++ file header
|
|
/^@@ / {
|
|
# Parse the hunk header "@@ -a,b +c,d @@" to seed line numbers.
|
|
# Find "+c" or "+c,d" and extract c.
|
|
match($0, /\+[0-9]+/)
|
|
ln = substr($0, RSTART+1, RLENGTH-1) + 0
|
|
next
|
|
}
|
|
/^\+/ {
|
|
# An added line. Print it (without the leading +) with line number.
|
|
printf "%d:%s\n", ln, substr($0, 2)
|
|
ln++
|
|
}
|
|
/^[^+]/ {
|
|
# Context or removed line. Context lines are normally suppressed
|
|
# by -U0, but advance the counter if any do appear.
|
|
if (/^ /) ln++
|
|
}
|
|
'
|
|
}
|
|
|
|
FAIL=0
|
|
EMDASH=$(printf '\xe2\x80\x94')
|
|
|
|
# AI-attribution literal phrases (mirror commit-msg-style-check).
|
|
LITERAL_PATTERNS=(
|
|
'Co-Authored-By: Claude'
|
|
'Co-authored-by: Claude'
|
|
'Generated with Claude Code'
|
|
'Generated with Claude'
|
|
'Generated by Claude'
|
|
'with help from Claude'
|
|
'Powered by Claude'
|
|
'powered by Claude'
|
|
'as an AI'
|
|
'As an AI'
|
|
'this commit was generated'
|
|
'This commit was generated'
|
|
'this change was generated'
|
|
'This change was generated'
|
|
)
|
|
|
|
# Cursor-attribution literal tells (case-insensitive at the call site).
|
|
CURSOR_LITERALS=(
|
|
'Co-Authored-By: Cursor'
|
|
'Co-authored-by: Cursor'
|
|
'Generated with Cursor'
|
|
'Generated by Cursor'
|
|
'with help from Cursor'
|
|
'Powered by Cursor'
|
|
)
|
|
|
|
# AI-tell signature emojis (UTF-8 byte sequences kept in a variable so the
|
|
# script source stays clean of the glyphs themselves).
|
|
EMOJI_TELLS=$(printf '\xf0\x9f\xa4\x96\n\xe2\x9c\xa8\n\xf0\x9f\xaa\x84\n\xf0\x9f\xa7\xa0\n\xf0\x9f\xa6\xbe\n\xf0\x9f\xa4\x9d')
|
|
|
|
# Scan each file's added lines.
|
|
while IFS= read -r file; do
|
|
[ -z "$file" ] && continue
|
|
test -f "$file" || continue
|
|
|
|
# Get added lines for this file, then filter out lines marked
|
|
# `# style-check: allow`.
|
|
ADDED=$(added_lines_for "$file" | grep -vF -- '# style-check: allow' || true)
|
|
[ -z "$ADDED" ] && continue
|
|
|
|
# Em-dash check.
|
|
if printf '%s' "$ADDED" | LC_ALL=C grep -qF -- "$EMDASH"; then
|
|
printf 'pre-commit: forbidden em dash (U+2014) in staged %s.\n' "$file" >&2
|
|
printf ' Replace with comma, period, colon, semicolon, or restructure.\n' >&2
|
|
printf ' Rule: github-discipline.md Rule 5.2\n' >&2
|
|
printf ' Locations (line numbers in the working tree):\n' >&2
|
|
printf '%s\n' "$ADDED" | LC_ALL=C grep -F -- "$EMDASH" | sed 's/^/ /' >&2
|
|
FAIL=1
|
|
fi
|
|
|
|
# AI-attribution literal-phrase check.
|
|
for p in "${LITERAL_PATTERNS[@]}"; do
|
|
if printf '%s' "$ADDED" | grep -qF -- "$p"; then
|
|
printf 'pre-commit: forbidden AI-attribution phrase in staged %s: %s\n' "$file" "$p" >&2
|
|
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
|
|
FAIL=1
|
|
fi
|
|
done
|
|
|
|
# Cursor-attribution check (mirrors commit-msg). Specific tells only, never
|
|
# the bare word "cursor" (a legitimate programming term: text/db cursor).
|
|
for p in "${CURSOR_LITERALS[@]}"; do
|
|
if printf '%s' "$ADDED" | grep -qiF -- "$p"; then
|
|
printf 'pre-commit: forbidden Cursor attribution in staged %s: %s\n' "$file" "$p" >&2
|
|
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
|
|
FAIL=1
|
|
fi
|
|
done
|
|
if printf '%s' "$ADDED" | grep -qiE -- '@cursor\.com'; then
|
|
printf 'pre-commit: forbidden Cursor email (@cursor.com) in staged %s.\n' "$file" >&2
|
|
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
|
|
FAIL=1
|
|
fi
|
|
|
|
# AI-signature emoji check.
|
|
while IFS= read -r emoji; do
|
|
[ -z "$emoji" ] && continue
|
|
if LC_ALL=C printf '%s' "$ADDED" | grep -qF -- "$emoji"; then
|
|
printf 'pre-commit: forbidden AI-signature emoji in staged %s.\n' "$file" >&2
|
|
printf ' Rule: github-discipline.md Rule 5.1\n' >&2
|
|
FAIL=1
|
|
break
|
|
fi
|
|
done <<<"$EMOJI_TELLS"
|
|
done <<<"$SCAN_FILES"
|
|
|
|
if [ "$FAIL" -ne 0 ]; then
|
|
printf '\n' >&2
|
|
printf 'Hook scope: only ADDED lines in the staged diff are gated.\n' >&2
|
|
printf 'Existing offending content is grandfathered until a sweep is run.\n' >&2
|
|
printf 'To temporarily bypass (use sparingly): add `# style-check: allow`\n' >&2
|
|
printf 'on the same line, or `git commit --no-verify` (logged in audit).\n' >&2
|
|
fi
|
|
|
|
exit $FAIL
|