diff --git a/.dirsize-baseline b/.dirsize-baseline new file mode 100644 index 0000000..4c0e15c --- /dev/null +++ b/.dirsize-baseline @@ -0,0 +1,8 @@ +# Directory-size baseline (docs/patterns/07) - flat SOURCE-file counts > 15 +# that are tolerated until split (ratchet: may only shrink, never grow; a NEW +# oversized directory fails `check-directory-size.sh --gate`). This file IS the +# god-folder whitelist. components/ is the EXAMPLE-DOMAIN demo surface and +# shrinks naturally as you replace the example domain with your own. +# Format: # reason +frontend/src/components # EXAMPLE-DOMAIN demo components (shrinks on customization) +frontend/src/hooks # shared hooks diff --git a/Makefile b/Makefile index db66827..69ab78b 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ audit-backend audit-frontend bandit-backend security-backend check-security \ release-prepare release-finish release-publish \ check-file-sizes check-complexity check-complexity-gate check-complexity-gate-update \ + check-directory-size check-directory-size-gate \ clean prod prod-down prod-logs help # --- Development --- @@ -276,6 +277,12 @@ check-complexity-gate: ## Complexity ratchet gate: fail on new/regressed offende check-complexity-gate-update: ## Regenerate .complexity-baseline from current offenders (ratchet may only shrink) @bash scripts/check-complexity.sh --update-baseline +check-directory-size: ## God-folder watcher (warn-only): >15 flat source files per dir under frontend/src + @bash scripts/check-directory-size.sh + +check-directory-size-gate: ## God-folder ratchet gate: fail on a NEW oversized dir vs .dirsize-baseline + @bash scripts/check-directory-size.sh --gate + # --- E2E Tests --- test-e2e: ## Run Playwright e2e tests (starts servers automatically) diff --git a/docs/patterns/07-complexity-cohesion-ratchet.md b/docs/patterns/07-complexity-cohesion-ratchet.md index 16d60ac..02e010f 100644 --- a/docs/patterns/07-complexity-cohesion-ratchet.md +++ b/docs/patterns/07-complexity-cohesion-ratchet.md @@ -8,10 +8,12 @@ `radon_warn.py` / `complexity_gate.py`), with `make check-file-sizes` / `check-complexity` / `check-complexity-gate` / `check-complexity-gate-update` and shrink-only `.filesize-baseline` + `.complexity-baseline` (plus a -`.filesize-whitelist`). radon is a backend dev-dependency. The **TypeScript -(eslint) half of the complexity gate degrades gracefully** and is inactive -until the template has an ESLint flat config (`eslint.config.js`); the -**god-folder watcher** is still documented-only - add it as below. +`.filesize-whitelist`). radon is a backend dev-dependency. The **god-folder +watcher** also ships - `scripts/check-directory-size.sh` + `make +check-directory-size` / `check-directory-size-gate` with a shrink-only +`.dirsize-baseline`. The only remaining gap is the **TypeScript (eslint) half +of the complexity gate**, which degrades gracefully and stays inactive until +the template has an ESLint flat config (`eslint.config.js`). --- diff --git a/scripts/check-directory-size.sh b/scripts/check-directory-size.sh new file mode 100644 index 0000000..a2734ce --- /dev/null +++ b/scripts/check-directory-size.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# check-directory-size.sh - god-folder watcher (docs/patterns/07). +# +# Counts the FLAT (maxdepth 1) source files (*.ts / *.tsx, tests excluded) in +# each directory under frontend/src and warns when a directory holds too many. +# Like the file-size watcher, this prevents god-folders from returning after +# they were grouped by concern. +# +# Two modes: +# (default) WARN_THRESHOLD (default 15) - warn, no fail. +# --gate exit 1 when a directory NOT listed in .dirsize-baseline is over +# the threshold (ratchet: existing, not-yet-split god-folders are +# tolerated, but a NEW one fails). +# +# Only version-controlled sources under frontend/src (git ls-files), so +# generated / gitignored trees drop out automatically. +# +# Exit codes: +# 0 = clean, warnings only, or only tolerated baseline directories +# 1 = (only with --gate) a new directory over the threshold + +set -euo pipefail + +WARN_THRESHOLD="${WARN_THRESHOLD:-15}" +ROOT_DIR="${ROOT_DIR:-frontend/src}" +BASELINE_FILE=".dirsize-baseline" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_ROOT" + +GATE=0 +[[ "${1:-}" == "--gate" ]] && GATE=1 + +# Tolerated (not-yet-split) directories, one path per line, '#' comments ok. +declare -A BASELINE=() +if [[ -f "$BASELINE_FILE" ]]; then + while IFS= read -r line; do + line="${line%%#*}"; line="$(echo -n "$line" | xargs || true)" + [[ -n "$line" ]] && BASELINE["$line"]=1 + done < "$BASELINE_FILE" +fi + +# Count flat (maxdepth 1) tracked *.ts/*.tsx per directory. Co-located test +# files (*.test.ts/x) are excluded: the guard is about grouping SOURCE files +# by concern, and tests live next to their subject by convention. +declare -A COUNT=() +while IFS= read -r f; do + [[ "$f" == */* ]] || continue + case "$f" in *.test.ts|*.test.tsx) continue;; esac + dir="${f%/*}" + COUNT["$dir"]=$(( ${COUNT["$dir"]:-0} + 1 )) +done < <(git ls-files "$ROOT_DIR" | grep -E '\.tsx?$' || true) + +status=0 +warned=0 +for dir in $(printf '%s\n' "${!COUNT[@]}" | sort); do + n="${COUNT[$dir]}" + (( n > WARN_THRESHOLD )) || continue + if [[ -n "${BASELINE[$dir]:-}" ]]; then + echo "BASELINE (tolerated): $dir has $n files (max $WARN_THRESHOLD)" + continue + fi + echo "WARNING: $dir has $n files (max $WARN_THRESHOLD) - group by concern" + warned=1 + (( GATE == 1 )) && status=1 +done + +if (( warned == 0 )); then + echo "check-directory-size: OK - no un-baselined directory over $WARN_THRESHOLD flat files." +fi +exit $status