From 279a7b1ed78649b1111caded2e558931e603b1cc Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 4 Oct 2026 17:15:50 +0200 Subject: [PATCH] repo contract: CLAUDE.md commands, test_changed, CI gate Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/repo-contract.yml | 25 ++++++++++++++ .pre-commit-config.yaml | 6 ++++ AGENTS.md | 9 +++++ scripts/check_repo_contract.sh | 26 +++++++++++++++ scripts/test_changed.sh | 52 +++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 .github/workflows/repo-contract.yml create mode 100755 scripts/check_repo_contract.sh create mode 100755 scripts/test_changed.sh diff --git a/.github/workflows/repo-contract.yml b/.github/workflows/repo-contract.yml new file mode 100644 index 0000000..89d5597 --- /dev/null +++ b/.github/workflows/repo-contract.yml @@ -0,0 +1,25 @@ +name: repo-contract + +# The local pre-commit hook can be skipped (a fresh clone has no hooks), so the +# contract is enforced on push too. Mirrors md-naming.yml. +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + repo-contract: + name: repo contract + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check out the shared gate + uses: actions/checkout@v4 + with: + repository: kuhyx/utils + path: .utils + + - name: Enforce the repo contract + run: bash .utils/scripts/check_repo_contract.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a74a83b..55c435a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,6 +36,12 @@ repos: - repo: local hooks: + - id: repo-contract + name: repo contract (CLAUDE.md Commands + test_changed) + entry: scripts/check_repo_contract.sh + language: system + pass_filenames: false + files: ^(CLAUDE\.md|AGENTS\.md|scripts/test_changed\.sh)$ - id: no-noqa name: Block noqa comments entry: '(?i)#\s*(noqa|type:\s*ignore)' diff --git a/AGENTS.md b/AGENTS.md index 3555da5..4730f19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,3 +79,12 @@ now < exempt_until*; the 09:30/11:00 cutoffs live in wake-alarm (`DOCS-morning-session-pc.md`) and must not be re-derived here. The enforce path waits up to 30 s for a freshly booted PC's refresher; status paths never wait, and tests pin the retry to 0. + +## Commands + +- run: `./run.sh` (locks the screen; never launch on the live display for checks) +- test: `python -m pytest -n auto -p no:sugar -q` +- test-changed: `scripts/test_changed.sh` +- lint: `pre-commit run ruff --all-files` +- coverage: `python -m pytest -n auto -p no:sugar -q` +- coverage-gaps: `coverage-gaps coverage.lcov` diff --git a/scripts/check_repo_contract.sh b/scripts/check_repo_contract.sh new file mode 100755 index 0000000..8488858 --- /dev/null +++ b/scripts/check_repo_contract.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# ============================================================================ +# Fail if this repo breaks the shared repo contract. +# +# Thin delegate to the shared gate in ~/src/utils, which owns the contract +# text. Copy this file to scripts/check_repo_contract.sh in a repo; never copy +# the shared gate itself, or the shim would exec itself forever. +# ============================================================================ + +set -euo pipefail + +readonly SHARED_GATE="${UTILS_ROOT:-$HOME/src/utils}/scripts/check_repo_contract.sh" + +main() { + if [[ ! -x "$SHARED_GATE" ]]; then + echo "Error: shared repo-contract gate not found at $SHARED_GATE" >&2 + echo " Clone github.com/kuhyx/utils to ~/src/utils, or set" >&2 + echo " UTILS_ROOT to where it lives." >&2 + exit 1 + fi + + exec bash "$SHARED_GATE" "$@" +} + +main "$@" diff --git a/scripts/test_changed.sh b/scripts/test_changed.sh new file mode 100755 index 0000000..c84a69c --- /dev/null +++ b/scripts/test_changed.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# ============================================================================ +# Run only the tests related to files changed vs HEAD (staged, unstaged and +# untracked). Quiet: failures plus a one-line summary. Falls back to the full +# suite when a change cannot be mapped to tests. Generated by repo_contract. +# ============================================================================ + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" +CHANGED=() +while IFS= read -r f; do + [[ -n "$f" && -e "$f" ]] && CHANGED+=("$f") +done < <({ git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u) + +pytest_run() { + # addopts in pyproject adds -v and the 100% coverage bar, which a partial + # run can never meet; replace it with the flags the suite really needs. + python3 -m pytest -o addopts="--strict-markers --strict-config -p screen_locker.tests.test_sync_fixtures" \ + -p no:sugar --no-cov "$@" +} + +if grep -qs testmon pyproject.toml requirements*.txt; then + pytest_run --testmon -q --tb=short; exit $? +fi + +full=0 +tests=() +for f in "${CHANGED[@]}"; do + case "$f" in + */conftest.py | conftest.py | pyproject.toml | requirements*.txt | pytest.ini | setup.cfg) full=1 ;; + *.py) + base="$(basename "$f" .py)" + base="${base#_}" # private modules (_foo.py) are tested by test_foo.py + if [[ "$base" == test_* || "$base" == *_test ]]; then + tests+=("$f") + else + while IFS= read -r t; do tests+=("$t"); done \ + < <(git ls-files "*test_${base}*.py" "*test__${base}*.py" "*${base}_test.py") + fi + ;; + esac +done + +if [[ $full -eq 0 && ${#CHANGED[@]} -eq 0 ]]; then echo "no changes vs HEAD: nothing to test"; exit 0; fi +if [[ $full -eq 0 && ${#tests[@]} -gt 0 ]]; then pytest_run -q --tb=short "${tests[@]}"; exit $?; fi +if [[ $full -eq 0 ]] && ! printf '%s\n' "${CHANGED[@]}" | grep -q '\.py$'; then + echo "no python changes: nothing to test"; exit 0 +fi +echo "no mapped tests: running full suite" +pytest_run -q --tb=short