Add quick gate and independent review pipeline
Introduce tools/quick_gate.py: a one-second check that parses every file in protocol, tests and tools, imports each protocol module, and runs the functional tests of the labs that need no experiment data. Wire it to a PostToolUse hook so edits under protocol or tests are checked automatically, and add two read-only review agents with slash commands that drive them: verifier for independent re-derivation of results, adversary for unsafe-state hunting in control, failsafe, session and reset code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
145
tools/quick_gate.py
Normal file
145
tools/quick_gate.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Быстрый шлюз проверки SDR Rover Link.
|
||||
|
||||
Проверяет за одну-две секунды то, что ломается чаще всего:
|
||||
|
||||
1. Синтаксис всех файлов в protocol, tests и tools.
|
||||
2. Импорт каждого модуля protocol.
|
||||
3. Функциональные проверки тех лаб, которым не нужны данные эксперимента.
|
||||
|
||||
Полный прогон лабы шлюз не заменяет.
|
||||
Он отвечает на один вопрос: не сломана ли кодовая база прямо сейчас.
|
||||
|
||||
Запуск:
|
||||
python tools/quick_gate.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import importlib
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
CHECKED_DIRECTORIES = ("protocol", "tests", "tools")
|
||||
|
||||
|
||||
def check_syntax() -> list[str]:
|
||||
"""Разобрать все файлы Python и вернуть список сообщений об ошибках."""
|
||||
|
||||
failures: list[str] = []
|
||||
|
||||
for directory in CHECKED_DIRECTORIES:
|
||||
for path in sorted((PROJECT_ROOT / directory).glob("*.py")):
|
||||
try:
|
||||
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
except SyntaxError as error:
|
||||
relative = path.relative_to(PROJECT_ROOT)
|
||||
failures.append(f"{relative}:{error.lineno}: синтаксис: {error.msg}")
|
||||
|
||||
return failures
|
||||
|
||||
|
||||
def check_protocol_imports() -> list[str]:
|
||||
"""Импортировать каждый модуль protocol и вернуть список сообщений об ошибках."""
|
||||
|
||||
failures: list[str] = []
|
||||
|
||||
for path in sorted((PROJECT_ROOT / "protocol").glob("*.py")):
|
||||
if path.name == "__init__.py":
|
||||
continue
|
||||
|
||||
module_name = f"protocol.{path.stem}"
|
||||
|
||||
try:
|
||||
importlib.import_module(module_name)
|
||||
except Exception:
|
||||
failures.append(f"{module_name}: импорт: {traceback.format_exc(limit=3).strip()}")
|
||||
|
||||
return failures
|
||||
|
||||
|
||||
def check_functional_tests() -> tuple[list[str], int, int]:
|
||||
"""
|
||||
Выполнить функциональные проверки лаб, не требующие данных эксперимента.
|
||||
|
||||
Возвращает:
|
||||
failures:
|
||||
Список сообщений об ошибках.
|
||||
|
||||
executed:
|
||||
Количество выполненных лаб.
|
||||
|
||||
skipped:
|
||||
Количество пропущенных лаб.
|
||||
"""
|
||||
|
||||
failures: list[str] = []
|
||||
executed = 0
|
||||
skipped = 0
|
||||
|
||||
for path in sorted((PROJECT_ROOT / "tests").glob("lab*.py")):
|
||||
source = path.read_text(encoding="utf-8")
|
||||
|
||||
if "def run_functional_tests" not in source:
|
||||
continue
|
||||
|
||||
module_name = f"tests.{path.stem}"
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except Exception:
|
||||
failures.append(f"{module_name}: импорт: {traceback.format_exc(limit=3).strip()}")
|
||||
continue
|
||||
|
||||
run_functional_tests = module.run_functional_tests
|
||||
parameters = inspect.signature(run_functional_tests).parameters
|
||||
required = [name for name, parameter in parameters.items() if parameter.default is parameter.empty]
|
||||
|
||||
if required:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
run_functional_tests()
|
||||
except Exception:
|
||||
failures.append(f"{module_name}: проверки: {traceback.format_exc(limit=3).strip()}")
|
||||
continue
|
||||
|
||||
executed += 1
|
||||
|
||||
return failures, executed, skipped
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Выполнить шлюз и вернуть код возврата процесса."""
|
||||
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
syntax_failures = check_syntax()
|
||||
|
||||
if syntax_failures:
|
||||
print("Шлюз не пройден: ошибки синтаксиса")
|
||||
for failure in syntax_failures:
|
||||
print(f" {failure}")
|
||||
return 1
|
||||
|
||||
import_failures = check_protocol_imports()
|
||||
test_failures, executed, skipped = check_functional_tests()
|
||||
failures = import_failures + test_failures
|
||||
|
||||
if failures:
|
||||
print("Шлюз не пройден")
|
||||
for failure in failures:
|
||||
print(f" {failure}")
|
||||
return 1
|
||||
|
||||
print(f"Шлюз пройден: синтаксис в порядке, protocol импортируется, функциональных проверок выполнено {executed}, пропущено {skipped}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user