Wire the depth and size gates, exempting structural depth

The depth cap had 104 violations. 57 are structural: whole projects with
their own layout (C/, dwm/, artgate/), a tool split into modules to satisfy
the line cap, prose trees, and directories whose name an external format
dictates (systemd, hooks, workflows, patches, fixtures). Those are exempted
the same way lib/ and tests/ already were.

The other 47 all sit under periodic_background/{hosts,digital_wellbeing},
which can only come down by extracting those units - blocked on a manual
guard repoint. So the hook runs --warn: the number stays visible on every
commit without blocking unrelated work. Enforcement is one flag away.

Also guards the argument order: `--all --warn` used to ignore --warn and
exit 1, which as a hook entry would have blocked every commit.

check_repo_size.sh is the companion gate - depth stops one subtree growing
a taxonomy, this stops the repo absorbing everything. It counts tracked
files (git ls-files, never find: the tree holds ~30k untracked extraction
leftovers) minus vendored and hook-generated trees: 1049 against a 1200
ceiling, printed every run so the threshold is auditable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
This commit is contained in:
Krzysztof Rudnicki 2026-08-23 19:26:16 +02:00
parent 2cf62a2402
commit 0a1e43fc11
4 changed files with 243 additions and 6 deletions

View File

@ -86,6 +86,27 @@ repos:
language: system
pass_filenames: false
always_run: true
- id: directory-depth-cap
name: Report paths over the directory-depth cap
# Repo-wide, and deliberately --warn for now: 47 paths under
# periodic_background/{hosts,digital_wellbeing} are over the cap and
# can only come down by extracting those two units, which is blocked
# on a manual guard repoint. Running it warn-only keeps the number
# visible on every commit without blocking unrelated work. Drop
# --warn once both units are out; that is the last step of the
# extraction, not a separate decision.
entry: bash meta/scripts/check_directory_depth.sh --warn --all
language: system
pass_filenames: false
always_run: true
- id: repo-size-cap
name: Enforce the source-file ceiling
# Counts tracked files only, excluding vendored and hook-generated
# trees. Prints the count on every run so the ceiling is auditable.
entry: bash meta/scripts/check_repo_size.sh
language: system
pass_filenames: false
always_run: true
- id: ci-baseline-green
name: Refuse to build on a red CI baseline
# Checks the last COMPLETED run of each required workflow on main.

View File

@ -0,0 +1,57 @@
{
"intent": "Wire the two repo-structure gates into pre-commit: the directory-depth cap (warn-only for now) and a new source-file ceiling, so structural drift is visible on every commit.",
"scope": [
"meta/scripts/check_directory_depth.sh - structural exemptions plus a --warn mode",
"meta/scripts/check_repo_size.sh - new source-file ceiling gate",
".pre-commit-config.yaml - two new always_run hooks",
"Non-goal: bringing the remaining 47 over-cap paths under the cap. They sit under periodic_background/{hosts,digital_wellbeing}, whose extraction is blocked on a manual sudo guard repoint.",
"Non-goal: syncing meta/.pre-commit-config.yaml, which is a separate standalone config already 219 lines divergent at HEAD."
],
"changes": [
"Exempted the 57 structurally-deep paths: whole projects with their own layout (C/, dwm/, artgate/), one module-split tool (misc/tools/), prose trees (docs/), three one-off leaves, and five externally-dictated directory segments (systemd, hooks, workflows, patches, fixtures).",
"Added --warn to check_directory_depth.sh: reports violations and exits 0. The hook uses it because the 47 remaining paths cannot be fixed without the blocked extraction; enforcement turns on by deleting the flag.",
"Guarded the --all argument order: '--all --warn' previously ignored --warn silently and exited 1, which would have blocked every commit. It now errors with a note that options precede --all.",
"Added check_repo_size.sh, counting tracked files via git ls-files (never find - the tree holds ~30k untracked extraction leftovers), excluding third_party/, docs/superpowers/ and .hippo/. Ceiling 1200 against a current 1049; the count is always printed so the threshold is auditable from a CI log."
],
"verification": [
{
"command": "./meta/scripts/check_directory_depth.sh --all",
"result": "pass",
"evidence": "EXIT=1 with exactly 47 violations, all under periodic_background/{hosts,digital_wellbeing}. Confirms the arithmetic 104 = 57 structural + 42 digital_wellbeing + 5 hosts, and that no unintended path was swept up by the new exemptions."
},
{
"command": "./meta/scripts/check_directory_depth.sh --warn --all",
"result": "pass",
"evidence": "EXIT=0. Also checked: single clean file EXIT=0; single over-cap file EXIT=1; --help EXIT=0; '--all --warn' now errors loudly instead of silently exiting 1."
},
{
"command": "./meta/scripts/check_repo_size.sh; ./meta/scripts/check_repo_size.sh --count",
"result": "pass",
"evidence": "'check_repo_size: 1049 source files (ceiling 1200)' EXIT=0; --count prints 1049 EXIT=0; unknown flag EXIT=1."
},
{
"command": "MAX_SOURCE_FILES=10 variant of check_repo_size.sh",
"result": "pass",
"evidence": "EXIT=1 with 'over the 10 ceiling' plus the extract-a-unit hint. Confirms the failure branch fires rather than being dead code."
},
{
"command": "pre-commit run directory-depth-cap --all-files; pre-commit run repo-size-cap --all-files",
"result": "pass",
"evidence": "Both report Passed through the real pre-commit harness, not just direct invocation."
},
{
"command": "pre-commit run --files meta/scripts/check_repo_size.sh meta/scripts/check_directory_depth.sh .pre-commit-config.yaml",
"result": "pass",
"evidence": "No hook failures; shfmt and shellcheck clean on both scripts."
}
],
"risks": [
"The depth gate is warn-only, so it cannot yet block a NEW over-cap path. This is deliberate and reversible by deleting --warn from the hook entry once hosts and digital_wellbeing are extracted.",
"The prefix exemptions are broad by design (e.g. all of linux_configuration/docs/). A future deep code tree under an exempted prefix would go unpoliced; the exemptions are individually commented so each can be narrowed.",
"The 1200 ceiling leaves 151 files of headroom. Pruning docs/superpowers is tracked separately and would not change this count, since that tree is excluded from it."
],
"rollback": [
"git revert the commit. Both gates are additive: the scripts are standalone and the hooks are new entries, so reverting restores the previous behaviour exactly.",
"After rollback, confirm 'pre-commit run --all-files' still passes and that no hook references the removed check_repo_size.sh."
]
}

