[split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook) and split all 44 files exceeding 500 LOC into domain-focused modules: - consent-service (Go): models, handlers, services, database splits - backend-core (Python): security_api, rbac_api, pdf_service, auth splits - admin-core (TypeScript): 5 page.tsx + sidebar extractions - pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits - voice-service (Python): enhanced_task_orchestrator split Result: 0 violations, 36 exempted (pipeline, tests, pure-data files). Go build verified clean. No behavior changes — pure structural splits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
161
scripts/check-loc.sh
Executable file
161
scripts/check-loc.sh
Executable file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# LOC Budget Checker — enforces the 500 LOC hard cap.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/check-loc.sh --changed # only git-changed files (pre-commit)
|
||||
# bash scripts/check-loc.sh --all # all source files in repo
|
||||
# bash scripts/check-loc.sh --staged # only staged files (for pre-commit hook)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — all files within budget
|
||||
# 1 — one or more violations found
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
MAX_LOC="${MAX_LOC:-500}"
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
EXCEPTIONS_FILE="${ROOT_DIR}/.claude/rules/loc-exceptions.txt"
|
||||
|
||||
red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
||||
yellow(){ printf '\033[33m%s\033[0m\n' "$*"; }
|
||||
|
||||
# Check if a file matches any exception pattern in loc-exceptions.txt
|
||||
is_exempt() {
|
||||
local file="$1"
|
||||
[[ -f "$EXCEPTIONS_FILE" ]] || return 1
|
||||
|
||||
while IFS= read -r line; do
|
||||
# Skip empty lines and comments
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
|
||||
# Extract glob pattern (everything before first |)
|
||||
local pattern="${line%%|*}"
|
||||
pattern="$(echo "$pattern" | xargs)" # trim whitespace
|
||||
[[ -z "$pattern" ]] && continue
|
||||
|
||||
# Match against the file path (supports ** globs)
|
||||
# shellcheck disable=SC2254
|
||||
case "$file" in
|
||||
$pattern) return 0 ;;
|
||||
esac
|
||||
done < "$EXCEPTIONS_FILE"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if file is a source file we care about
|
||||
is_source_file() {
|
||||
local file="$1"
|
||||
case "$file" in
|
||||
*.py|*.go|*.ts|*.tsx) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Skip non-source directories
|
||||
should_skip_path() {
|
||||
local file="$1"
|
||||
case "$file" in
|
||||
*/node_modules/*|*/.next/*|*/__pycache__/*|*/venv/*|*/.git/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main() {
|
||||
local mode="changed"
|
||||
case "${1:-}" in
|
||||
--changed) mode="changed" ;;
|
||||
--staged) mode="staged" ;;
|
||||
--all) mode="all" ;;
|
||||
esac
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
# Collect files based on mode
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp)
|
||||
trap "rm -f '$tmpfile'" EXIT
|
||||
|
||||
case "$mode" in
|
||||
staged)
|
||||
git diff --name-only --cached > "$tmpfile"
|
||||
;;
|
||||
changed)
|
||||
{ git diff --name-only --cached 2>/dev/null; git diff --name-only 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null; } | sort -u > "$tmpfile"
|
||||
;;
|
||||
all)
|
||||
# Use find + xargs wc for speed, then filter >500 LOC only
|
||||
find . \( -name '*.py' -o -name '*.go' -o -name '*.ts' -o -name '*.tsx' \) \
|
||||
-not -path '*/node_modules/*' \
|
||||
-not -path '*/.next/*' \
|
||||
-not -path '*/__pycache__/*' \
|
||||
-not -path '*/venv/*' \
|
||||
-not -path '*/.git/*' \
|
||||
-exec wc -l {} + 2>/dev/null \
|
||||
| awk -v max="$MAX_LOC" '$1 > max && !/total$/ { sub(/^[[:space:]]*[0-9]+[[:space:]]*\.\//, ""); print }' > "$tmpfile"
|
||||
# For --all mode, we already have only the violating files
|
||||
# Override the check loop to just check exemptions
|
||||
local fast_violations=0
|
||||
local fast_checked=0
|
||||
local fast_exempted=0
|
||||
while IFS= read -r file; do
|
||||
[[ -z "$file" ]] && continue
|
||||
[[ -f "$file" ]] || continue
|
||||
if is_exempt "$file"; then
|
||||
fast_exempted=$((fast_exempted + 1))
|
||||
continue
|
||||
fi
|
||||
fast_checked=$((fast_checked + 1))
|
||||
local loc
|
||||
loc=$(wc -l < "$file" 2>/dev/null || echo 0)
|
||||
red "VIOLATION: $file ($loc LOC > $MAX_LOC)"
|
||||
fast_violations=$((fast_violations + 1))
|
||||
done < "$tmpfile"
|
||||
echo ""
|
||||
if (( fast_violations > 0 )); then
|
||||
red "$fast_violations file(s) exceed ${MAX_LOC} LOC budget. (violations: $fast_violations, exempted: $fast_exempted)"
|
||||
exit 1
|
||||
fi
|
||||
green "LOC budget check passed. (exempted: $fast_exempted)"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
local violations=0
|
||||
local checked=0
|
||||
local exempted=0
|
||||
|
||||
while IFS= read -r file; do
|
||||
[[ -z "$file" ]] && continue
|
||||
[[ -f "$file" ]] || continue
|
||||
is_source_file "$file" || continue
|
||||
should_skip_path "$file" && continue
|
||||
|
||||
if is_exempt "$file"; then
|
||||
exempted=$((exempted + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
checked=$((checked + 1))
|
||||
local loc
|
||||
loc=$(wc -l < "$file" 2>/dev/null || echo 0)
|
||||
|
||||
if (( loc > MAX_LOC )); then
|
||||
red "VIOLATION: $file ($loc LOC > $MAX_LOC)"
|
||||
violations=$((violations + 1))
|
||||
fi
|
||||
done < "$tmpfile"
|
||||
|
||||
echo ""
|
||||
if (( violations > 0 )); then
|
||||
red "$violations file(s) exceed ${MAX_LOC} LOC budget. (checked: $checked, exempted: $exempted)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
green "LOC budget check passed. (checked: $checked, exempted: $exempted)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user