#!/usr/bin/env bash# Generated by scripts/setup/githooks.sh — do not edit directly.# Source of truth: scripts/setup/hooks/pre-commit.template## Output contract: every check announces itself and the hook always ends with# a single PASS/FAIL verdict naming exactly which checks failed, so a buried# error can't be masked by a later tool's "Success" line.set-uopipefail# Resolve the working tree at runtime so the hook operates on the checkout# actually being committed — including linked worktrees. A hardcoded path would# point the sed/git-add fixer at the wrong tree and silently drop new files.# VENV stays absolute: the virtualenv lives only in the main checkout.REPO_ROOT="$(gitrev-parse--show-toplevel)"VENV="__VENV__"cd"$REPO_ROOT"||exit1# ── Reporting helpers ────────────────────────────────────────────────────FAILURES=()section(){printf'\n── %s ──\n'"$1">&2;}note(){printf' %s\n'"$1">&2;}fail(){FAILURES+=("$1");printf' ✗ %s\n'"$1">&2;}pass(){printf' ✓ %s\n'"$1">&2;}print_verdict(){printf'\n'>&2if((${#FAILURES[@]}==0));thenprintf'✓ pre-commit: all checks passed.\n'>&2return0fiprintf'════════════════════════════════════════════════════════════\n'>&2printf'✗ pre-commit BLOCKED — %d check(s) failed:\n'"${#FAILURES[@]}">&2localfforfin"${FAILURES[@]}";doprintf' • %s\n'"$f">&2doneprintf' (scroll up to the matching "── ... ──" section for details)\n'>&2printf'════════════════════════════════════════════════════════════\n'>&2return1}# ── trailing-whitespace / conflict markers: check only, no auto-fix ──────# Every staged file is checked, solutions/private included. Those blobs are# gitfilter ciphertext (solver.crypto.gitfilter) which contains NUL bytes, so# git classifies them as binary and skips the whitespace check automatically —# no exclude needed, and no risk of flagging random ciphertext bytes.section"trailing whitespace"ifgitdiff--check--cached--no-color;thenpass"no trailing-whitespace / conflict markers in staged changes"elsefail"staged changes contain trailing whitespace or conflict markers"fi# ── compiled binaries: drop staged build artifacts ───────────────────────# The C runner compiles each solution to an executable (p<NNNN>_s<K>_c, no# extension); these are gitignored build artifacts regenerated on demand and# must never enter a commit. Inspect each staged blob (not the worktree file)# and unstage any that begins with the ELF magic (7f 45 4c 46).section"compiled binaries"removed_bins=()whileIFS=read-rf;do[[-z"$f"]]&&continuehead_hex=$(gitcat-file-p":$f"2>/dev/null|head-c4|xxd-p)if[["$head_hex"=="7f454c46"]];thengitrm--cached--quiet--"$f"removed_bins+=("$f")fidone<<(gitdiff--cached--name-only--diff-filter=d)if((${#removed_bins[@]}>0));thennote"unstaged compiled binaries (excluded from this commit):"forbin"${removed_bins[@]}";donote" $b";donepass"compiled binaries removed from the index"elsepass"no compiled binaries staged"fi# ── gitignored files: block staged paths matched by .gitignore ───────────# `git check-ignore` reports a path as NOT ignored once it is in the index, so# --no-index is required to evaluate the staged set against the ignore rules# directly. That also drops git's "tracked files are never ignored" rule, so a# tracked file matching a pattern would be a false positive — but .gitignore is# kept correct (its `!.gitignore` / `!.gitattributes` negations follow every# broad dot rule, so no tracked file matches), which is why the whole staged# set can be checked. Compiled binaries were already dropped above; this catches# anything else gitignored (other build artifacts, .env, logs, …) force-added.section"gitignored files"staged=$(gitdiff--cached--name-only--diff-filter=d)ignored=""if[[-n"$staged"]];thenignored=$(printf'%s\n'"$staged"|gitcheck-ignore--no-index--stdin2>/dev/null||true)fiif[[-z"$ignored"]];thenpass"no gitignored files staged"elsenote"staged files are matched by .gitignore:"whileIFS=read-rg;donote" $g";done<<<"$ignored"fail"gitignored files are staged — unstage them (or commit deliberately)"fi# ── audit/: gitignored whenever it exists ─────────────────────────────────# audit/ holds the run logs (scripts/ops/capture-run.sh, release.sh) and review material# that name collaborators and the host's layout. It is safe only while .gitignore covers# it, so check the rule itself (--no-index), not just what happens to be staged — and# that nothing under it is already tracked.section"audit/ folder (gitignored)"if[[!-eaudit]];thenpass"no audit/ folder in this checkout"elif!gitcheck-ignore--no-index-qaudit/;thenfail"audit/ exists but is not gitignored — restore '/audit/' in .gitignore"elif[[-n"$(gitls-files--audit)"]];thenfail"files under audit/ are tracked — 'git rm -r --cached audit' before committing"elsepass"audit/ is gitignored"fi# ── encryption filter: staged private blobs must be ciphertext ───────────# The commit-side twin of pre-push's encryption-at-rest audit: every STAGED# blob under solutions/private must begin with the gitfilter MAGIC. With the# clean filter wired and working, staging plaintext produces ciphertext, so# this passes untouched; a blob lacking the magic means the filter is unwired# or broken in this clone — the exact state that would commit (and later push)# private plaintext to the public repo. Ciphertext staged with the filter# unwired (an unauthorized per-user clone moving files) is fine: the property# gated is the blob's content, not the wiring.section"encryption filter (solutions/private)"magic_hex=$("$VENV/bin/python"-c'from solver.crypto import wire; print(wire.MAGIC.hex())'2>/dev/null)if[[-z"$magic_hex"]];thenfail"could not read the gitfilter magic (is the venv intact?)"elsemlen=$((${#magic_hex}/2))plain_private=()staged_private=0whileIFS=read-rf;do[[-z"$f"]]&&continuestaged_private=$((staged_private+1))head_hex=$(gitcat-file-p":$f"2>/dev/null|head-c"$mlen"|xxd-p)[["$head_hex"=="$magic_hex"]]||plain_private+=("$f")done<<(gitdiff--cached--name-only--diff-filter=d--solutions/private)if((staged_private==0));thenpass"no private files staged"elif((${#plain_private[@]}==0));thenpass"$staged_private staged private blob(s), all ciphertext (magic=0x$magic_hex)"elsenote"staged PLAINTEXT under solutions/private:"forpin"${plain_private[@]}";donote" !! $p";donenote"the encryption filter is not wired/working in this clone. Fix it, then re-stage:"note" solver shell: git-filter install (verifies key access first)"note" or directly: $VENV/bin/python -m solver.crypto.gitfilter install"note" then: git add --renormalize -- solutions/private"fail"plaintext private files staged — the gitfilter is not working"fifi# ── shellcheck: staged shell scripts ─────────────────────────────────────# Scoped to what you staged, unlike the flake8/mypy sweeps below: a shell script is# checked on its own with no cross-file resolution, so sweeping the tree here buys# nothing — pre-push does that as the backstop, the same commit-side/push-side pairing# the audits above use. The hook templates are shell too (they only become# .git/hooks/* once githooks.sh substitutes __VENV__), so they are linted with the# same rules as the scripts that render them.## Like flake8/mypy, this reads the WORKTREE file rather than the staged blob, so a# finding names a real path and line you can jump to. shellcheck is a base apt package# (scripts/setup/dev_env.sh) — a missing binary is a broken install, not an opt-out.## No options here: they live in .shellcheckrc at the repo root, so this lane, pre-push's# whole-tree lane, scripts/linters/check.sh and your editor all judge a script the same# way. `external-sources` there is what keeps THIS lane honest — without it, a script# would pass or fail on whether the file it sources happened to be staged alongside it.section"shellcheck (staged shell scripts)"mapfile-tstaged_sh<<(gitdiff--cached--name-only--diff-filter=d--'*.sh''*.template')if((${#staged_sh[@]}==0));thenpass"no shell scripts staged"elif!command-vshellcheck>/dev/null2>&1;thennote"install it with: make install-system"fail"shellcheck is not installed"elifshellcheck--"${staged_sh[@]}";thenpass"shellcheck clean (${#staged_sh[@]} script(s))"elsefail"shellcheck reported issues"fi# ── flake8: packages + solutions ───────────────────────────────────────────# The whole solutions tree (public + decrypted private) is a proper package —# every directory carries an __init__.py — so module resolution is unambiguous# and the lint scope is simply `solutions packages`.section"flake8 (solutions, packages)"if"$VENV/bin/flake8"solutionspackages;thenpass"flake8 clean"elsefail"flake8 reported issues"fi# ── mypy: packages + solutions + scripts + tests ───────────────────────────# `scripts` is in scope because it holds tracked Python that ships and runs:# scripts/setup/euler-smtp.py is installed to /usr/local/bin and run as a service.# Type-checking the shell half of the tree (shellcheck, above) while leaving that# unchecked was the gap. `tests` is in scope so a refactor that changes a signature is# caught in the tests that still call the old one, before the suite has to run.section"mypy (solutions, packages, scripts, tests)"if"$VENV/bin/mypy"solutionspackagesscriptstests;thenpass"mypy clean"elsefail"mypy reported type errors"fi# ── layering: the solver shell must not import the web tier ──────────────# docs/package-guide.md § 1. Known couplings sit in scripts/linters/layers.allow,# which only ratchets down: a new import fails, and so does a stale allowlist line.section"layering (webshell → site → solver)"if"$VENV/bin/python"scripts/linters/layers.py.;thenpass"no import of a higher layer"elsefail"a package imports a layer above it (or layers.allow has a stale line)"fi# ── Verdict ──────────────────────────────────────────────────────────────print_verdictexit$?