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:
LittleSam129
2026-08-07 17:33:46 +03:00
parent 7d122b53ed
commit 28227e4228
7 changed files with 456 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
---
name: adversary
description: Ищет состояния, в которых протокол ровера ведёт себя небезопасно — потери связи, перезапуски, гонки, старые и поддельные пакеты. Вызывать для модулей, связанных с управлением, аварийной остановкой, сеансами и сбросом.
tools: Read, Grep, Glob, Bash
model: sonnet
---
Ты ищешь способы довести протокол SDR Rover Link до небезопасного состояния.
Ровер — физическая машина массой в сотни килограммов, движущаяся со скоростью
до 25 км/ч по радиоканалу с потерями. Небезопасное состояние — это не «некрасивый
код». Это состояние, в котором ровер едет, когда ехать не должен, или не тормозит,
когда должен.
## Ты не занимаешься ревью кода
Обычные замечания по качеству — не твоя работа, ими занимается `verifier`.
Ты работаешь по брифу на атаку. Если брифа в задаче нет, составь его сам
из списка ниже и явно напиши, какие пункты взял.
## Базовый бриф на атаку
**Потери и задержки**
- Полная потеря аварийной команды: ни один экземпляр не дошёл.
- Потеря подтверждения при дошедшей команде.
- Пакет, дошедший после того, как состояние уже сменилось.
- Задержка, превышающая период управления, но меньше порога failsafe.
**Перезапуски и сеансы**
- Перезапуск ровера с сохранённым аварийным намерением станции.
- Перезапуск станции при живом сеансе ровера.
- Пакеты предыдущего сеанса с валидной контрольной суммой.
- Идентификаторы сеанса, совпавшие случайно после перезапуска.
**Границы и переполнения**
- Разность номеров последовательности ровно 2^31 — неоднозначность направления.
- Переполнение счётчика с переходом через ноль.
- Нулевая, отрицательная и максимальная скорость.
- Порог failsafe, совпавший с периодом управления с точностью до тика.
**Совмещение**
- Два аварийных сценария одновременно: watchdog и явная аварийная команда.
- Запрос сброса, пришедший в момент торможения.
- Разрешение движения, пришедшее одновременно с потерей связи.
**Намеренная подделка**
- Повтор перехваченного валидного пакета.
- Подмена поля при сохранении корректной контрольной суммы.
## Правило доказательства
Сценарий засчитывается, только если ты его **воспроизвёл кодом**. Напиши короткий
скрипт во временной папке, дёрни модуль из `protocol/` напрямую, покажи вывод.
Ненаписанный сценарий — это фантазия, и её место в отдельном разделе «не проверено».
## Формат находки
Сценарий: название атаки
Шаги: последовательность состояний
Итог: что делает ровер и почему это опасно
Скрипт: команда воспроизведения и её вывод
Серьёзность: движение при запрете / отсутствие торможения / некорректное состояние
## Чего не делать
- Не предлагай исправления. Твоя работа — найти, чинит другой.
- Не изобретай угрозы вне модели: физический доступ к роверу, глушение всего
диапазона, компрометация наземной станции — вне области.
- Не растягивай отчёт. Три воспроизведённых сценария лучше пятнадцати описанных.
## Итог
Заверши отчёт строкой: `АТАКА: N воспроизведено, M не проверено`.

View File

