From c4bb3e93f677d577e4c3160866ff98ff5ac96c50 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 17 Aug 2026 14:52:24 +0200 Subject: [PATCH] meta: add the check_file_length.sh gate the splits assumed existed The shell-splitting work has been referring to check_file_length.sh --all as its per-commit gate. No such script was ever committed: it is absent from the working tree and from every commit in history, so each split so far was checked by eye. Cap is 250 lines, source extensions only (sh/bash/zsh, py, kt/kts, dart, ts/tsx/js/jsx). Markdown, HTML, JSON, YAML and lockfiles are out of scope, as are third_party/ and docs/superpowers/. --all reports exactly 24 files over the cap, independently reproducing the count the brief carried. That agreement is what confirms the extension scope is the intended one. Deliberately NOT wired into .pre-commit-config.yaml: 24 files are still over, so a hook would block every commit. It becomes a hook once --all exits 0. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY --- .../check-file-length-gate-2026-08-17.json | 53 +++++++ meta/scripts/check_file_length.sh | 135 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 docs/superpowers/evidence/check-file-length-gate-2026-08-17.json create mode 100755 meta/scripts/check_file_length.sh diff --git a/docs/superpowers/evidence/check-file-length-gate-2026-08-17.json b/docs/superpowers/evidence/check-file-length-gate-2026-08-17.json new file mode 100644 index 00000000..7f4c32af --- /dev/null +++ b/docs/superpowers/evidence/check-file-length-gate-2026-08-17.json @@ -0,0 +1,53 @@ +{ + "intent": "Provide the per-commit file-length gate that the shell-splitting work has been referring to. It did not exist: no check_file_length.sh was ever committed to this repo, so every split so far was checked by eye.", + "scope": [ + "meta/scripts/check_file_length.sh — new script", + "Non-goal: wiring it into .pre-commit-config.yaml. 24 files are still over the cap, so a hook would block every commit. It lands as a hook once --all exits 0." + ], + "changes": [ + "New meta/scripts/check_file_length.sh enforcing a 250-line cap, taking either --all (every tracked file) or an explicit file list.", + "Scope is source extensions only (sh/bash/zsh, py, kt/kts, dart, ts/tsx/js/jsx). Markdown, HTML, JSON, YAML, lockfiles and .gitignore are excluded: a length cap on data and prose is noise.", + "third_party/ and docs/superpowers/ are excluded as vendored or generated trees.", + "Reports each violation with its line count and the overage, sorted worst-first, on stderr; exits 1 when anything is over." + ], + "verification": [ + { + "command": "bash meta/scripts/check_file_length.sh --all", + "result": "fail-as-expected", + "evidence": "Reports exactly 24 files over the cap and exits 1. This independently reproduces the count carried in NEXT_SESSION_PROMPT.md (20 shell, 2 kotlin, 1 dart, plus the markdown brief which this scope correctly excludes), which is what confirms the extension scope matches the intended one." + }, + { + "command": "bash meta/scripts/check_file_length.sh meta/scripts/check_file_length.sh", + "result": "pass", + "evidence": "'all checked files are within 250 lines', exit 0 — the gate passes its own cap at 135 lines." + }, + { + "command": "bash meta/scripts/check_file_length.sh phone_focus_mode/dns_enforcer.sh", + "result": "fail-as-expected", + "evidence": "'325 phone_focus_mode/dns_enforcer.sh (+75)', exit 1 — named-file mode reports the overage." + }, + { + "command": "bash meta/scripts/check_file_length.sh NEXT_SESSION_PROMPT.md third_party/agent-skills/hooks/simplify-ignore.sh", + "result": "pass", + "evidence": "Both exit 0: the markdown is out of extension scope and the 302-line vendored script is under an excluded prefix." + }, + { + "command": "bash meta/scripts/check_file_length.sh", + "result": "fail-as-expected", + "evidence": "'Error: pass --all or one or more file paths', exit 1 — no-argument invocation cannot be mistaken for a clean run." + }, + { + "command": "shellcheck meta/scripts/check_file_length.sh", + "result": "pass", + "evidence": "Clean. An initial SC2155 on the readonly SCRIPT_NAME assignment was fixed by splitting declaration from assignment, not suppressed." + } + ], + "risks": [ + "The cap is a blunt measure: a 260-line file with one long data table is treated the same as a 260-line file with tangled control flow. The gate is an alarm, not a judgement.", + "Excluding docs/superpowers/ means generated evidence and contract artifacts are never capped. That is intended, but a genuinely oversized script placed there would go unchecked." + ], + "rollback": [ + "git revert the commit and delete the script; nothing depends on it yet because it is deliberately not wired into pre-commit.", + "After rollback, confirm pre-commit run --all-files still passes, which it will, since no hook references the script." + ] +} diff --git a/meta/scripts/check_file_length.sh b/meta/scripts/check_file_length.sh new file mode 100755 index 00000000..558c3d74 --- /dev/null +++ b/meta/scripts/check_file_length.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash + +# ============================================================================ +# Enforce the repo-wide source-file length cap. +# +# Every source file must be at most MAX_LINES lines. The cap exists so that a +# file stays readable in one sitting and so that a split is forced while the +# seams are still obvious, rather than after a file has grown past rescue. +# +# Checks tracked source files only. Data, docs, config and vendored trees are +# out of scope: a 900-line lockfile or an HTML report is not something a +# human is expected to read top to bottom, so a cap there is noise. +# +# check_file_length.sh --all # every tracked source file +# check_file_length.sh ... # only the named files +# +# Exits 0 when everything is within the cap, 1 when anything is over. +# ============================================================================ + +set -euo pipefail + +SCRIPT_NAME="$(basename "$0")" +readonly SCRIPT_NAME +readonly MAX_LINES=250 + +# Extensions that carry hand-written logic. Kept explicit rather than +# inverted: a new data format should not silently become subject to the cap. +readonly SOURCE_EXTENSIONS=( + sh bash zsh + py + kt kts + dart + ts tsx js jsx +) + +# Vendored or generated trees. Their length is upstream's business, and a +# split would be reverted by the next sync. +readonly EXCLUDED_PREFIXES=( + third_party/ + docs/superpowers/ +) + +usage() { + echo "Usage: $SCRIPT_NAME [--all | ...]" + echo "Options:" + echo " --all Check every tracked source file" + echo " -h, --help Show this help" + echo + echo "Source files must be at most $MAX_LINES lines." + exit 0 +} + +# True when the path carries a source extension we cap. +is_source_file() { + local path="$1" ext="${1##*.}" + # A path with no dot in its final component has no extension to match. + [[ "$path" == *.* ]] || return 1 + local candidate + for candidate in "${SOURCE_EXTENSIONS[@]}"; do + [[ "$ext" == "$candidate" ]] && return 0 + done + return 1 +} + +# True when the path sits under a tree we deliberately do not police. +is_excluded() { + local path="$1" prefix + for prefix in "${EXCLUDED_PREFIXES[@]}"; do + [[ "$path" == "$prefix"* ]] && return 0 + done + return 1 +} + +# Print "\t" for each in-scope file that exceeds the cap. +report_violations() { + local path lines + for path in "$@"; do + [[ -f "$path" ]] || continue + is_source_file "$path" || continue + is_excluded "$path" && continue + lines=$(wc -l <"$path") + if ((lines > MAX_LINES)); then + printf '%s\t%s\n' "$lines" "$path" + fi + done +} + +collect_all_tracked() { + git ls-files +} + +main() { + local -a targets=() + + if [[ $# -eq 0 ]]; then + echo "Error: pass --all or one or more file paths" >&2 + exit 1 + fi + + if [[ "$1" == "--all" ]]; then + mapfile -t targets < <(collect_all_tracked) + else + targets=("$@") + fi + + local violations + violations="$(report_violations "${targets[@]}" | sort -rn)" + + if [[ -z "$violations" ]]; then + echo "check_file_length: all checked files are within $MAX_LINES lines" + return 0 + fi + + local count + count="$(printf '%s\n' "$violations" | wc -l)" + + echo "check_file_length: $count file(s) over the $MAX_LINES-line cap:" >&2 + printf '%s\n' "$violations" | while IFS=$'\t' read -r lines path; do + printf ' %5d %s (+%d)\n' "$lines" "$path" "$((lines - MAX_LINES))" >&2 + done + return 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -h | --help) + usage + ;; + *) + break + ;; + esac +done + +main "$@"