Files
crashtestdev/scripts/lint-voice.sh
Ken 69390e319b fix: add unnecessary negative contrast detection to voice linting
- New lint check: 'doesn't just X. They Y' pattern (Claude rhetorical crutch)
- Fixed last instance in judgment essay: 'doesn't just lose income. They lose'
  → 'loses the acknowledgment... The income is secondary.'
- Updated voice-check skill dimension 2 with the pattern description
- Updated AGENTS.md with the new check
- Tightened regex to avoid false positives on legitimate 'isn't only' framing

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-05-27 00:46:21 +00:00

180 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# lint-voice.sh -- Anti-slop linter for Crash Test Dev posts
# Usage: ./scripts/lint-voice.sh src/content/posts/my-post.md
#
# Checks for AI writing tells and voice mismatches.
# Exit code 0 = clean, 1 = issues found.
set -uo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <markdown-file>"
exit 2
fi
FILE="$1"
if [[ ! -f "$FILE" ]]; then
echo "File not found: $FILE"
exit 2
fi
# Strip frontmatter (everything between first and second ---)
BODY=$(sed -n '/^---$/,/^---$/!p' "$FILE" | tail -n +1)
WORD_COUNT=$(echo "$BODY" | wc -w)
ISSUES=0
WARNINGS=0
echo "=== Voice Lint: $(basename "$FILE") ==="
echo " Words: $WORD_COUNT"
echo ""
# --- CHECK 1: Em-dash density ---
EMDASH_COUNT=$(echo "$BODY" | grep -o '—' | wc -l)
EMDASH_DOUBLE=$(echo "$BODY" | grep -oP '(?<!\-)--(?!\-)' | wc -l)
TOTAL_DASHES=$((EMDASH_COUNT + EMDASH_DOUBLE))
if [[ $WORD_COUNT -gt 0 ]]; then
DENSITY=$(echo "scale=1; $TOTAL_DASHES * 1000 / $WORD_COUNT" | bc 2>/dev/null || echo "0")
else
DENSITY="0"
fi
if [[ $TOTAL_DASHES -gt 5 ]]; then
echo "FAIL Em-dashes: $TOTAL_DASHES found ($DENSITY per 1000 words)"
echo " Normal: 1-3 per 1000 words. Yours: $DENSITY"
echo " Lines:"
grep -n '—\|[^-]--[^-]' "$FILE" | head -20 | sed 's/^/ /'
echo ""
ISSUES=$((ISSUES + 1))
elif [[ $TOTAL_DASHES -gt 3 ]]; then
echo "WARN Em-dashes: $TOTAL_DASHES found ($DENSITY per 1000 words)"
echo " Getting close to AI territory. Consider replacing 1-2."
echo ""
WARNINGS=$((WARNINGS + 1))
else
echo "OK Em-dashes: $TOTAL_DASHES ($DENSITY per 1000 words)"
echo ""
fi
# --- CHECK 2: AI trigger words ---
TRIGGER_WORDS="delve|tapestry|multifaceted|nuanced|landscape|comprehensive|pivotal|leverage[sd]?|leveraging|robust|utilize[sd]?|utilizing|seamless|cutting-edge|game.changer|paradigm|synergy|holistic|streamline"
TRIGGERS=$(echo "$BODY" | grep -oiP "\b($TRIGGER_WORDS)\b" | sort | uniq -c | sort -rn)
if [[ -n "$TRIGGERS" ]]; then
echo "FAIL AI trigger words found:"
echo "$TRIGGERS" | sed 's/^/ /'
echo ""
ISSUES=$((ISSUES + 1))
else
echo "OK No AI trigger words"
echo ""
fi
# --- CHECK 3: Hedging phrases ---
HEDGE_PATTERNS="it.s worth noting|it is worth noting|it.s important to note|one might argue|it could be said|in today.s digital|in today.s rapidly|in the realm of|it goes without saying|at the end of the day|in an era where|it is important to understand"
HEDGES=$(echo "$BODY" | grep -oiP "$HEDGE_PATTERNS" | sort | uniq -c | sort -rn)
if [[ -n "$HEDGES" ]]; then
echo "FAIL Hedging phrases found:"
echo "$HEDGES" | sed 's/^/ /'
echo ""
ISSUES=$((ISSUES + 1))
else
echo "OK No hedging phrases"
echo ""
fi
# --- CHECK 4: Filler transitions ---
FILLER_PATTERNS="in conclusion|moving on|with that said|that being said|to summarize|in summary|as we.ve seen|as mentioned earlier|without further ado|let.s dive in|let.s delve"
FILLERS=$(echo "$BODY" | grep -oiP "$FILLER_PATTERNS" | sort | uniq -c | sort -rn)
if [[ -n "$FILLERS" ]]; then
echo "FAIL Filler transitions found:"
echo "$FILLERS" | sed 's/^/ /'
echo ""
ISSUES=$((ISSUES + 1))
else
echo "OK No filler transitions"
echo ""
fi
# --- CHECK 5: Unnecessary negative contrast ("doesn't just X, they Y") ---
NEGCONTRAST_PATTERNS="doesn.t just .{1,40}\. (They|It|She|He|We)|isn.t just .{1,40}\. (It|They)|doesn.t just .{1,40}, (but|it.s|they)|not just about .{1,40}\. (It|They)|more than just .{1,40}\. (It|They)"
NEGCONTRASTS=$(echo "$BODY" | grep -oiP "$NEGCONTRAST_PATTERNS" | sort | uniq -c | sort -rn)
if [[ -n "$NEGCONTRASTS" ]]; then
echo "WARN Unnecessary negative contrast patterns found:"
echo "$NEGCONTRASTS" | sed 's/^/ /'
echo " Pattern: \"doesn't just X. They Y\" -- Claude rhetorical crutch."
echo " Fix: State Y directly. The contrast adds nothing."
echo " Lines:"
grep -niP "$NEGCONTRAST_PATTERNS" "$FILE" | head -10 | sed 's/^/ /'
echo ""
WARNINGS=$((WARNINGS + 1))
else
echo "OK No unnecessary negative contrast"
echo ""
fi
# --- CHECK 6: Resolution closers ---
CLOSER_PATTERNS="the key takeaway|the takeaway here|at the end of the day|what this means is|the bottom line|the real question is"
CLOSERS=$(echo "$BODY" | grep -oiP "$CLOSER_PATTERNS" | sort | uniq -c | sort -rn)
if [[ -n "$CLOSERS" ]]; then
echo "WARN Resolution closer phrases found:"
echo "$CLOSERS" | sed 's/^/ /'
echo ""
WARNINGS=$((WARNINGS + 1))
else
echo "OK No resolution closers"
echo ""
fi
# --- CHECK 6: Receipts check (links and code blocks) ---
LINK_COUNT=$(echo "$BODY" | grep -oP '\[.*?\]\(http[^)]+\)' | wc -l)
URL_COUNT=$(echo "$BODY" | grep -oP 'https?://[^\s\)]+' | wc -l)
CODE_BLOCK_COUNT=$(echo "$BODY" | grep -c '^```' || true)
CODE_BLOCK_COUNT=$((CODE_BLOCK_COUNT / 2))
TOTAL_EVIDENCE=$((LINK_COUNT + URL_COUNT + CODE_BLOCK_COUNT))
if [[ $TOTAL_EVIDENCE -eq 0 && $WORD_COUNT -gt 500 ]]; then
echo "WARN No receipts: 0 links, 0 URLs, 0 code blocks in $WORD_COUNT words"
echo " Ken's voice rule: \"Show receipts. Always. Non-negotiable.\""
echo " Consider: Is this a manifesto (OK without receipts) or a war story (needs them)?"
echo ""
WARNINGS=$((WARNINGS + 1))
else
echo "OK Evidence: $LINK_COUNT links, $URL_COUNT URLs, $CODE_BLOCK_COUNT code blocks"
echo ""
fi
# --- CHECK 7: Sentence length variation ---
# Count sentences (rough: split on . ! ? followed by space or newline)
SENTENCES=$(echo "$BODY" | grep -oP '[^.!?]+[.!?]' | wc -l)
if [[ $SENTENCES -gt 5 ]]; then
# Get word counts per sentence
LENGTHS=$(echo "$BODY" | grep -oP '[^.!?]+[.!?]' | while read -r sent; do echo "$sent" | wc -w; done)
AVG=$(echo "$LENGTHS" | awk '{sum+=$1} END {printf "%.0f", sum/NR}')
# Count sentences within 5 words of average
UNIFORM=$(echo "$LENGTHS" | awk -v avg="$AVG" '{if ($1 >= avg-5 && $1 <= avg+5) count++} END {printf "%.0f", (count/NR)*100}')
if [[ $UNIFORM -gt 70 ]]; then
echo "WARN Sentence uniformity: ${UNIFORM}% within 5 words of average ($AVG words)"
echo " AI benchmark: >70% uniform. Human writing varies wildly."
echo " Try: Add some 5-word punches. Break up medium sentences."
echo ""
WARNINGS=$((WARNINGS + 1))
else
echo "OK Sentence variation: ${UNIFORM}% uniform (avg $AVG words/sentence)"
echo ""
fi
fi
# --- SUMMARY ---
echo "=== Summary ==="
if [[ $ISSUES -eq 0 && $WARNINGS -eq 0 ]]; then
echo "CLEAN -- No issues found"
exit 0
elif [[ $ISSUES -eq 0 ]]; then
echo "$WARNINGS warning(s), 0 failures"
echo "Review warnings but publishable."
exit 0
else
echo "$ISSUES FAILURE(s), $WARNINGS warning(s)"
echo "Fix failures before publishing."
exit 1
fi