scripts/code_health measures the files a change touches on both sides (base revision and head) and fails when: - a function or file over target grows past its own value on the base (CC 20, 300 own lines, nesting 6, file 2,000 lines); - a new function or file is over target; - a pattern rule gains a fingerprint (rule + enclosing function + line text), so fixing one violation never buys another. Code that moves keeps its cap: functions match by name-independent body hash, pattern hits by (rule, line text) once their function has left the old place. Nested functions are their own units, so a closure edit never counts against the hook or function around it. Python CC is ruff's C901 minus the direct nested defs; TypeScript is parsed with the lockfile's typescript (no type check). Rules: swallowed exceptions (S110, BLE001, E722), correctness (F821/F823/ F811/F841, RUF006), hangs (ASYNC230/240, S113, subprocess/urlopen without timeout, sync config I/O in async def, get_event_loop, gather results checked against Exception), profile scope (hardcoded ~/.hermes, a new HERMES_* env var, UnscopedSecretError -> os.getenv, import-time capture, raw threading.Thread, P05/P06 from profile_scope_patterns.json), argv process identity, bool() of an env string, and if/elif ladders. Growth of a file already over 2,000 lines is advisory: replaying 300 merged PRs, a blocking file-size ratchet would have stopped 46 of them before the 119 over-target files are split. Everything else blocks; the same replay finds 18 PRs that would have needed a change, every finding a real hit for its rule. No baseline file: base is measured on the fly, so an improvement that lands on main is the new cap at once and parallel PRs never conflict on ratchet state. `# health: allow <RULE> -- <why>` is the reviewed escape.
105 lines
4.3 KiB
Python
Executable File
105 lines
4.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Run every blocking lint check CI runs, locally, with the same pinned tools.
|
|
|
|
python scripts/check # everything, against the merge-base with origin/main
|
|
python scripts/check --only health # just the code-health ratchet
|
|
python scripts/check --install-hook # run it as the git pre-commit hook
|
|
|
|
CI runs the same command (``.github/workflows/lint.yml``), so a green local run means a green
|
|
lint lane. The pre-commit hook is skippable (``git commit --no-verify``); CI is not.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO))
|
|
|
|
from scripts.code_health import gitio # noqa: E402
|
|
from scripts.code_health.ruff_runner import resolve_ruff # noqa: E402
|
|
|
|
_HOOK_MARK = "# hermes-agent scripts/check pre-commit hook"
|
|
_HOOK = f"""#!/usr/bin/env bash
|
|
{_HOOK_MARK} (installed by `scripts/check --install-hook`; skip once with --no-verify)
|
|
repo="$(git rev-parse --show-toplevel)"
|
|
# Hooks are shared by every worktree; a branch that predates scripts/check commits unchecked.
|
|
[ -f "$repo/scripts/check" ] || exit 0
|
|
py=python3
|
|
command -v python3 >/dev/null 2>&1 || py=python
|
|
exec "$py" "$repo/scripts/check"
|
|
"""
|
|
|
|
# name -> argv (relative to the repo root); mirrors the blocking steps in lint.yml.
|
|
_GUARDS = {
|
|
"footguns": ["scripts/check-windows-footguns.py", "--all"],
|
|
"shebangs": ["scripts/check_bash_shebangs.py"],
|
|
"no-tmp": ["scripts/check_no_tmp_literals.py"],
|
|
"config-writers": ["scripts/check_config_yaml_writers.py"],
|
|
"os-marker-fakes": ["scripts/ci/check_os_marker_fakes.py"],
|
|
}
|
|
|
|
|
|
def _run(argv: list[str]) -> tuple[int, str]:
|
|
proc = subprocess.run(
|
|
argv, cwd=REPO, capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
stdin=subprocess.DEVNULL, timeout=900, check=False,
|
|
)
|
|
return proc.returncode, (proc.stdout + proc.stderr).strip()
|
|
|
|
|
|
def _jobs(only: str | None, base: str | None) -> dict[str, list[str]]:
|
|
jobs = {name: [sys.executable, *argv] for name, argv in _GUARDS.items()}
|
|
jobs["ruff"] = [*resolve_ruff(REPO), "check", "."]
|
|
health = [sys.executable, "-m", "scripts.code_health"]
|
|
jobs["health"] = health + (["--base", base] if base else [])
|
|
if only:
|
|
return {name: argv for name, argv in jobs.items() if name in only.split(",")}
|
|
return jobs
|
|
|
|
|
|
def install_hook() -> int:
|
|
hooks = Path(gitio.git(REPO, "rev-parse", "--path-format=absolute", "--git-path", "hooks").strip())
|
|
target = hooks / "pre-commit"
|
|
if target.exists() and _HOOK_MARK not in target.read_text(encoding="utf-8-sig", errors="replace"):
|
|
print(f"scripts/check: {target} exists and is not ours; add `python scripts/check` to it")
|
|
return 1
|
|
hooks.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(_HOOK, encoding="utf-8", newline="\n")
|
|
os.chmod(target, 0o755) # windows-footgun: ok — chmod +x is a no-op on Windows, harmless
|
|
print(f"scripts/check: pre-commit hook installed at {target}")
|
|
return 0
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(prog="scripts/check", description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--only", help=f"comma list of: {', '.join([*_GUARDS, 'ruff', 'health'])}")
|
|
parser.add_argument("--base", help="base revision for the code-health ratchet")
|
|
parser.add_argument("--install-hook", action="store_true")
|
|
args = parser.parse_args(argv)
|
|
if args.install_hook:
|
|
return install_hook()
|
|
try:
|
|
jobs = _jobs(args.only, args.base)
|
|
except RuntimeError as exc:
|
|
print(f"scripts/check: {exc}", file=sys.stderr)
|
|
return 2
|
|
with ThreadPoolExecutor(max_workers=len(jobs) or 1) as pool:
|
|
results = dict(zip(jobs, pool.map(_run, jobs.values()), strict=True))
|
|
failed = [name for name, (code, _) in results.items() if code != 0]
|
|
for name in failed:
|
|
print(f"\n=== {name} (failed) ===\n{results[name][1]}")
|
|
status = "FAILED: " + ", ".join(failed) if failed else "ok"
|
|
print(f"\nscripts/check: {len(results)} checks, {status}")
|
|
return 1 if failed else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|