#!/usr/bin/env bash# Lint one directory: whitespace, mypy, flake8, shellcheck, plus docstrings and layering for packages.dir_to_check=$1if[[-z"${dir_to_check}"]];thenecho"Error: Directory to check must be provided as first argument">&2exit1fi# trailing-whitespace: auto-fix all text files in ${dir_to_check}, mirroring# the pre-commit hook (scripts/setup/hooks/pre-commit.template). Globs match# the same families flagged by `git diff --check`. Add new extensions here# when the repo grows to include them.fix_files=$(find"${dir_to_check}"-typef\(\-name'*.py'-o-name'*.c'-o-name'*.h'-o-name'*.html'\-o-name'*.json'-o-name'*.md'-o-name'*.sh'-o-name'*.txt'\-o-name'*.template'-o-name'*.yml'-o-name'*.yaml'\))if[[-n"$fix_files"]];thenecho"$fix_files"|xargs-d'\n'sed-i's/[[:space:]]*$//'fi# trailing-whitespace: check remaining changes (exclude encrypted files)echo"checking trailing-whitespace ${dir_to_check} ..."gitdiff--check--no-color--"${dir_to_check}"':!*.enc'ws_rc=$?echo"trailing-whitespace ${dir_to_check} -> ${ws_rc}"# Check if there are Python filesif!find"${dir_to_check}"-typef-name"*.py"|grep-q.;thenecho"No Python files found in ${dir_to_check}, skipping mypy"mypy_rc=0elseecho"checking mypy ${dir_to_check} ..."mypy"${dir_to_check}"mypy_rc=$?echo"mypy ${dir_to_check} -> ${mypy_rc}"fiecho"checking flake8 ${dir_to_check} ..."flake8"${dir_to_check}"flake8_rc=$?echo"flake8 ${dir_to_check} -> ${flake8_rc}"# Shell lint — the shell half of the tree, held to the same bar as the Python half.# (This comment does not open with the tool's name: a comment starting `# shellcheck`# in front of a command is read as a *directive*, and an unparseable one is an error.)# The git-hook templates are shell too — they only become .git/hooks/* once# githooks.sh substitutes __VENV__ — so they are linted alongside the scripts that# render them. Options come from .shellcheckrc at the repo root — shared with the git# hooks and your editor — so nothing is configured on this command line. shellcheck is# a base apt package (scripts/setup/dev_env.sh), so a missing binary is a broken# install, not an opt-out — it fails.# The file list goes through an array rather than `| xargs`, so the reported code is# the linter's own 1 and not xargs' 123 — the lanes' codes are summed for the exit# status below, where a needlessly large one is both misleading and closer to the# wrap at 256.mapfile-tsh_files<<(find"${dir_to_check}"-typef\(-name'*.sh'-o-name'*.template'\))if((${#sh_files[@]}==0));thenecho"No shell scripts found in ${dir_to_check}, skipping shellcheck"shellcheck_rc=0elif!command-vshellcheck>/dev/null2>&1;thenecho"Error: shellcheck is not installed — run 'make install-system'">&2shellcheck_rc=1elseecho"checking shellcheck ${dir_to_check} ..."shellcheck--"${sh_files[@]}"shellcheck_rc=$?echo"shellcheck ${dir_to_check} -> ${shellcheck_rc}"fi# the interaction guard: `dialogue.interactive` must be called through its module, never# bound by import. A by-value copy is a second seam that `--silent` and the tests cannot# reach, so a command would prompt when it must not (docs/developer-guide.md §3.10).seam_rc=0ifgrep-rn'from solver.shell.dialogue import.*\binteractive\b'"${dir_to_check}"--include='*.py';thenecho"^ import the module and call dialogue.interactive() instead">&2seam_rc=1fiecho"interaction guard ${dir_to_check} -> ${seam_rc}"# command docstrings: the standard in docs/developer-guide.md §3.8, enforced by the# `check-commands` shell command. Only meaningful for the packages — the command# registry is what it walks, and only an admin subject sees all of it (the command is# admin-floored, so a lesser profile finds no such command and this step is skipped).# Run from the host shell, which loads the web host's commands as well as the solver's.doclint_rc=0layers_rc=0if[["${dir_to_check}"==packages||"${dir_to_check}"==*/packages]];thenecho"checking command docstrings ..."python-meuler_webshell.shell.host"check-commands"doclint_rc=$?echo"command docstrings -> ${doclint_rc}"# layering: webshell → site → solver, never upward (docs/package-guide.md § 1).echo"checking layering ..."python"$(dirname"$0")/layers.py""$(dirname"${dir_to_check}")"layers_rc=$?echo"layering -> ${layers_rc}"fiexit$(("${ws_rc}"+"${mypy_rc}"+"${flake8_rc}"+"${shellcheck_rc}"+"${seam_rc}"+"${doclint_rc}"+"${layers_rc}"))