testsAndMisc/.pre-commit-config.yaml
Krzysztof kuhy Rudnicki 6b828d29aa Extract digital-wellbeing; the directory-depth cap now enforces
The last unit over the cap is now github.com/kuhyx/digital-wellbeing, cloned
at ~/digital-wellbeing, and `--warn` is gone from the directory-depth-cap
hook. `check_directory_depth.sh --all` exits 0 with zero violations, down from
46 at the start of the session. That was the point of the campaign.

History was filtered across all three paths the unit has lived at (63
commits); the current path alone holds 2.

The six suites that test this code lived in linux_configuration/tests/ rather
than beside it, so they were filtered separately (24 commits) and merged in as
a second root. A path filter of the unit alone would have left them here,
testing code that had left the repo -- and their names would still have been
in shell-tests.yml's hardcoded arrays, failing CI on missing files.

linux_configuration/lib/common.sh is vendored into vendor/ rather than trimmed
to the four functions actually used: set_actual_user_vars calls two of the
others, so a trim breaks silently. It is in vendor/ and not lib/ because
test_music_parallelism.sh copies the unit's own lib/ into a scratch worktree
while stubbing the shared lib; in one directory the real common.sh overwrote
the stub, and the suite silently measured the wrong library.

Running the callers caught what shellcheck and grep both missed:
check_and_enable_services.sh died with "DW_REPO: unbound variable" because the
definition sat below its first use. It now runs clean and reports ok for every
extracted-unit check.

Two long-standing live defects fixed as a consequence. The pacman-wrapper
drift manifest embeds ABSOLUTE paths and had named deleted scripts/ paths
since the flatten, so the check reported a permanent false "stale or
tampered"; it now verifies, with unchanged hashes proving the extracted bytes
are identical. And the last two stale live references are gone: 8 -> 0.

The two pre-commit excludes for setup_midnight_shutdown.sh are dropped rather
than moved. lsattr shows nothing in the unit is chattr +i any more, and the
fixer hooks leave the file alone when allowed to run.

Removing 80 shell files also shrank the jscpd denominator enough that the
virtualenv/tqdm clones inside the GENERATED .ci-mirror-venv/ tipped the repo
to 2.09% and blocked this commit, which adds no duplication. That directory is
the same vendored third-party shell the ignore list already excludes as
.venv/, just under another name; jscpd walks the working tree rather than the
index, so being untracked did not spare it. Excluded by name: 1.74%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:57:51 +02:00

585 lines
25 KiB
YAML

