utils/scripts/check_subproject_scope.sh
Krzysztof kuhy Rudnicki 39e52d07c2 Stop the recovery tick stealing our own grab, and fix pre-commit for the monorepo
`_holds_grab` asked `grab_current() is self._root`, but our own transient
children take the grab for themselves: a posted Tk menu grabs the menu
window, so the test read "the grab was lost" and `grab_set_global()` took it
back a second later, off our own widget mid-interaction. That is what made
screen-locker's sport selector unusable while locked on 2026-07-26. The
predicate is now `grab_current() is not None` -- any window we own means the
lock still has pointer and keyboard -- and it is public, `holds_grab`, so an
embedder can ask the same question instead of re-deriving it. Anything that
does not resolve against our widget map (a Tcl-created `ttk.Combobox`
popdown, a stale name) raises `KeyError` and counts as lost: fail closed,
because losing a popdown beats believing we hold a grab we do not. The
blind spot for a second `tk.Tk()` reporting its root as "." is unchanged from
the old test, and now written down.

`_verify` and `_drain` scheduled the next tick *after* running it, so any
exception ended all re-assertion of coverage, grab and VT for the rest of the
lock -- the exact fail-open this module exists to prevent, in a file whose
docstring is a monotonicity proof. The reschedule moves into `finally`, and
the warnings only fire when the reason changes: these states can last the
whole lock, and a 1Hz journal stream is not the alternative to silence.

The pre-commit side: both subproject configs were written as if cwd were the
subproject, but pre-commit runs hooks from the git root -- which is ~/utils,
not gatelock. Every pushed run since at least 2026-07-11 has been red, and
for compounding reasons: pylint and bandit could not find their config,
bandit's tests exclusion never matched, the pytest hook ran with no config at
all, and with no `files:` filter each config linted the *whole* monorepo, so
gatelock's mypy was type-checking crdt-sync. All paths are now root-relative,
each config is scoped to its own subproject, and the pytest hook goes through
a shared `scripts/run_subproject_tests.sh` that supplies the missing `cd` and
fails loudly if the subproject is gone.

Scoping via `files:` has one sharp edge -- rename the directory and the regex
matches nothing, so every hook passes on zero files, green and useless. An
`always_run` scope-guard hook turns that silent pass into a failure.

bandit 1.7.10 threw inside its AST visitor on Python 3.14 and *skipped* every
file while still reporting zero issues; 1.9.4 actually scans. Codespell was
also flagging a real typo the noise had buried.

Housekeeping the above turned up: `gatelock/.github/` and `crdt-sync/.github/`
were dead -- GitHub only reads the root `.github/workflows`, which already has
all four jobs -- and the two subproject `.gitignore`s were byte-identical to
each other and subsumed by the new root one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 15:08:41 +02:00

41 lines
1.4 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Fail if a subproject's pre-commit config is scoped to a directory that has
# moved or vanished.
#
# Each subproject config carries `files: ^<subproject>/`. If the directory is
# renamed, that regex matches nothing and EVERY hook in the config reports
# "no files to check" -- green CI with no linting at all. This hook runs with
# always_run, so the top-level filter cannot skip it, and turns that silent
# pass into a loud failure.
#
# Usage: scripts/check_subproject_scope.sh <subproject>
# ============================================================================
set -euo pipefail
readonly REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
main() {
if [[ $# -ne 1 ]]; then
echo "Usage: $(basename "$0") <subproject>" >&2
exit 1
fi
local subproject="$1"
local config="$REPO_ROOT/$subproject/.pre-commit-config.yaml"
if [[ ! -f "$config" ]]; then
echo "Error: $subproject/.pre-commit-config.yaml is missing." >&2
echo " Every hook scoped to '^$subproject/' would silently pass." >&2
exit 1
fi
if ! grep -qx "files: \^$subproject/" "$config"; then
echo "Error: $config does not scope itself to '^$subproject/'." >&2
echo " Hooks would run over the whole monorepo, or over nothing." >&2
exit 1
fi
}
main "$@"