View File

@ -12,17 +12,34 @@
# third_party/ vendored - its shape is upstream's business
# docs/superpowers/ hook-generated bookkeeping, not hand-placed
# .github/skills/ layout fixed by the agent harness
# C/ a C project: its own build layout, not taxonomy
# artgate/ a Flask app; routes/ and styles/ are framework shape
# misc/tools/ one tool split across modules to satisfy the line cap
# docs/ prose trees; chapter folders are not code taxonomy
# dwm/ an upstream-shaped suckless tree
# utils/data/ data files beside the script that reads them
# zsh/scratchpad/ a single scratch file
# run_all/ the aggregate runner's own fixtures
#
# Exempt, by path segment: any file under a `lib/` or `tests/` directory.
# Those two names are structural, not taxonomic - a library split out to
# satisfy the 250-line cap, and the test suite that the shell-coverage
# ratchet requires to sit beside it. Capping them would force one gate to
# fight another.
# Exempt, by path segment: `lib/` and `tests/` are structural, not taxonomic
# - a library split out to satisfy the 250-line cap, and the test suite the
# shell-coverage ratchet requires beside it. Capping them would force one
# gate to fight another. The same argument covers the directories whose name
# is dictated by an external format rather than chosen by us: systemd unit
# dirs, pacman/git hook dirs, GitHub workflow dirs, patch sets and test
# fixtures. Each is matched as a whole segment, so `libvirt/` or `binutils/`
# is unaffected.
#
# check_directory_depth.sh --all # every tracked file
# check_directory_depth.sh <file>... # only the named files
# check_directory_depth.sh --all --warn # report but always exit 0
#
# Exits 0 when everything is within the cap, 1 when anything is over.
# With --warn it reports violations and still exits 0; the hook runs in that
# mode because 47 paths under `periodic_background/{hosts,digital_wellbeing}`
# are over the cap and can only be fixed by extracting those two units, which
# is blocked on a manual guard repoint. Drop --warn from the hook once they
# are out; the cap is then enforced for real.
# ============================================================================
set -euo pipefail
@ -35,6 +52,19 @@ readonly EXCLUDED_PREFIXES=(
third_party/
docs/superpowers/
.github/skills/
# Whole projects that carry their own conventional layout.
linux_configuration/C/
linux_configuration/dwm/
python_pkg/artgate/
# A single tool split into modules to satisfy the 250-line cap.
linux_configuration/misc/tools/
# Prose trees: a chapter folder is not code taxonomy.
linux_configuration/docs/
# One-off leaves, listed explicitly rather than exempting the segment
# name everywhere: `data` and `scratchpad` are too generic for that.
linux_configuration/utils/data/
linux_configuration/zsh/scratchpad/
meta/scripts/run_all/
)
# Directory names that never count as taxonomy. Matched as whole path
@ -42,12 +72,21 @@ readonly EXCLUDED_PREFIXES=(
readonly EXEMPT_SEGMENTS=(
lib
tests
# Names dictated by an external format, not chosen as a category.
systemd
hooks
workflows
patches
fixtures
)
WARN_ONLY=0
usage() {
echo "Usage: $SCRIPT_NAME [--all | <file>...]"
echo "Usage: $SCRIPT_NAME [--warn] [--all | <file>...]"
echo "Options:"
echo " --all Check every tracked file"
echo " --warn Report violations but always exit 0"
echo " -h, --help Show this help"
echo
echo "Files must sit at most $MAX_DEPTH directories below the repo root."
@ -115,6 +154,11 @@ main() {
fi
if [[ "$1" == "--all" ]]; then
if [[ $# -gt 1 ]]; then
echo "Error: --all takes no further arguments (got: ${*:2})" >&2
echo "Note: options must precede --all, e.g. --warn --all" >&2
exit 1
fi
mapfile -t targets < <(git ls-files)
else
targets=("$@")
@ -135,6 +179,11 @@ main() {
printf '%s\n' "$violations" | while IFS=$'\t' read -r depth path; do
printf ' %3d %s (+%d)\n' "$depth" "$path" "$((depth - MAX_DEPTH))" >&2
done
if ((WARN_ONLY)); then
echo "check_directory_depth: --warn given, not failing" >&2
return 0
fi
return 1
}
@ -143,6 +192,10 @@ while [[ $# -gt 0 ]]; do
-h | --help)
usage
;;
--warn)
WARN_ONLY=1
shift
;;
*)
break
;;

106
meta/scripts/check_repo_size.sh Executable file
View File

@ -0,0 +1,106 @@
#!/usr/bin/env bash
# ============================================================================
# Enforce a ceiling on the number of source files in the repo.
#
# The companion to the directory-depth cap. Depth stops any one subtree from
# growing its own taxonomy; this stops the repo as a whole from quietly
# becoming the place everything lands. When the count reaches the ceiling the
# answer is to extract a unit into its own repository - not to raise the
# number because the commit is inconvenient.
#
# "Source" means tracked files that a human wrote and maintains here. Three
# categories are therefore excluded from the count:
#
# third_party/ vendored; upstream decides how many files it has
# docs/superpowers/ hook-generated evidence/contract bookkeeping, which
# grows once per commit and would dominate the number
# .hippo/ agent-generated store, same argument
#
# Counting is done with `git ls-files`, never `find`: the working tree also
# holds untracked extraction leftovers running to tens of thousands of files,
# and a `find`-based count would be meaningless.
#
# The count is always printed, so the threshold can be audited from any CI log
# rather than being taken on trust.
#
# check_repo_size.sh # count and enforce
# check_repo_size.sh --count # print the number only, always exit 0
#
# Exits 0 when at or under the ceiling, 1 when over.
# ============================================================================
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
# Set from the 1049 files counted when this gate was written, plus room to
# work. Extract a unit rather than raising it; see the header.
readonly MAX_SOURCE_FILES=1200
readonly EXCLUDED_PREFIXES=(
third_party/
docs/superpowers/
.hippo/
)
COUNT_ONLY=0
usage() {
echo "Usage: $SCRIPT_NAME [--count]"
echo "Options:"
echo " --count Print the source-file count and exit 0"
echo " -h, --help Show this help"
echo
echo "Fails when tracked source files exceed $MAX_SOURCE_FILES."
exit 0
}
# Tracked files minus the generated and vendored trees. Printed one per line.
source_files() {
local -a filter=()
local prefix
for prefix in "${EXCLUDED_PREFIXES[@]}"; do
filter+=(-e "^${prefix}")
done
# grep exits 1 when it filters everything out; that is not an error here.
git ls-files | grep -v "${filter[@]}" || true
}
main() {
local count
count="$(source_files | wc -l)"
if ((COUNT_ONLY)); then
echo "$count"
return 0
fi
if ((count > MAX_SOURCE_FILES)); then
echo "check_repo_size: $count source files, over the $MAX_SOURCE_FILES ceiling" >&2
echo "Extract a unit into its own repo rather than raising the cap." >&2
return 1
fi
echo "check_repo_size: $count source files (ceiling $MAX_SOURCE_FILES)"
return 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
usage
;;
--count)
COUNT_ONLY=1
shift
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
main