# ==============================================================================
# Pre-commit Configuration - Multi-language Linting & Formatting
# ==============================================================================
# Install: pre-commit install && pre-commit install --hook-type pre-push
# Fast lint: pre-commit run --all-files (linters only, ~10 s)
# Full suite: pre-commit run --all-files --hook-stage pre-push (+ tests)
# Update hooks: pre-commit autoupdate
# ==============================================================================
# Global settings
default_language_version:
python: python3
# By default every hook runs ONLY at commit-time. Pre-push runs only the
# hooks that explicitly opt in via `stages: [pre-push]` (currently
# pytest-coverage + prettier). This keeps `git push` cheap; commit-time
# already enforces the full lint suite.
default_stages: [pre-commit]
# Fail fast on first error (set to false to see all errors)
fail_fast: false
# Configuration
ci:
autofix_commit_msg: "style: auto-fix by pre-commit hooks"
autoupdate_commit_msg: "chore: update pre-commit hooks"
repos:
# ===========================================================================
# GENERAL HOOKS - File formatting and validation
# ===========================================================================
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
- id: check-yaml
args: [--unsafe]
- id: check-json
# Exclude JSONC files (VS Code configs, TypeScript configs)
exclude: ^(\.vscode/|.*/\.vscode/|.*tsconfig.*\.json)
- id: check-toml
- id: check-xml
- id: check-added-large-files
args: [--maxkb=2000]
- id: check-merge-conflict
- id: check-case-conflict
- id: check-symlinks
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: detect-private-key
- id: debug-statements
- id: name-tests-test
args: [--pytest-test-first]
- id: check-ast
- id: check-builtin-literals
- id: check-docstring-first
- id: fix-byte-order-marker
- id: mixed-line-ending
args: [--fix=lf]
- id: requirements-txt-fixer
# ===========================================================================
# BINARY BLOCKER - Prevent binary/image files from being committed
# ===========================================================================
- repo: local
hooks:
- id: no-binaries
name: Block binary/image files
entry: meta/scripts/check_no_binaries.sh
language: script
always_run: false
- id: file-length-cap
name: Enforce the 250-line file cap
# Repo-wide (--all), not per-file: a file that grows past the cap
# without being staged in this commit must still be caught. Wired only
# after the campaign that brought every file under it — turning this on
# earlier would have blocked every unrelated commit in the repo.
entry: bash meta/scripts/check_file_length.sh --all
language: system
pass_filenames: false
always_run: true
- id: directory-depth-cap
name: Enforce the directory-depth cap
# Repo-wide and ENFORCING. It ran --warn while the 46 paths under
# periodic_background/{hosts,digital_wellbeing} were over the cap;
# both units are now their own repos (kuhyx/hosts-blocker,
# kuhyx/digital-wellbeing) and the count is 0, so the cap is real.
# Re-adding --warn would silently re-open the hole it was there to
# close: put the offending path somewhere legal instead.
entry: bash meta/scripts/check_directory_depth.sh --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.
# Fails closed on a definitively red baseline; warns and passes on
# anything it cannot determine (offline, no gh, no runs yet), because
# the real per-change guarantee is the pre-push ci-mirror hook and
# blocking every commit on network reachability is not worth it.
# Bypass a deliberate fix-forward commit with CI_GREEN_SKIP=1.
entry: bash meta/scripts/check_ci_green.sh
language: system
pass_filenames: false
always_run: true
- id: ai-evidence-contract
name: Require AI evidence artifacts for code changes
entry: meta/scripts/check_ai_evidence.sh
language: script
pass_filenames: false
always_run: true
- id: ai-multifile-contract
name: Require workflow contract for multi-file code changes
entry: meta/scripts/check_agent_contract.sh
language: script
pass_filenames: false
always_run: true
- id: append-only-sessions
name: Enforce append-only session logs
entry: meta/scripts/check_append_only_sessions.sh
language: script
pass_filenames: false
always_run: true
# ===========================================================================
# POLLING SCRIPT LINTER - Detect fork-storm anti-patterns in shell scripts
# ===========================================================================
- repo: local
hooks:
- id: no-inline-python
name: Block Python logic embedded in shell scripts
entry: meta/scripts/check_no_inline_python.sh
language: script
types_or: [shell, zsh]
- id: no-polling-antipatterns
name: Block polling script anti-patterns
entry: meta/scripts/check_polling_antipatterns.sh
language: script
types: [shell]
require_serial: true
exclude: ^(\.git/|tests/)
- id: shell-coverage-ratchet
name: Every shell library has a test suite beside it
# The ratchet from meta/shell-coverage-allowlist.txt: a lib under a
# lib/ directory needs a tests/run_all.sh, unless it is allowlisted.
# Wired here on 2026-08-23. It existed but was never in pre-commit,
# so it had been silently unenforced -- a new untested lib could land
# freely. That was only affordable to fix once 578c4a6c dropped the
# jail-measured 100% bar: --all went from >600s to 0.31s, and the
# per-file mode this hook uses is milliseconds.
entry: bash meta/scripts/check_shell_coverage.sh
language: system
types: [shell]
exclude: ^third_party/
# ===========================================================================
# NOQA BLOCKER - Zero tolerance for noqa/type:ignore suppression comments
# ===========================================================================
- repo: local
hooks:
- id: no-noqa
name: Block noqa comments
entry: '(?i)#\s*(noqa|type:\s*ignore)'
language: pygrep
types: [python]
- id: no-ruff-noqa
name: Block ruff noqa file-level comments
entry: '(?i)#\s*ruff:\s*noqa'
language: pygrep
types: [python]
# ===========================================================================
# RUFF - Fast Python linter and formatter (replaces black, isort, flake8, etc.)
# ===========================================================================
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.2
hooks:
# Linter - run first to catch issues
- id: ruff
args:
- --fix
- --unsafe-fixes
- --exit-non-zero-on-fix
- --show-fixes
types_or: [python, pyi]
# Formatter - run after linting
- id: ruff-format
types_or: [python, pyi]
# ===========================================================================
# MYPY - Static type checking (per-commit on changed files only)
# Was on pre-push, but force-push diffs caused full-repo scans + OOM. On
# pre-commit it sees only the file(s) currently staged → near-instant.
# ===========================================================================
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
hooks:
- id: mypy
stages: [pre-commit]
args:
- --ignore-missing-imports
- --no-error-summary
- --disable-error-code=no-untyped-def
- --disable-error-code=no-untyped-call
- --disable-error-code=var-annotated
- --disable-error-code=no-any-unimported
- --disable-error-code=type-arg
- --disable-error-code=no-any-return
- --disable-error-code=misc
- --disable-error-code=unused-ignore
- --disable-error-code=unreachable
- --disable-error-code=assignment
- --disable-error-code=no-redef
- --disable-error-code=attr-defined
- --disable-error-code=arg-type
- --disable-error-code=union-attr
- --disable-error-code=call-overload
- --disable-error-code=return-value
- --disable-error-code=redundant-cast
- --disable-error-code=empty-body
- --disable-error-code=list-item
exclude: >-
(?x)^(
Bash/.*|
\.venv/.*|
linux_configuration/misc/tools/.*
)$
additional_dependencies:
- types-requests
- types-PyYAML
- types-python-dateutil
# ===========================================================================
# PYLINT - Comprehensive Python linter (per-commit on changed files only)
# Was on pre-push, but force-push diffs caused full-repo scans + OOM.
# ===========================================================================
- repo: https://github.com/pylint-dev/pylint
rev: v3.3.2
hooks:
- id: pylint
stages: [pre-commit]
args:
- --rcfile=pyproject.toml
- --fail-under=10
- --jobs=4
additional_dependencies:
- pytest
- python-chess
- requests
- pygame
- pillow
# archwiki_rag parses the local arch-wiki-docs HTML dump. pylint runs
# in its own venv, so a package being in meta/requirements.txt is not
# enough for it to resolve the import.
- beautifulsoup4
- markdownify
# android_ui parses `uiautomator dump` output. Same reason as
# beautifulsoup4 above: requirements.txt is not on pylint's path.
- defusedxml
# artgate computes image gates over pixel arrays. Same reason again:
# it is the first package here to import numpy.
- numpy
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.0#subdirectory=gatelock
- anthropic
- rich
- typer
# Test suites and conftest fixtures are linted separately; they use
# patterns (protected-access, missing docstrings, fixture shadowing)
# that don't belong in the source-code 10/10 gate.
exclude: ^(Bash/|\.venv/)|(^|/)(tests/|conftest\.py)
# ===========================================================================
# BANDIT - Security linter (per-commit on changed files only)
# Was on pre-push, but force-push diffs caused full-repo scans + OOM.
# ===========================================================================
- repo: https://github.com/PyCQA/bandit
rev: 1.7.10
hooks:
- id: bandit
stages: [pre-commit]
args:
- -c
- pyproject.toml
- --severity-level=high
- --confidence-level=medium
- --skip=B113
additional_dependencies: ["bandit[toml]"]
exclude: ^(Bash/|\.venv/|tests/|.*test.*\.py$)
# ===========================================================================
# PYTEST + COVERAGE - Run tests and enforce 100% code coverage
# Only tests for subpackages with changed files are run (see script).
# Uses pytest-xdist (-n auto) to parallelize across all CPUs.
# ===========================================================================
- repo: local
hooks:
- id: pytest-coverage
name: pytest with coverage enforcement
# Serialised against builds and pacman transactions on this 7.6 GB
# machine via the heavy-job lock. run_with_heavy_lock.sh is a plain
# passthrough on any clone that has not installed one.
entry: bash meta/scripts/run_with_heavy_lock.sh pytest python meta/scripts/pytest_changed_packages.py
language: system
types: [python]
pass_filenames: true
require_serial: true
stages: [pre-commit]
- id: requirements-sync
name: requirements.txt covers all python_pkg imports
entry: python3 meta/scripts/check_requirements_sync.py --package python_pkg
language: system
pass_filenames: false
files: ^(python_pkg/.*\.py|requirements\.txt)$
- id: requirements-test-sync
name: requirements-test.txt covers all python_pkg imports
# python-tests.yml installs this SUBSET rather than the full list, so
# it needs the same completeness guarantee. Without this hook a new
# third-party import would pass locally and fail in CI with a bare
# ModuleNotFoundError.
entry: python3 meta/scripts/check_requirements_sync.py --package python_pkg --requirements meta/requirements-test.txt
language: system
pass_filenames: false
files: ^(python_pkg/.*\.py|meta/requirements-test\.txt)$
- id: session-autopsy-hook-mutations
name: session_autopsy hook tests are non-vacuous (mutation)
entry: python3 meta/scripts/mutate_session_autopsy_hooks.py
language: system
pass_filenames: false
require_serial: true
# Scoped to the two files that can invalidate the proof: the suite
# itself and the mutation definitions. Takes ~40s, so it must not run
# on unrelated commits. Skips itself where ~/.claude is absent (CI).
files: ^(linux_configuration/tests/test_session_autopsy_hooks\.py|meta/scripts/mutate_session_autopsy_hooks\.py)$
- id: ci-mirror
name: CI mirror (clean-venv install + --all-files + changed-pkg pytest)
entry: bash meta/scripts/ci_mirror.sh
language: system
pass_filenames: false
always_run: true
require_serial: true
stages: [pre-push]
# ===========================================================================
# VULTURE - Dead code detection (disabled - doesn't work well with pre-commit)
# ===========================================================================
# - repo: https://github.com/jendrikseipp/vulture
# rev: v2.13
# hooks:
# - id: vulture
# args:
# - --min-confidence=80
# - --exclude=.venv,Bash,__pycache__
# exclude: ^(Bash/|\.venv/)
# ===========================================================================
# PYUPGRADE - Upgrade Python syntax (disabled - incompatible with Python 3.14)
# ===========================================================================
# - repo: https://github.com/asottile/pyupgrade
# rev: v3.19.0
# hooks:
# - id: pyupgrade
# args:
# - --py310-plus
# ===========================================================================
# CODESPELL - Spell checking in code (expanded ignore list for non-English)
# ===========================================================================
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
- id: codespell
args:
- --skip=*.json,*.lock,*-lock.yaml,*.min.js,*.min.css,.git,__pycache__,.venv,*.txt
# Many entries below are Polish words, not typos: they are field
# names from Polish data formats (e.g. `dokument` in the e-paragon /
# JPK e-receipt schema that billsplit parses). "Correcting" them to
# English breaks the parsers that match on them.
# `daa` is MediaTek's Download Agent Authentication, a BROM security
# feature named in the mtk_root notes - not a misspelling of "data".
# `artic` is SearXNG's engine id for the Art Institute of Chicago
# (setup_searxng.sh), not a misspelling of "arctic" - renaming it
# would stop matching the engine.
- --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,bloc,proces,serwer,parametr,adres,hart,dout,metod,tekst,synonim,grup,mosty,lokal,skalar,milion,nowe,tre,hel,alph,iif,dokument,daa,artic
exclude: ^(Bash/ffmpeg-build/|LaTeX/|.*\.geojson$)
# ===========================================================================
# DOCFORMATTER - Format docstrings (disabled - causes recursion errors)
# ===========================================================================
# - repo: local
# hooks:
# - id: docformatter
# name: docformatter
# entry: docformatter
# language: system
# types: [python]
# args:
# - --in-place
# - --wrap-summaries=88
# - --wrap-descriptions=88
# ===========================================================================
# INTERROGATE - Docstring coverage (disabled - causes recursion on large files)
# ===========================================================================
# - repo: https://github.com/econchick/interrogate
# rev: 1.7.0
# hooks:
# - id: interrogate
# args:
# - --fail-under=0
# - --verbose
# - --ignore-init-method
# - --ignore-init-module
# - --ignore-magic
# - --ignore-private
# - --ignore-semiprivate
# - --exclude=Bash,.venv,__pycache__
# pass_filenames: false
# ===========================================================================
# AUTOFLAKE - Remove unused imports/variables
# Disabled: fully redundant with ruff (F401, F841, F811) + --fix
# ===========================================================================
# - repo: https://github.com/PyCQA/autoflake
# rev: v2.3.1
# hooks:
# - id: autoflake
# args:
# - --in-place
# - --remove-all-unused-imports
# - --remove-unused-variables
# - --remove-duplicate-keys
# - --expand-star-imports
# ===========================================================================
# SAFETY - Check for security vulnerabilities in dependencies
# ===========================================================================
# Note: Safety requires API key for full functionality, disabled by default
# - repo: https://github.com/Lucas-C/pre-commit-hooks-safety
# rev: v1.3.2
# hooks:
# - id: python-safety-dependencies-check
# files: requirements.*\.txt$
# ===========================================================================
# PYRIGHT - Microsoft's type checker (very strict, optional)
# ===========================================================================
# Uncomment to enable - can be slow and very strict
# - repo: https://github.com/RobertCraiworthy/pyright-action
# rev: v1.1.350
# hooks:
# - id: pyright
# ===========================================================================
# CHECK JSON/YAML/TOML formatting (runs on push only — slow Node.js startup)
# ===========================================================================
- repo: local
hooks:
- id: prettier
name: prettier (capped scope)
# Wrapper runs prettier inside its own systemd-run scope so its
# memory budget is independent of the outer pre-push cgroup that
# has already accumulated page cache from pytest/mypy/pylint.
entry: meta/scripts/run_prettier_capped.sh
language: system
types_or: [yaml, json, markdown]
exclude: >-
(?x)^(
Bash/.*|
\.venv/.*|
third_party/.*|
\.github/skills/.*|
docs/superpowers/(plans|specs)/.*|
linux_configuration/report/.*|
.*\.lock
)$
stages: [pre-push]
# ===========================================================================
# SHELLCHECK - Shell script linting
# Wrapper batches files to avoid OOM on large repos.
# ===========================================================================
- repo: local
hooks:
- id: shellcheck
name: shellcheck
# NO --severity filter: every finding shellcheck reports, down to info
# and style, fails the commit. The previous --severity=warning quietly
# accepted 222 findings across 94 of 244 shell files -- and among them
# were real defects, not noise: two scripts that reported SUCCESS on an
# unknown command, five that sourced a path off by one directory (so
# they failed at runtime), a stale `source=` directive that had silently
# disabled following for six i3blocks scripts, and a diagnostic that
# would hang on stdin when its file list came back empty.
# Settings live in .shellcheckrc (external-sources, source-path).
entry: bash -c 'printf "%s\0" "$@" | xargs -0 -n 40 shellcheck' --
language: system
types: [shell]
# shellcheck only understands sh/bash/dash/ksh; it cannot parse zsh
# syntax (e.g. extended globs like (#s)). zsh files are checked by the
# zsh-syntax hook below instead.
exclude_types: [zsh]
# Vendored upstream code: we do not own it and patches here would be
# lost on the next vendor sync. Matches the prettier hook's exclusion.
#
# PKGBUILDs are makepkg manifests, not standalone scripts: they have no
# shebang by design, and pkgname/pkgver/source/sha512sums are read by
# makepkg while pkgdir/srcdir are injected by it. shellcheck therefore
# reports every one of them as unused-or-undefined (SC2148/SC2034/
# SC2154) -- all false positives, and none suppressible without
# littering the file with directives. Same reasoning as zsh above:
# the dialect is outside what shellcheck can judge.
exclude: ^third_party/|(^|/)PKGBUILD$
# ===========================================================================
# ZSH SYNTAX - zsh -n parse check (shellcheck has no zsh mode)
# ===========================================================================
- repo: local
hooks:
- id: zsh-syntax
name: zsh syntax check
entry: bash -c 'for f in "$@"; do zsh -n "$f" || exit 1; done' --
language: system
types: [zsh]
# ===========================================================================
# CHECK PYTHON LOCATION - All Python files must be under python_pkg/
# ===========================================================================
- repo: local
hooks:
- id: check-python-location
name: check Python files are under python_pkg/
entry: meta/scripts/check_python_location.sh
language: script
types: [python]
# ===========================================================================
# REMOVE EMPTY DIRECTORIES - Clean up empty folders in the repo
# ===========================================================================
- repo: local
hooks:
- id: remove-empty-dirs
name: remove empty directories
entry: find . -type d -empty -not -path './.git/*' -delete -print
language: system
pass_filenames: false
always_run: true
# ===========================================================================
# SECRET PATTERNS - Block commits containing sensitive data
# ===========================================================================
- repo: local
hooks:
- id: check-no-secrets
name: check for leaked secrets
entry: meta/scripts/check_no_secrets.sh
language: script
exclude: ^(\.secret-patterns|\.pre-commit-config\.yaml|.*\.geojson)$
- id: check-no-dufs-conf
name: block dufs secret config (.dufs_cloud.conf)
entry: meta/scripts/check_no_dufs_conf.sh
language: script
always_run: true
pass_filenames: false
# ===========================================================================
# COMMITIZEN - Conventional commits (optional)
# ===========================================================================
# - repo: https://github.com/commitizen-tools/commitizen
# rev: v3.13.0
# hooks:
# - id: commitizen
# - id: commitizen-branch
# stages: [push]