#!/usr/bin/env bash # check-rules.sh — project rule audit engine. # # Usage: # bash scripts/check-rules.sh # full audit (verbose, includes slow checks) # bash scripts/check-rules.sh --fast # fast audit (quiet, skips slow checks) — for pre-commit # bash scripts/check-rules.sh --quiet # full audit, only prints failures # # Exit code: 0 = all rules pass (warnings are non-fatal), 1 = one or more FAILED. # # This is a generalized version of the rules engine proven in the # RCEO-PersonalAssistant project. Add project-specific checks by appending # `check "" ""` calls below. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" # shellcheck disable=SC1091 source "$HERE/lib/common.sh" REPO_ROOT="$(repo_root)" cd "$REPO_ROOT" # --- argument parsing --- RULE_FAST=false RULE_VERBOSE=true for arg in "$@"; do case "$arg" in --fast) RULE_FAST=true; RULE_VERBOSE=false ;; --quiet) RULE_VERBOSE=false ;; *) die "check-rules.sh: unknown argument '$arg'" ;; esac done export RULE_FAST RULE_VERBOSE init_counters $RULE_VERBOSE && echo "=== Project Rule Audit ===" TODAY="$(date +%Y-%m-%d)" # ---------------------------------------------------------------------------- # 1. Shellcheck — every .sh/.bash must pass (zero warnings, incl. info-level). # Runs in Docker so the host stays clean (no native shellcheck required). # ---------------------------------------------------------------------------- $RULE_VERBOSE && log_step "Shell scripts (shellcheck)" mapfile -d '' SH_FILES < <(find . -path ./.git -prune -o -path ./.tmp -prune -o -path ./vendor -prune -o -path ./node_modules -prune -o \( -name '*.sh' -o -name '*.bash' \) -print0 2>/dev/null) if [ "${#SH_FILES[@]}" -gt 0 ]; then if have shellcheck; then if shellcheck "${SH_FILES[@]}" >/dev/null 2>&1; then check "All shell scripts pass shellcheck (host)" "pass" else check "shellcheck reports violations — run: shellcheck " "fail" fi elif have docker; then MNT_FILES=() for f in "${SH_FILES[@]}"; do MNT_FILES+=("/mnt/${f#./}"); done if docker run --rm -v "$REPO_ROOT:/mnt" koalaman/shellcheck:stable "${MNT_FILES[@]}" >/dev/null 2>&1; then check "All shell scripts pass shellcheck (docker)" "pass" else check "shellcheck (docker) reports violations" "fail" fi else check "No shellcheck or docker available to lint scripts" "warn" fi else check "No shell scripts to lint" "pass" fi # ---------------------------------------------------------------------------- # 2. Docker image pinning — no ':latest' tags in compose or Dockerfiles. # ---------------------------------------------------------------------------- $RULE_VERBOSE && log_step "Docker image pinning" if grep -rqE '(image:|FROM).*:latest' --include='docker-compose*.y*ml' --include='Dockerfile*' . 2>/dev/null; then check "No ':latest' image tags (pin everything)" "fail" else check "No ':latest' image tags" "pass" fi # ---------------------------------------------------------------------------- # 2b. Container naming — every service in a docker-compose file MUST set an # explicit container_name (never rely on Docker's default _). # ---------------------------------------------------------------------------- $RULE_VERBOSE && log_step "Container naming" COMPOSE_FILES="$(find . -path ./.git -prune -o \( -name 'docker-compose*.yml' -o -name 'docker-compose*.yaml' -o -name 'compose.yml' -o -name 'compose.yaml' \) -print 2>/dev/null || true)" if [ -n "$COMPOSE_FILES" ]; then BAD=0 while IFS= read -r cf; do [ -n "$cf" ] || continue # Count top-level service keys (2-space indent under services:) and # compare against the number of container_name: declarations. svc_count=$(awk '/^services:/{f=1;next} f&&/^[^[:space:]]/{f=0} f&&/^[[:space:]]{2}[[:alnum:]_-]+:[[:space:]]*$/{c++} END{print c+0}' "$cf") cn_count=$(grep -cE '^[[:space:]]*container_name:' "$cf" 2>/dev/null || echo 0) if [ "${svc_count:-0}" -gt 0 ] && [ "$cn_count" -lt "$svc_count" ]; then BAD=$((BAD + 1)) fi done </dev/null 2>&1; then if git diff --quiet && git diff --cached --quiet; then check "Working tree clean" "pass" else check "Uncommitted changes present" "warn" fi else check "Not a git repo (git checks skipped)" "pass" fi # ---------------------------------------------------------------------------- # 6. Hooks installed — self-check that git hooks were set up. # ---------------------------------------------------------------------------- $RULE_VERBOSE && log_step "Git hooks" if [ -f .git/hooks/pre-commit ]; then check "pre-commit hook installed" "pass" else check "pre-commit NOT installed (run: bash scripts/setup-hooks.sh)" "warn" fi if [ -f .git/hooks/pre-push ]; then check "pre-push hook installed" "pass" else check "pre-push NOT installed (run: bash scripts/setup-hooks.sh)" "warn" fi # ---------------------------------------------------------------------------- # 7. WORKING.md completion — no unchecked tasks may remain at commit time. # ---------------------------------------------------------------------------- $RULE_VERBOSE && log_step "Task completion" if [ -f WORKING.md ]; then UNCHECKED="$(grep -cF -- '- [ ]' WORKING.md || true)" if [ "$UNCHECKED" -eq 0 ]; then check "WORKING.md has no unchecked tasks" "pass" else check "WORKING.md has ${UNCHECKED} unchecked task(s) — finish them before committing" "fail" fi else check "WORKING.md absent (no active task tracker)" "pass" fi # ---------------------------------------------------------------------------- # 8. CNW markers — empty `CNW:` markers flag unresolved questions for the human. # ---------------------------------------------------------------------------- $RULE_VERBOSE && log_step "Unresolved questions" EMPTY_CNW="$(grep -rn 'CNW:$' . --include='*.md' 2>/dev/null | head -20 || true)" if [ -z "$EMPTY_CNW" ]; then check "No empty CNW: markers (unresolved questions)" "pass" else CNW_COUNT="$(printf '%s\n' "$EMPTY_CNW" | grep -c . || true)" check "${CNW_COUNT} unresolved CNW: marker(s) — needs user input" "warn" fi # ---------------------------------------------------------------------------- # 9. Hygiene — merge-conflict markers and trailing whitespace must never land. # ---------------------------------------------------------------------------- $RULE_VERBOSE && log_step "File hygiene" if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then CONFLICT="$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | xargs -r grep -lE '^(<<<<<<<|=======|>>>>>>>)' 2>/dev/null || true)" if [ -z "$CONFLICT" ]; then check "No merge-conflict markers staged" "pass"; else check "Merge-conflict markers staged: $CONFLICT" "fail"; fi fi # ---------------------------------------------------------------------------- # 10. (slow, skipped in --fast) Project test suite via `make test` if present. # ---------------------------------------------------------------------------- if [ "$RULE_FAST" = false ] && [ -f Makefile ] && grep -qE '^test:' Makefile; then $RULE_VERBOSE && log_step "Test suite (make test)" if make test >/dev/null 2>&1; then check "make test passes" "pass" else check "make test FAILS" "fail" fi fi print_summary_and_exit