## ADDED Requirements ### Requirement: Форма тела запроса На тело неверной формы хаб SHALL отвечать 422, а не 500. В `POST /agent/state` поле `issues` SHALL быть списком, каждый его элемент — объектом с непустым строковым `key`. Иначе 422 `bad_state` с номером элемента, и пачка не применяется. В `POST /wiki/sync` поле `project` в теле, если задано, SHALL быть строкой. Иначе 422 `bad_request` «project — строка». #### Scenario: Элемент состояния не объект - **WHEN** агент шлёт `POST /agent/state {issues: ["A-1"]}` - **THEN** 422 `bad_state` «issues[0] — объект задачи с key», зеркало не изменилось #### Scenario: Проект вики не строкой - **WHEN** `POST /wiki/sync {project: 5}` - **THEN** 422 `bad_request` «project — строка», пакет не создан ### Requirement: Ошибка доступа к файлу хранилища Если файл хранилища вложений не читается (`PermissionError`, `IsADirectoryError` или другая ошибка ОС), хаб SHALL обрабатывать его как отсутствующий: при `claim` `ensure_attachment` получает `size = null`, `GET /files/` и скачивание вложения отвечают 404, как для удалённого файла. В лог хаба SHALL писаться предупреждение с путём и ошибкой. `FileNotFoundError` в лог не пишется. #### Scenario: Нет прав на файл - **WHEN** у файла `files/` сняты права на чтение, агент берёт `ensure_attachment` с этим `file` - **THEN** `claim` отвечает 200, у команды `size = null`; в логе — предупреждение с путём ### Requirement: Пустой SDLC_HUB_HOME Переменная `SDLC_HUB_HOME`, заданная пустой строкой, SHALL трактоваться как незаданная: каталог данных — `~/.local/share/sdlc-hub/`, проверка прежнего каталога выполняется. Хаб MUST NOT писать данные в рабочий каталог процесса из-за пустой переменной. #### Scenario: Пустая переменная - **WHEN** `SDLC_HUB_HOME= sdlc-hub tokens` - **THEN** база открыта из `~/.local/share/sdlc-hub/hub.sqlite`; в рабочем каталоге не появился `hub.sqlite` ### Requirement: Восстановление прерванной публикации агента Перед публикацией версии агента модуля хаб SHALL приводить `release/<модуль>/` к опубликованному состоянию по `manifest.json`. Если `agent.py.prev` есть, а `agent.py` нет или не совпадает по sha256 с манифестом, тогда как `agent.py.prev` совпадает, `agent.py.prev` SHALL возвращаться на место `agent.py`. Если `agent.py` совпадает с манифестом, оставшийся `agent.py.prev` удаляется. В других сочетаниях файлы не трогаются, в лог пишется предупреждение. Годный `agent.py.prev`, совпадающий с манифестом, MUST NOT перезаписываться другим содержимым. #### Scenario: Сбой между переименованием и записью - **WHEN** опубликована v5, прошлая публикация упала после переименования: `agent.py` нет, `agent.py.prev` — v5, манифест — v5; новая публикация v6 снова падает при записи - **THEN** в `release/jira/` — `agent.py` v5 и манифест v5, опубликованной остаётся v5 #### Scenario: Сбой поверх неудачного возврата - **WHEN** `agent.py` — недописанный v6, `agent.py.prev` — v5, манифест — v5; запущена публикация v6 - **THEN** перед записью `agent.py` восстановлен из `.prev` (v5); при сбое записи опубликованной остаётся v5, а `.prev` не затёрт недописанным файлом