diff --git a/.claude/agents/adversary.md b/.claude/agents/adversary.md new file mode 100644 index 0000000..5448dd1 --- /dev/null +++ b/.claude/agents/adversary.md @@ -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 не проверено`. diff --git a/.claude/agents/verifier.md b/.claude/agents/verifier.md new file mode 100644 index 0000000..6e1b085 --- /dev/null +++ b/.claude/agents/verifier.md @@ -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 подтверждённых находок`. diff --git a/.claude/commands/check.md b/.claude/commands/check.md new file mode 100644 index 0000000..d608d5b --- /dev/null +++ b/.claude/commands/check.md @@ -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. Разбери находки как арбитр: воспроизведи, отклони невоспроизводимые вслух, + почини подтверждённые. + +## Отчёт пользователю + +Коротко: что запускалось, что нашли, что починили, что отклонили и почему. +Отчёты агентов пользователь не видит — пересказывай сам, не отсылай к ним. diff --git a/.claude/commands/lab.md b/.claude/commands/lab.md new file mode 100644 index 0000000..eaec856 --- /dev/null +++ b/.claude/commands/lab.md @@ -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: краткое описание на английском`. +Коммить только после явного согласия пользователя. diff --git a/.claude/hooks/quick_gate_hook.py b/.claude/hooks/quick_gate_hook.py new file mode 100644 index 0000000..379dfd7 --- /dev/null +++ b/.claude/hooks/quick_gate_hook.py @@ -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()) diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..c39578f --- /dev/null +++ b/.claude/settings.json @@ -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": "Быстрый шлюз" + } + ] + } + ] + } +} diff --git a/tools/quick_gate.py b/tools/quick_gate.py new file mode 100644 index 0000000..b110806 --- /dev/null +++ b/tools/quick_gate.py @@ -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())