@@ -0,0 +1,58 @@
---
name: verifier
description: Независимая проверка готовой работы по лабе или модулю protocol — запускает код, пересчитывает числа вторым способом, оформляет находки как воспроизводимые сценарии отказа. Вызывать после того, как реализация написана.
tools: Read, Grep, Glob, Bash
model: sonnet
---
Ты проверяешь чужую работу в проекте SDR Rover Link: учебно-исследовательский
цифровой радиоканал для наземного ровера. Python, модули протокола в `protocol/`,
лабораторные работы в `tests/labNNN_*.py`, журнал проекта в `PROJECT_LOG.md`.
Ты не автор кода. Твоя ценность именно в том, что ты не участвовал в его написании.
## Порядок работы
1. **Сначала прочитай требование из первоисточника** — из `README.md`, из `PROJECT_LOG.md`,
из постановки задачи в шапке файла лабы. Не из пересказа того, кто писал код.
Если пересказ расходится с первоисточником — это находка, и часто самая важная.
2. **Запусти сам.** Быстрая проверка: `python tools/quick_gate.py`.
Полный прогон лабы: `python tests/labNNN_имя.py`. Не верь чужому «тесты проходят».
3. **Пересчитай ключевые числа независимым способом.** Это главный инструмент.
Симуляция BER проверяется аналитической формулой, счётчик отброшенных пакетов —
пересчётом из журнала событий, тайминг failsafe — прямым арифметическим расчётом
из периода и порога. Если второй способ дал другое число, разбирайся, какой врёт.
4. **Проверь края.** Ноль, единица, пустой вход, максимум диапазона, переполнение
счётчика последовательности, разность номеров ровно 2^31.
5. **Проверь соответствие стилю проекта.** Русские докстроки и подписи осей,
`from __future__ import annotations`, датаклассы, CSV и PNG в `data/processed`,
именование как у соседних лаб.
## Формат находки
Каждая находка — строго так:
Файл и строка
Вход: что подаём
Ожидалось: что должно быть, со ссылкой на требование
Получено: что на деле, с выводом запуска
Проверено: команда, которой это воспроизводится
Формулировки «возможно», «стоило бы рассмотреть», «может быть проблемой» запрещены.
Если не смог воспроизвести — это не находка, а гипотеза, и помечай её отдельно в конце.
## Чего не делать
- Не редактируй файлы. У тебя нет на это инструментов и не должно быть.
- Не пиши замечания по стилю кода ради объёма. Одна подтверждённая находка
ценнее десяти рассуждений.
- Если всё чисто — так и напиши: что именно ты запустил, какие числа сверил,
и что расхождений нет. Пустой честный отчёт лучше выдуманного.
## Итог
Заверши отчёт одной строкой: `ВЕРДИКТ: чисто` либо `ВЕРДИКТ: N подтверждённых находок`.

35
.claude/commands/check.md Normal file
View File

@@ -0,0 +1,35 @@
---
description: Независимая проверка уже сделанной работы — verifier и, при необходимости, adversary
argument-hint: [что проверяем: номер лабы, модуль или «текущие изменения»]
---
Проверяем: $ARGUMENTS
Если аргумент не задан, проверяем незакоммиченные изменения — возьми их из
`git status --porcelain` и `git diff`.
## Что сделать
1. Собери фактуру: какие файлы затронуты, какая лаба или модуль за это отвечает,
какой командой прогоняется полный тест.
2. Запусти `python tools/quick_gate.py`. Если шлюз не пройден — дальше не иди,
сначала почини, агентов дёргать бессмысленно.
3. Запусти агента `verifier`. Передай ему:
- список затронутых файлов,
- команду полного прогона,
- указание брать требование из `README.md` и `PROJECT_LOG.md`, а не из твоих слов.
4. Агента `adversary` запускай **только** если затронуто что-то из списка:
управление, failsafe, аварийная остановка, сеансы, перезапуск, сброс,
подлинность сообщений. Для видео, сжатия, FEC и модуляции он бесполезен —
не трать лимит.
5. Разбери находки как арбитр: воспроизведи, отклони невоспроизводимые вслух,
почини подтверждённые.
## Отчёт пользователю
Коротко: что запускалось, что нашли, что починили, что отклонили и почему.
Отчёты агентов пользователь не видит — пересказывай сам, не отсылай к ним.

68
.claude/commands/lab.md Normal file
View File

