## MODIFIED Requirements ### Requirement: Процесс и доступ `jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:` (по умолчанию 8790), база и настройки в `~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты API SHALL требовать `Authorization: Bearer `: токен агента модуля открывает только `/agent/*` (маршруты ядра — от имени этого агента, маршруты модуля со scope `agent` — только агенту своего модуля, см. `modules`, «Агенты модулей»), клиентский — всё остальное. `jira-hub tokens` SHALL печатать клиентский токен и токены агентов с именами модулей. Исключение — раздача веб-приложения (`GET /app`, `GET /app/…`) и перенаправления `GET /`, `GET /links`: это статические файлы без данных, они отдаются без токена. Ответы API — JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или `Content-Length`), 401, 404, 409, 413 (тело больше предела), 422, 500 (внутренняя ошибка); реле общей папки дополнительно отвечает 502. Методы, кроме GET, POST, PUT и PATCH, отвечают 501 стандартной страницей `http.server`, не JSON. #### Scenario: Чужой токен - **WHEN** токеном агента `jira` вызван `GET /issues` - **THEN** 401 #### Scenario: Неподдерживаемый метод - **WHEN** `DELETE /issues/AURORA-1` - **THEN** 501, тело не JSON; база не меняется #### Scenario: Страница без токена - **WHEN** `GET /app/board` без заголовка `Authorization`, сборка приложения есть - **THEN** 200, `Content-Type: text/html; charset=utf-8`, тело — `index.html` сборки; данных зеркала в нём нет #### Scenario: Токены после миграции - **WHEN** база v6 с агентским токеном T мигрировала в v7, запущен `jira-hub tokens` - **THEN** вывод содержит клиентский токен, `jira: T` и новый токен `wiki` ### Requirement: Маршруты проектов Хаб SHALL предоставлять под клиентским токеном: - `GET /projects` → список проектов по `created_at`, затем `label`; каждый — плоский объект из полей ядра (`label`, `name`, `created_at`) и полей всех модулей (`modules`, «Поля проекта от модулей»). Модуль jira даёт `board_id`, `board_type`, `issues_count` (задачи зеркала с меткой проекта), модуль wiki — `wiki_root`, `wiki_root_title`, `wiki_pages`, `wiki_synced_at`, `wiki_sync_error`. - `POST /projects` `{label, name, author?, <поля модулей>}` → 201 `{project, batch_id}`: в одной транзакции проект записывается, модули записывают свои поля и ставят свои пакеты; `batch_id` — пакет первого модуля, вернувшего пакет (у jira — «Новый проект <метка>» из одного `refresh` по базовой JQL, уже включающей новую метку). Автор пакетов — `author` (по умолчанию `api`). Неизвестное поле, неверные поля ядра (см. `data-model`, «Проекты») или модуля — 422 с указанием поля; метка, совпадающая с существующей без учёта регистра, — 409. При ошибке ни проекта, ни пакетов нет. - `PATCH /projects/{label}` `{<изменяемые поля модулей>, author?}` → 200 `{project, batch_ids}`: модули правят свои поля и ставят пакеты в одной транзакции. Неизменяемое поле (метка, название, `board_id`) — 422 «<поле> после создания не меняется»; неизвестное поле или неверное значение — 422 с указанием поля; проекта нет — 404. #### Scenario: Новый проект - **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120, author: "viewer"}`, есть проект `AWPCA` - **THEN** 201; `GET /projects` отдаёт два проекта, у `STOCKS_DRRK` `board_type = null`, `wiki_root = null`; в очереди пакет автора `viewer` с `refresh {jql: 'project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")'}` #### Scenario: Проект без доски - **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК"}` - **THEN** 201, `board_id = null` #### Scenario: Проект с корнем вики - **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234"}` - **THEN** 201; в очереди пакет `refresh` и пакет `wiki_sync {label: "STOCKS_DRRK", root: "DOC-001234"}`; `batch_id` — пакет `refresh` #### Scenario: Повтор метки - **WHEN** `POST /projects {label: "AWPCA", name: "…"}`, проект `AWPCA` есть - **THEN** 409, пакет не поставлен #### Scenario: Без названия - **WHEN** `POST /projects {label: "STOCKS_DRRK", name: " "}` - **THEN** 422 с указанием поля `name` #### Scenario: Неизвестное поле - **WHEN** `POST /projects {label: "X", name: "X", board: 5}` - **THEN** 422 с указанием поля `board`, проекта нет #### Scenario: Корень вики у существующего проекта - **WHEN** `PATCH /projects/AWPCA {wiki_root: "DOC-000777"}` - **THEN** 200, `batch_ids` — один пакет `wiki_sync`; `GET /projects` отдаёт `AWPCA` с `wiki_root = DOC-000777` #### Scenario: Доску не меняют - **WHEN** `PATCH /projects/AWPCA {board_id: 1}` - **THEN** 422 «board_id после создания не меняется», доска прежняя ### Requirement: Публикация версии агента `jira-hub agent publish [--module <модуль>] PATH` SHALL публиковать файл агента модуля (по умолчанию `jira`; неизвестный модуль или модуль без команд — код 2 и список модулей с агентами в stderr): текст в UTF-8 не больше 1 МБ, ровно одна строка `AGENT_VERSION = <целое>`, версия больше опубликованной для этого модуля. Файл SHALL записываться в `/release/<модуль>/agent.py` (каталоги 0700), затем `manifest.json` `{version, sha256, size, published_at}`; оба — через временный файл с `fsync` и переименование, манифест последним. Прежний `agent.py` на время публикации SHALL переименовываться в `agent.py.prev` и удаляться после записи манифеста; если запись нового файла или манифеста не удалась (`OSError`), хаб SHALL вернуть `agent.py.prev` на место `agent.py`, удалить временные файлы и завершить команду сообщением «не удалось записать публикацию: <ошибка>» и кодом 1 — опубликованной остаётся прежняя версия. Команда SHALL работать без запущенного `serve` и печатать «опубликована v, sha256 <первые 12 символов>…, байт»; при ошибке — сообщение и код 1, опубликованное не меняется. При старте хаб SHALL один раз перенести прежнюю публикацию `release/agent.py` и `release/manifest.json` в `release/jira/`, если `release/jira/` ещё нет; ошибка переноса — предупреждение в лог, хаб стартует. Хаб SHALL читать манифест модуля агента при каждом `claim`, `GET /agent/release` и `GET /agent`; манифест годен, только если это объект, `version` — целое число (не логическое) не меньше 1, `sha256` — строка из 64 символов `[0-9a-f]`; негодный или нечитаемый манифест означает «публикации нет» и предупреждение в лог — одно на каждый новый текст ошибки. Файл `agent.py`, не декодируемый как UTF-8 при совпавшем sha256, означает «публикации нет» (`GET /agent/release` — 404 `no_release`). `GET /agent?module=<модуль>` (без параметра — `jira`) и каждый элемент `GET /agents` SHALL дополняться полями `release` (`{version, sha256, published_at}` или `null`), `update_state`, `launcher_version`, `agent_reported` (был ли `claim` этого агента с момента запуска хаба) и `update_hint`: «агент запущен без run.py — обновится только вручную» (опубликована версия выше версии агента, агент онлайн, `agent_reported = true`, `launcher_version = null`), «откат с v на v: » (`rolled_back`), «v отвергнута: » (`rejected`), иначе `null`. #### Scenario: Публикация новой версии - **WHEN** опубликована v5, `jira-hub agent publish jira-agent/src/agent.py` с `AGENT_VERSION = 6` - **THEN** в `release/jira/` — новый `agent.py` и манифест с `version = 6`, `agent.py.prev` нет; вывод «опубликована v6, sha256 …»; код 0 #### Scenario: Публикация eva-agent - **WHEN** `jira-hub agent publish --module wiki eva-agent/src/agent.py` с `AGENT_VERSION = 1`, опубликована jira v14 - **THEN** в `release/wiki/` — v1; `claim` агента `wiki` получает `agent_release {version: 1, …}`, `claim` агента `jira` — `{version: 14, …}` #### Scenario: Неизвестный модуль - **WHEN** `jira-hub agent publish --module nope agent.py` - **THEN** код 2, в stderr «неизвестный модуль nope» и список модулей с агентами; `release/` не изменён #### Scenario: Версия не выше опубликованной - **WHEN** опубликована v6, публикуется файл с `AGENT_VERSION = 6` - **THEN** «опубликована v6, новая должна быть больше», код 1; `release/` не изменён #### Scenario: Нет строки версии - **WHEN** публикуется файл без строки `AGENT_VERSION = <целое>` или с двумя такими строками - **THEN** сообщение об ошибке, код 1; `release/` не изменён #### Scenario: Диск полон при записи манифеста - **WHEN** опубликована v8, публикуется v9, запись `manifest.json` завершилась `OSError` - **THEN** «не удалось записать публикацию: …», код 1, без трейсбека; `agent.py` и манифест — прежние v8, `GET /agent/release` отдаёт v8; временных файлов и `agent.py.prev` в `release/jira/` нет #### Scenario: Негодный манифест - **WHEN** в `manifest.json` `version = 6.9` (или `true`, или `sha256` короче 64 символов), хаб получает 100 `claim` - **THEN** в `claim` `agent_release = null`, `GET /agent/release` — 404; в логе одно предупреждение о манифесте #### Scenario: Агент без запускателя - **WHEN** опубликована v6, агент v5 онлайн, в последнем `claim` `launcher_version = null` - **THEN** `GET /agent` → `agent_reported = true`, `update_hint` = «агент запущен без run.py — обновится только вручную» #### Scenario: Хаб перезапущен, агент ещё не выходил на связь - **WHEN** опубликована v10, агент v9 под запускателем, хаб перезапущен, `claim` после запуска ещё не было - **THEN** `GET /agent` → `agent_reported = false`, `launcher_version = null`, `update_hint = null`; после первого `claim` — `agent_reported = true`, `launcher_version` из `claim` #### Scenario: Перенос прежней публикации - **WHEN** хаб с `release/agent.py` v14 и манифестом без `release/jira/` запущен на новом коде - **THEN** публикация v14 лежит в `release/jira/`, `claim` агента `jira` получает `agent_release {version: 14, …}`