#!/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 " 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 '(?/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: 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