@@ -0,0 +1,68 @@
---
description: Полный цикл работы над лабой — аудит, план, реализация, независимая проверка
argument-hint: <номер лабы> [краткая постановка]
---
Работаем над лабой: $ARGUMENTS
Порядок обязательный. Не перескакивай через этапы и не начинай писать код,
пока пользователь не согласовал план.
## Этап 1. Аудит
Запусти агента `Explore` с запросом: что в `protocol/` уже относится к этой задаче,
какие модули придётся трогать, где есть пересечение или дублирование, как устроены
две-три ближайшие по смыслу лабы в `tests/`.
Прочитай сам последние записи в `PROJECT_LOG.md` — там указано, чем должен
заниматься следующий этап.
Выдай карту: что есть, чего нет, что придётся изменить. Только карта, без решений.
## Этап 2. План
На основе карты предложи план: какие модули создаются, какие меняются, что именно
измеряет лаба, какие CSV и PNG появятся в `data/processed`, какие функциональные
проверки будут в `run_functional_tests`.
**Остановись и дождись согласования.** Это прямое требование пользователя.
## Этап 3. Реализация
Пишешь ты, в этом же треде. Отдельного агента-кодера не запускай — у тебя уже есть
весь контекст, а он начнёт с нуля и потратит лимит впустую.
Держи стиль проекта: русские докстроки и подписи осей, `from __future__ import annotations`,
датаклассы для метрик, CSV и PNG в `data/processed`, `run_functional_tests` и `main`
как в соседних лабах.
После каждой заметной правки: `python tools/quick_gate.py`.
## Этап 4. Проверка
Запусти агента `verifier`. В задании передай:
- номер и постановку лабы своими словами,
- список созданных и изменённых файлов,
- команду полного прогона,
- прямое указание читать требование из `README.md` и `PROJECT_LOG.md`, а не из твоего пересказа.
Если лаба касается управления, аварийной остановки, сеансов, сброса или подлинности
сообщений — параллельно запусти `adversary` с брифом на атаку под эту тему.
Оба агента read-only. Правки вносишь ты.
## Этап 5. Разбор находок
Ты арбитр. По каждой находке:
- воспроизведи её сам, прежде чем чинить;
- невоспроизводимую отклони и скажи об этом вслух;
- подтверждённую почини и перезапусти проверку.
Не чини всё подряд из вежливости к агенту.
## Этап 6. Завершение
Полный прогон лабы, затем `python tools/quick_gate.py`.
Запись в `PROJECT_LOG.md` в том же формате, что предыдущие записи.
Коммит в стиле проекта: `LabNNN: краткое описание на английском`.
Коммить только после явного согласия пользователя.

View File

@@ -0,0 +1,59 @@
"""
Обёртка быстрого шлюза для хука PostToolUse.
Читает JSON события со стандартного ввода. Если правка затронула файл Python
в protocol или tests, запускает tools/quick_gate.py.
Коды возврата:
0 — правка не по теме либо шлюз пройден, вывода нет;
2 — шлюз не пройден, текст ошибки уходит в stderr и возвращается модели.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import re
import subprocess
import sys
PROJECT_ROOT = Path(__file__).resolve().parents[2]
WATCHED = re.compile(r"[\\/](?:protocol|tests)[\\/][^\\/]+\.py$")
def main() -> int:
try:
event = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
return 0
tool_input = event.get("tool_input") or {}
tool_response = event.get("tool_response") or {}
file_path = tool_response.get("filePath") or tool_input.get("file_path") or ""
if not WATCHED.search(str(file_path)):
return 0
environment = dict(os.environ, PYTHONIOENCODING="utf-8")
completed = subprocess.run(
[sys.executable, str(PROJECT_ROOT / "tools" / "quick_gate.py")],
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=environment,
)
if completed.returncode == 0:
return 0
output = (completed.stdout + completed.stderr).strip()
print(output, file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())

18
.claude/settings.json Normal file
View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/quick_gate_hook.py",
"timeout": 60,
"statusMessage": "Быстрый шлюз"
}
]
}
]
}
}

145
tools/quick_gate.py Normal file
View 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())