75 KiB
hub-api Specification
Purpose
HTTP API хаба sdlc-hub serve: маршруты агента (выдача команд, отчёты, загрузка состояния), маршруты интерфейсов (зеркало, заметки, пакеты, настройки), резервные копии, автообновление зеркала и реле общей папки для агента за VPN.
Requirements
Requirement: Процесс и доступ
sdlc-hub serve SHALL запускать один процесс: HTTP на 0.0.0.0:<port> (по умолчанию 8790), база и настройки в
~/.local/share/sdlc-hub/ (переопределяется SDLC_HUB_HOME). Все маршруты API SHALL требовать
Authorization: Bearer <token>: токен агента модуля открывает только /agent/* (маршруты ядра — от имени этого агента,
маршруты модуля со scope agent — только агенту своего модуля, см. modules, «Агенты модулей»), клиентский — всё
остальное. sdlc-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, запущен
sdlc-hub tokens - THEN вывод содержит клиентский токен,
jira: Tи новый токенwiki
Requirement: Маршруты агента
Хаб SHALL предоставлять агенту следующие маршруты (все под агентским токеном):
POST /agent/claim{agent_version, settings_version, last_jira_call_at, update_state, launcher_version}— долгий запрос до 30 с; ответ{command: {id, type, params, pause_seconds}, settings_version, agent_release}или{command: null, settings_version, agent_release}, гдеagent_release—{version, sha256}опубликованной версии агента илиnull; обновляетagent_status(last_jira_call_at = nullне затирает прежнее значение).update_state(объект илиnull,errorхранится до 500 символов) иlauncher_version(целое илиnull) необязательны; хаб SHALL хранить последние значения в памяти процесса, значения неверного типа игнорировать, не отвергаяclaim. Если есть команда вclaimed,claimSHALL выдать её снова (повторная выдача, см.commands), а не следующую из очереди.GET /agent/settings— настройки для агента (без токенов).GET /agent/commands/{id}— команда по id (для восстановления из журнала), включаяpause_seconds.POST /agent/commands/{id}/result{status, result, error, jira_calls, rate_limited}— идемпотентно: повторный отчёт по команде в терминальном статусе отвечает 200 без изменений, кроме позднего отчётаdoneсresult.keyпоensure_issueвcancelledилиfailedбез результата (результат записывается, статус не меняется, заметкаnew:Nпереносится на ключ). Отчёт по команде вqueuedSHALL отвергаться 409not_claimed.errorхаб SHALL обрезать до 1500 символов.POST /agent/state{command_id, issues: [...], links: [...], neighbor_keys: [...], page, last_page}— загрузка состояния пачками;neighbor_keys— ключи вне базовой JQL, дочитанные по связям; хаб обновляет зеркало иsync_run. Каждая пачка применяется независимо;pageиlast_pageнеобязательны и хабом не используются. Поле задачиcommentsнеобязательно; если оно есть, хаб SHALL до записи проверить его форму: список объектов,id— непустая строка,body— строка,author,created,updated— строка илиnull; нарушение — 422bad_stateс ключом задачи и именем поля, пачка не применяется. Поле задачиattachments(агент v14) необязательно и проверяется так же: список объектов,id— непустая строка,filename— непустая строка,size— неотрицательное целое илиnull,mime_type,author,created— строка илиnull.GET /agent/known-keys— ключи зеркала (для определения соседей).GET /agent/release— опубликованная версия агента{version, sha256, content}(content— текстagent.py); нет публикации или файл не сходится с манифестом по sha256 — 404no_release.GET /agent/files/{sha256}— файл вложения из хранилища хаба{file, size, content}(content— base64 содержимого); файла нет — 404no_file.POST /agent/files{content}— файл вложения, скачанный агентом из Jira, в хранилище хаба; ответ{file, size}; проверки и ошибки — как уPOST /files(см. «Хранилище файлов вложений»), кроме пустого файла: от агента он принимается (в Jira бывают пустые вложения).
Scenario: Выдача с подстановкой ключа
- WHEN следующая команда ссылается на
new:60, аensure_issueдляnew:60завершилась сAURORA-1590 - THEN
claimотдаёт команду сAURORA-1590в параметрах
Scenario: Повторный отчёт
- WHEN агент дважды прислал
resultдля команды 15 - THEN второй ответ 200, статус и результат не изменились
Scenario: Отчёт по невыданной команде
- WHEN
POST /agent/commands/16/resultсstatus = done, команда 16 вqueued - THEN 409
not_claimed, команда осталасьqueued
Scenario: Длинная ошибка
- WHEN агент прислал
failedсerrorдлиной 5000 символов - THEN в
command.errorпервые 1500 символов
Scenario: Поздний отчёт после предела выдач
- WHEN
ensure_issue {temp_key: new:8, …}переведена хабом вfailed«агент не отчитался после 3 выдач», затем агент прислалdoneсresult.key = AURORA-1630 - THEN команда
failed, вresult—AURORA-1630; заметкаnew:8перенесена наAURORA-1630
Scenario: Публикация в ответе claim
- WHEN опубликована v6, агент v5 отправил
claim, команд нет - THEN ответ
{command: null, settings_version, agent_release: {version: 6, sha256: …}}
Scenario: Состояние обновления от агента
- WHEN агент прислал в
claimupdate_state = {state: rolled_back, from_version: 6, to_version: 7, error: "код 1 через 3 с"}иlauncher_version = 1 - THEN
GET /agentотдаёт этоupdate_state,launcher_version = 1иupdate_hint«откат с v7 на v6: код 1 через 3 с»
Scenario: Скачивание версии
- WHEN агентским токеном
GET /agent/release, опубликована v6 - THEN 200
{version: 6, sha256, content}, sha256 отcontentв UTF-8 равенsha256; клиентским токеном — 401; без публикации — 404no_release
Scenario: Скачивание файла вложения
- WHEN агентским токеном
GET /agent/files/<sha>, файл загружен - THEN 200
{file: <sha>, size, content}, sha256 от декодированногоcontentравен<sha>; клиентским токеном — 401; файла нет — 404no_file
Scenario: Неверный комментарий в пачке
- WHEN
POST /agent/stateс задачами X и Y, у Y комментарий{id: "7", body: 42} - THEN 422
bad_stateс ключом Y и полемbody; ни X, ни Y в зеркале не изменились
Requirement: Маршруты интерфейсов
Хаб SHALL предоставлять интерфейсам (MCP, веб-приложение) следующие маршруты под клиентским токеном:
GET /issuesс фильтрамиproject(разрез по проекту, см.data-model, «Проекты»: свои и соседи,neighborотносительно проекта; неизвестный проект — 404),sprint,assignee,work_kind,status,label,epic,neighbor,q(подстрока темы),hide_closed(1/true— без задач, чейstatusвходит в настройкуclosed_statuses; задача без статуса не закрыта),limit/offset— строки зеркала (все поляissue) сwork_kindиз заметок; параметрfact_statusхаб игнорирует, как любой неизвестный; сокращение до карточки — дело интерфейса. Безproject— всё зеркало,neighbor— признак зеркала (вне базовой JQL).GET /issues/{key}— задача целиком: поля,epic_summary(тема эпика из зеркала илиnull), связи в обе стороны (links_out: [{target_key, link_type, summary, status}],links_in: [{source_key, link_type, summary, status}], гдеsummaryиstatus— связанной задачи из зеркала илиnull, если её там нет), заметки, последние 10 команд по ней (безfetch_attachment),comments: [{comment_id, author, created, updated, body, attachment_ids}]— все комментарии, новые сверху (поcreated, затемcomment_id, по убыванию);attachments: [{attachment_id, filename, size, mime_type, author, created, state, error}]— все вложения задачи, новые сверху (поcreated_at, затемattachment_id, по убыванию),stateиerror— см.data-model, «Скачанные вложения» (error— только приfailed, иначеnull);description_attachment_idsиattachment_idsкомментария — id вложений, на которые ссылается текст (см. «Ссылки на вложения в тексте»), в порядке первого упоминания, без повторов.PUT /issues/{key}/note— заметки (частичное обновление,updated_byиз тела); поле вне списка заметок (в том числеfact_status) — 422 «неизвестные поля заметки: …», заметка не меняется.POST /batches{title, author, project?, commands: [...]}→{id, estimate_seconds}; 422 при неизвестном типе, неверных параметрах (в том числе неверного типа значения), временном ключе безensure_issue, двух и болееensure_issueс одинаковойsummary(сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся темы), по проверкам постановки изcommands(типbugбез настройки, файл вложения не загружен, доска дляensure_sprint), а также по проверкам пакета проекта (см.commands), если переданproject.GET /attachments/{id},POST /attachments/{id}/fetch,GET /attachments/{id}/content— вложения задач (см. «Скачивание вложений»).POST /files{content}(content— base64 содержимого файла) →{file, size}, гдеfile— sha256 содержимого (64 hex-символа, нижний регистр); см. «Хранилище файлов вложений».GET /batches?limit=&project=(сproject— только пакеты этого проекта),GET /batches/{id}(с командами иproject),POST /batches/{id}/cancel— отменяетqueuedкоманды пакета; ответ — вид пакета с полемstill_claimed: список{id, type}команд пакета вclaimed(они будут выполнены агентом; остановить — отмена команды сforce).POST /commands/{id}/retry{author?, project?}— новая команда с теми же параметрами в новом пакете «Повтор команды N»; хаб снимает с параметров то, что дописывает сам (board_idуensure_sprint), и проверяет команду заново.projectпередан и у исходного пакета другой проект (или его нет) — 422 «команда #N не из проекта P»; не передан — новый пакет получает проект исходного и проходит его проверки.POST /commands/{id}/cancel— только дляqueued; дляclaimed— 409, если не переданforce: true.POST /refresh{keys | jql | full: true}→ пакет из одногоrefresh;full— по базовой JQL из проектов, без проектов — 422 «нет проектов».GET /agent,GET /sync-runs?limit=,GET /settings,PUT /settings(частично; поднимаетsettings_version; ответ — все настройки после записи, как уGET /settings, включаяsettings_version).
Scenario: Фильтр по спринту
- WHEN
GET /issues?sprint=Спринт 2 - THEN только задачи с таким
sprint, отсортированы по сроку, затем по ключу
Scenario: Список проекта
- WHEN свои задачи
STOCKS_DRRK— AURORA-1 и AURORA-4, сосед — AURORA-2;GET /issues?project=STOCKS_DRRK - THEN три задачи, у AURORA-2
neighbor = true; сneighbor=1— только AURORA-2; задач только с меткойAWPCAнет
Scenario: Неизвестный проект
- WHEN
GET /issues?project=NOPE - THEN 404
Scenario: Пакеты проекта
- WHEN пакет 10 поставлен с
project = STOCKS_DRRK, пакет 11 — без проекта;GET /batches?project=STOCKS_DRRK - THEN в ответе только пакет 10
Scenario: Повтор чужой команды
- WHEN команда 40 из пакета проекта
AWPCA;POST /commands/40/retry {project: "STOCKS_DRRK"} - THEN 422 «команда #40 не из проекта STOCKS_DRRK», пакет не создан
Scenario: Повтор ensure_sprint
- WHEN
ensure_sprint41 failed, в еёparamsboard_id = 4216;POST /commands/41/retry - THEN новый пакет создан,
board_idв новой команде вычислен хабом заново
Scenario: Полный прогон без проектов
- WHEN проектов нет,
POST /refresh {full: true} - THEN 422 «нет проектов», пакет не поставлен
Scenario: Отмена взятой команды
- WHEN
POST /commands/15/cancelбезforce, командаclaimed - THEN 409 с
claimed_at; сforce: true—cancelled, следующий отчёт агента по ней принимается, статус не меняется
Scenario: Поздний отчёт по отменённому созданию
- WHEN
ensure_issue {temp_key: new:7, …}отменена сforce: true, затем агент прислалdoneсresult.key = AURORA-1620 - THEN команда
cancelled, вresult—AURORA-1620; заметкаnew:7перенесена наAURORA-1620
Scenario: Дубль темы в пакете
- WHEN
POST /batchesс двумяensure_issue, у обеихsummary = "Шахматка: комментарии" - THEN 422 с темой «Шахматка: комментарии», пакет не создан
Scenario: Тема не строка
- WHEN
POST /batchesсensure_issue {temp_key: new:1, summary: 42, …} - THEN 422 с указанием параметра
summary, пакет не создан
Scenario: Отмена пакета с командой у агента
- WHEN в пакете 9 команда 30
claimed, команды 31 и 32queued;POST /batches/9/cancel - THEN 31 и 32
cancelled; в ответеstill_claimed = [{id: 30, type: …}]; 30 остаётсяclaimed
Scenario: Ответ на запись настроек
- WHEN
PUT /settings {relay_dir: "/Volumes/externalSSD/utm/rshb/jira-bridge"} - THEN 200; ответ содержит
relay_dirс записанным значением, новуюsettings_versionи остальные настройки; токенов в ответе нет
Scenario: Карточка с комментариями и связями
- WHEN у AURORA-9001 два комментария (14.10 и 15.10), исходящая связь на AURORA-9002 («API свода», «Новая») и на AURORA-1, которой нет в зеркале; эпик AURORA-9100 «Тестовый эпик»
- THEN
GET /issues/AURORA-9001отдаётcommentsс комментарием от 15.10 первым, у связи на AURORA-9002 —summary = "API свода",status = "Новая", у связи на AURORA-1 —summary = null,status = null;epic_summary = "Тестовый эпик"
Scenario: Без закрытых
- WHEN
closed_statuses = ["Готово", "Снято"], в зеркале задачи в статусах «В работе», «Готово», «Снято» и без статуса;GET /issues?hide_closed=1 - THEN в ответе задачи «В работе» и без статуса; без
hide_closed— все четыре
Requirement: Резервные копии
Перед выдачей первой команды пакета, содержащего записи в Jira (любая команда, кроме refresh и fetch_attachment), хаб
SHALL скопировать файл базы в backups/<дата-время>-<batch>.sqlite и хранить последние backup_keep копий.
Scenario: Ротация
- WHEN копий больше
backup_keep - THEN самые старые удаляются до
backup_keep
Scenario: Скачивание без копии
- WHEN выдаётся
fetch_attachmentпакета «Скачать вложение …» - THEN копия базы не создаётся
Requirement: Автообновление зеркала
Если auto_refresh_minutes > 0, агент на связи и есть хотя бы один проект, хаб SHALL поставить пакет «Автообновление»
из refresh {jql: <базовая JQL из проектов>}, когда в очереди нет команд в queued и claimed и последний sync_run
начат раньше, чем auto_refresh_minutes назад (или sync_run ещё нет).
Scenario: Очередь занята
- WHEN интервал прошёл, но в очереди есть
queuedкоманды - THEN автообновление не ставится до опустошения очереди
Scenario: Есть выданная команда без отчёта
- WHEN интервал прошёл,
queuedнет, одна команда вclaimed - THEN автообновление не ставится; команда довыдаётся при следующем
claim
Scenario: Недавнее обновление
- WHEN очередь пуста,
auto_refresh_minutes = 60, последнийsync_runначат 20 минут назад - THEN автообновление не ставится
Scenario: Два проекта
- WHEN интервал прошёл, очередь пуста, проекты
AWPCAиSTOCKS_DRRK - THEN поставлен один
refreshс JQLproject = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")
Requirement: Реле через общую папку
sdlc-hub serve SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка relay_dir пуста (null), реле
бездействует; настройку оно SHALL перечитывать не реже раза в 5 с, включение и выключение не требуют перезапуска. Если
каталог relay_dir существует, реле SHALL создать в нём подкаталоги req/, resp/, work/; если не существует —
записать одно предупреждение в лог и проверять снова.
Обмен:
- запрос агента — файл
req/<id>.json:{id, method, path, body, authorization, deadline}, гдеdeadline— момент UTC (ISO 8601), после которого ответ не нужен; файлы, имя которых начинается с точки, реле MUST NOT читать; - реле SHALL брать запрос переименованием в
work/<id>.jsonи обрабатывать каждый взятый запрос в отдельном потоке; - запрос с истёкшим
deadlineSHALL выбрасываться без ответа;POST /agent/claimSHALL пересылаться, только если доdeadlineосталось не меньше 35 с, иначе выбрасываться без ответа; - остальные запросы реле SHALL пересылать в свой HTTP на
<host>:<port>процессаserve(127.0.0.1, еслиserveслушает0.0.0.0) тем же методом, путём и телом, с заголовкомAuthorizationиз файла, и записывать ответresp/<id>.json={id, status, body}(тело — строка ответа HTTP) через временный файл с именем на точку и переименование; затем удалятьwork/<id>.json; - если соединение со своим HTTP не удалось или ответ нарушает протокол HTTP, ответ SHALL иметь
status = 502и текст ошибки вbody; - нечитаемый JSON запроса SHALL перечитываться до 2 с, затем запрос выбрасывается с записью в лог.
При первой подготовке каждой папки после старта реле (в том числе новой папки после смены relay_dir) реле SHALL
удалить файлы из work/ с записью в лог. Повторная подготовка той же папки (пропал подкаталог, не было доступа) SHALL
только создавать недостающие подкаталоги, не трогая work/. Раз в минуту
реле SHALL удалять ответы в resp/ старше 10 мин, запросы в req/ с deadline старше 60 с и файлы с именем на точку
старше 10 мин. На каждый обработанный запрос реле SHALL писать в лог хаба строку relay <METHOD> <path> <status> <секунды>. HTTP API, обработчики и проверка токенов при этом не меняются.
Scenario: Запрос через папку
- WHEN в
req/появилсяa1.jsonсGET /agent/settings, агентским токеном иdeadlineчерез 60 с - THEN в
resp/появляетсяa1.jsonсоstatus = 200и телом настроек;work/a1.jsonудалён
Scenario: Чужой токен через папку
- WHEN в файле запроса
GET /issuesс агентским токеном - THEN ответ со
status = 401, как по HTTP
Scenario: Просроченный claim
- WHEN реле взяло
POST /agent/claim, доdeadlineкоторого осталось 20 с - THEN запрос выброшен без ответа и без обращения к хабу; команда в очереди осталась
queued
Scenario: Долгий claim не задерживает другие запросы
- WHEN реле пересылает
claim, который ждёт команду 30 с, и в это время вreq/появляетсяGET /agent/settings - THEN ответ на
settingsпишется, не дожидаясьclaim
Scenario: Папка не подключена
- WHEN
relay_dirзадан, но каталога нет; через минуту каталог появился - THEN в логе одно предупреждение; после появления каталога реле создаёт подкаталоги и обрабатывает запросы
Scenario: Сбой протокола своего HTTP
- WHEN при пересылке запроса свой HTTP оборвал ответ (
http.client.HTTPException) - THEN в
resp/ответ соstatus = 502и текстом ошибки; реле продолжает работу
Scenario: Повторная подготовка не трогает взятые запросы
- WHEN реле работает, в
work/лежит взятый запрос, и пропал подкаталогresp/ - THEN реле создаёт
resp/заново; файл вwork/не удалён, в логе нет «удалён незавершённый запрос»
Requirement: Снимок доски
Хаб SHALL предоставлять GET /board?project= под клиентским токеном — согласованный снимок зеркала для доски связей,
прочитанный в одной транзакции:
generated_at— момент снимка (UTC);last_sync— последний завершённый полный прогон (refreshпо текущей базовой JQL из проектов):{started_at, finished_at, issues_count}илиnull, если такого не было; общий для всех проектов;issues— сproject— свои задачи проекта и его соседи (см.data-model, «Проекты»),neighborотносительно проекта; безproject— все задачи зеркала, включая эпики и соседей,neighbor— признак зеркала. Поля:key,summary,issue_type,status,priority,assignee,sprint,duedate,planned_start,planned_end,epic_key,labels(список),neighbor(bool),work_kind(из заметки,nullесли заметки нет); порядок — по ключу;links— сproject— связи, у которых хотя бы один конец — своя задача проекта; безproject— все связи зеркала:{source, target, link_type},link_type— имя типа в Jira, направление как вissue_link(sourceблокирует/ссылается наtarget). Неизвестный проект — 404. Перевод в модель доски (вид работы по меткам, имена эпиков, логические типы связей) — дело интерфейса; хаб отдаёт данные зеркала без преобразований.
Scenario: Снимок с задачами и связью
- WHEN в зеркале эпик AURORA-1351, задачи AURORA-1620 (метка
kind-dev, эпик AURORA-1351) и AURORA-1621, связьAURORA-1620 → AURORA-1621типаASUPR_Блокер_Начала, заметкаwork_kind = "разработка"у AURORA-1620 - THEN
GET /boardотдаёт три задачи по ключу, у AURORA-1620labels = ["kind-dev"],epic_key = "AURORA-1351",work_kind = "разработка"; вlinks—{source: "AURORA-1620", target: "AURORA-1621", link_type: "ASUPR_Блокер_Начала"}
Scenario: Снимок проекта
- WHEN AURORA-1 (метка
STOCKS_DRRK) связана с AURORA-2 (меткаAWPCA), AURORA-2 связана с AURORA-3 (меткаAWPCA);GET /board?project=STOCKS_DRRK - THEN
issues— AURORA-1 (neighbor = false) и AURORA-2 (neighbor = true);links— только AURORA-1 ↔ AURORA-2
Scenario: Последний полный прогон
- WHEN в журнале синхронизаций полный прогон, завершённый в 10:00, и после него обновление по ключам в 11:00
- THEN
last_sync.finished_at— 10:00
Scenario: Без токена
- WHEN
GET /boardбез токена или с агентским токеном - THEN 401
Requirement: Публикация версии агента
sdlc-hub agent publish [--module <модуль>] PATH SHALL публиковать файл агента модуля (по умолчанию jira;
неизвестный модуль или модуль без команд — код 2 и список модулей с агентами в stderr): текст в UTF-8 не больше 1 МБ,
ровно одна строка AGENT_VERSION = <целое>, версия больше опубликованной для этого модуля. Файл SHALL записываться в
<SDLC_HUB_HOME>/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,
sdlc-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
sdlc-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
sdlc-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.jsonversion = 6.9(илиtrue, илиsha256короче 64 символов), хаб получает 100claim - THEN в
claimagent_release = null,GET /agent/release— 404; в логе одно предупреждение о манифесте
Scenario: Агент без запускателя
- WHEN опубликована v6, агент v5 онлайн, в последнем
claimlauncher_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.pyv14 и манифестом безrelease/jira/запущен на новом коде - THEN публикация v14 лежит в
release/jira/,claimагентаjiraполучаетagent_release {version: 14, …}
Requirement: Раздача веб-приложения
Хаб SHALL отдавать собранное веб-приложение (спека web-app) из каталога пакета src/sdlc_hub/web/app/, читая файлы
с диска на каждый запрос:
GET /app/assets/<путь>— файл изassets/сборки; путь, который после разрешения выходит заassets/, не существует или содержит нулевой байт, — 404;Content-Typeпо расширению (.js—text/javascript; charset=utf-8,.css—text/css; charset=utf-8,.svg—image/svg+xml,.png—image/png,.ico—image/x-icon,.woff2—font/woff2,.map—application/json, прочее —application/octet-stream);Cache-Control: public, max-age=31536000, immutable;- любой другой
GET /appилиGET /app/…—index.htmlсборки,text/html; charset=utf-8,Cache-Control: no-cache; - если
index.htmlсборки нет — на любойGET /app…503text/html; charset=utf-8с текстом, что приложение не собрано и нужно выполнитьmake webв каталоге sdlc-hub; GET /иGET /links— 302 сLocation: /app/(главная страница приложения — «Обзор»). Сборка приложения не хранится в git; её создаётmake web.
Scenario: Файл сборки
- WHEN в сборке есть
assets/index-abc123.js, запрошенGET /app/assets/index-abc123.js - THEN 200,
Content-Type: text/javascript; charset=utf-8,Cache-Controlсimmutable, тело — содержимое файла
Scenario: Путь приложения
- WHEN запрошен
GET /app/issues/AURORA-1620 - THEN 200, тело —
index.htmlсборки,Cache-Control: no-cache
Scenario: Выход за каталог сборки
- WHEN запрошен
GET /app/assets/../../api.py(в том числе в виде%2e%2e) - THEN 404, содержимое файлов вне
assets/не отдаётся
Scenario: Приложение не собрано
- WHEN каталога сборки нет, запрошен
GET /app/board - THEN 503, тело — HTML с подсказкой
make web
Scenario: Старый адрес доски
- WHEN запрошен
GET /linksилиGET / - THEN 302,
Location: /app/
Scenario: Нулевой байт в пути
- WHEN запрошен
GET /app/assets/%00илиGET /app/assets/a%00.js - THEN 404
not_found, а не 500
Requirement: Хранилище файлов вложений
Хаб SHALL хранить файлы вложений в каталоге files/ внутри каталога данных, по одному файлу на содержимое с именем
sha256 (без расширения и имени вложения: имя задаёт команда). Каталог SHALL иметь права 0700 (выставляются при создании
и при каждом старте, как у backups/). POST /files:
contentне строка или не base64 — 400bad_request; пустой файл — 422empty_file;- файл больше 10 МБ (10 485 760 байт) после декодирования — 413
too_large(тело запроса до 16 МБ проходит); - запись атомарная: временный файл в
files/, затем переименование; повторная загрузка того же содержимого отвечает тем жеfileи не создаёт второй копии. Файлы в бэкапы базы MUST NOT входить. Фоновыйtickхаба SHALL не чаще раза в сутки в пределах работы процесса (после старта — на первом тике) удалять файлы старше 30 дней (по времени изменения), на которые не ссылается ни одна командаensure_attachmentвqueuedилиclaimed(скачанные вложенияattachment_fileот уборки не защищены: их можно скачать снова), и подкаталогиdownloads/<attachment_id>/(копии MCP, см.mcp) старше 30 дней по времени изменения; повтор команды после уборки отвергается при постановке 422 «файл не загружен: POST /files». Повторная загрузка того же содержимого SHALL обновлять время изменения файла; выдача файлаGET /attachments/{id}/content— тоже.
Scenario: Загрузка файла
- WHEN
POST /files {content: base64("abc")} - THEN 200
{file: "ba7816bf…", size: 3}; вfiles/файлba7816bf…с содержимымabc
Scenario: Повторная загрузка
- WHEN тот же
POST /filesвторой раз - THEN 200 с тем же
file; вfiles/по-прежнему один файл
Scenario: Слишком большой файл
- WHEN
POST /filesс файлом 11 МБ - THEN 413
too_large, вfiles/ничего не появилось
Scenario: Уборка старых файлов
- WHEN в
files/файл A старше 30 дней без ссылок, файл B старше 30 дней, на него ссылаетсяensure_attachmentвqueued, файл C загружен вчера - THEN после уборки удалён только A
Scenario: Скачанное вложение и уборка
- WHEN в
files/файл D скачан агентом (attachment_file) 40 дней назад и с тех пор не выдавался; вdownloads/каталог31337/создан 40 дней назад - THEN после уборки D и
downloads/31337/удалены;stateвложения —none
Requirement: Маршруты проектов
Хаб SHALL предоставлять под клиентским токеном:
GET /projects→ список проектов поcreated_at, затемlabel; каждый — плоский объект из полей ядра (label,name,created_at) и полей всех модулей (modules, «Поля проекта от модулей»). Модуль jira даётboard_id,board_type,issues_count(задачи зеркала с меткой проекта),jira_synced_at(время завершения последнего полного обновления зеркала, общее для всех проектов;null, если его не было), модуль wiki —wiki_root,wiki_root_title,wiki_pages,wiki_synced_at,wiki_sync_error.POST /projects{label, name, author?, <поля модулей>}→ 201{project, batch_id, batches}: в одной транзакции проект записывается, модули записывают свои поля и ставят свои пакеты;batches— список{module, id}всех поставленных пакетов в порядке модулей;batch_id— пакет первого модуля, вернувшего пакет (у jira — «Новый проект <метка>» из одногоrefreshпо базовой JQL, уже включающей новую метку). Автор пакетов —author(по умолчаниюapi). Неизвестное поле, неверные поля ядра (см.data-model, «Проекты») или модуля — 422 с указанием поля; метка, совпадающая с существующей без учёта регистра, — 409. При ошибке ни проекта, ни пакетов нет.PATCH /projects/{label}{<изменяемые поля модулей>, author?}→ 200{project, batch_ids, batches}: модули правят свои поля и ставят пакеты в одной транзакции;batches— список{module, id}. Неизменяемое поле (метка, название,board_id) — 422 «<поле> после создания не меняется»; неизвестное поле или неверное значение — 422 с указанием поля; проекта нет — 404.
Scenario: Новый проект
- WHEN
POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120, author: "viewer"}, есть проектAWPCA - THEN 201;
GET /projectsотдаёт два проекта, уSTOCKS_DRRKboard_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;batches—[{module: "jira", id: <refresh>}, {module: "wiki", id: <wiki_sync>}]
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,batches—[{module: "wiki", id: <он же>}];GET /projectsотдаётAWPCAсwiki_root = DOC-000777
Scenario: Доску не меняют
- WHEN
PATCH /projects/AWPCA {board_id: 1} - THEN 422 «board_id после создания не меняется», доска прежняя
Scenario: Свежесть Jira в проекте
- WHEN последний полный пакет
refreshзавершён в 11:07,GET /projects - THEN у каждого проекта
jira_synced_at— это время; до первого полного обновления —null
Requirement: Приём журнала изменений
Хаб SHALL принимать от агента журнал изменений задач маршрутом POST /agent/history (агентский токен) с телом
{command_id, changes: [...]}, элемент — {history_id, seq, key, created, author, field_id, field_name, from, from_string, to, to_string}. До записи хаб SHALL проверить форму всей пачки: changes — список объектов;
history_id, key, created, field_name — непустые строки; seq — целое ≥ 0; author, field_id, from,
from_string, to, to_string — строка или null. Нарушение — 422 bad_history с номером элемента и именем поля,
пачка не применяется. Принятая пачка записывается в issue_change в одной транзакции (см. data-model, «Журнал
изменений задач»); ответ — {accepted, duplicates} (записано новых строк, отброшено повторов). Задача элемента может
отсутствовать в зеркале — элемент всё равно записывается.
Scenario: Пачка журнала
- WHEN
POST /agent/historyс тремя элементами, один из них уже есть вissue_change - THEN 200
{accepted: 2, duplicates: 1}
Scenario: Неверный элемент
- WHEN
POST /agent/historyс двумя элементами, у второгоseq = "0" - THEN 422
bad_historyс номером элемента 1 и полемseq; ни один элемент не записан
Scenario: Клиентский токен
- WHEN
POST /agent/historyклиентским токеном - THEN 401
Requirement: Лента изменений
Хаб SHALL отдавать интерфейсам (клиентский токен) ленту событий за период маршрутом GET /changes с параметрами:
since,until— ISO-8601 со смещением илиZ(время без смещения — 422); безuntil— текущее время; безsince—untilминус 24 часа;project— только свои задачи проекта (метка проекта; соседи не входят); неизвестный проект — 404;key— события одной задачи (без ограничения проектом);kinds— через запятую изstatus,description,dates,comment,other, илиall; по умолчанию —status,description,dates,comment;limit— по умолчанию 500, не больше 2000.
События: строки issue_change с at в [since, until] — вид status (поле status), description (поле
description), dates (поля duedate, planned_start, planned_end), other (прочие поля); и комментарии
issue_comment с created_at в [since, until] — вид comment. Ответ — {since, until, truncated, changes: [...]},
элемент — {at, key, summary, status, kind, field, author, from, to, from_value, to_value}: summary и status —
текущие из зеркала (null, если задачи в зеркале нет); from/to — from_string/to_string строки журнала, а если
строка null — from_value/to_value; from_value/to_value — сырые значения строки журнала; у комментария
field = "comment", from = null, to — текст комментария, from_value = to_value = null. Порядок — по at
по убыванию, затем key, history_id, seq (комментарии — comment_id). Событий больше limit — отдаются первые
limit по этому порядку и truncated = true. Неверное время, since позже until, неизвестный вид, limit вне
1..2000 — 422 bad_request с именем параметра.
Scenario: Что закрылось за два часа
- WHEN в 12:00 UTC
GET /changes?project=AWPCA&since=2026-10-22T10:00:00Z&kinds=status; в журнале — X (меткаAWPCA) «В работе» → «Готово» в 11:15 UTC и Y (меткаSTOCKS_DRRK) в 11:20 UTC - THEN одно событие: X,
kind = status,from = "В работе",to = "Готово", автор иatиз журнала
Scenario: Комментарий в ленте
- WHEN у X комментарий с
created = "2026-10-22T13:30:00.000+0300"(10:30 UTC), запрос сsince10:00 UTC - THEN в ленте событие
kind = comment,at = "2026-10-22T10:30:00+00:00",to— текст комментария
Scenario: Прочие поля по запросу
- WHEN в журнале X сменился исполнитель; запрос без
kinds, затем сkinds=all - THEN в первом ответе события нет; во втором —
kind = other,field = "assignee"
Scenario: Сосед проекта не в ленте
- WHEN Z без метки
AWPCAсвязана с задачейAWPCA; в журнале Z сменился статус;GET /changes?project=AWPCA - THEN события Z нет;
GET /changes?key=Zего отдаёт
Scenario: Неверный период
- WHEN
GET /changes?since=вчера - THEN 422
bad_requestс параметромsince
Requirement: Журнал в журнале синхронизаций
GET /sync-runs SHALL отдавать у каждого прогона также history_at (null — журнал по прогону не принят).
Scenario: Прогон с журналом
- WHEN полный прогон завершился
doneс принятым журналом;GET /sync-runs - THEN у прогона
history_at— время приёма; у хвостовогоrefresh {keys}—null
Requirement: История задачи в карточке
GET /issues/{key} SHALL отдавать также changes — все строки issue_change задачи (все виды, включая other) в
формате элемента ленты без summary и status, по at по убыванию (строки с at = NULL — в конце); журнала нет —
changes = [].
Scenario: Задача с историей
- WHEN у X в журнале смена статуса вчера и правка описания сегодня;
GET /issues/X - THEN
changesиз двух элементов: первым — правка описания (kind = description), вторым — смена статуса
Requirement: Скачивание вложений
Хаб SHALL предоставлять интерфейсам под клиентским токеном:
GET /attachments/{id}— вложение из зеркала{attachment_id, key, filename, size, mime_type, author, created, state, error, command_id, batch_id}, гдеcommand_id/batch_id— последнейfetch_attachmentпо нему (илиnull); нет в зеркале — 404no_attachment.POST /attachments/{id}/fetch{author?}— запросить скачивание. Нет в зеркале — 404no_attachment;state = readyилиfetching— ответ как уGET /attachments/{id}, новой команды нет; размер больше 10 МБ — 413too_large; последний известный хабу агент (agent_status.agent_version) старше v14 — 409agent_too_old«агент v не умеет скачивать вложения — нужен v14» (версия неизвестна — проверка не делается); иначе хаб SHALL поставить пакет из однойfetch_attachment(см.commands, автор — из тела, иначеapi) и ответить какGET /attachments/{id}сstate = fetching. Проверка «уже ставится» и постановка SHALL выполняться в одной транзакции записи: два одновременных запроса ставят одну команду.GET /attachments/{id}/content— содержимое приstate = ready: 200, тело — байты файла,Content-Type—mime_typeвложения (нет или не видатип/подтип—application/octet-stream),Content-Disposition: attachment; filename*=UTF-8''<имя в URL-кодировке>,X-Content-Type-Options: nosniff,Content-Length; иначе 404not_fetched. Отчётdoneпоfetch_attachment(POST /agent/commands/{id}/result) SHALL записыватьattachment_fileизresult.fileиresult.size, если файлfileесть в хранилище и его размер равенsize; иначе команда SHALL становитьсяfailed«хаб: файл не найден в хранилище» иattachment_fileне меняется.
Scenario: Первый запрос
- WHEN вложение #31337 в зеркале,
state = none, агент v14;POST /attachments/31337/fetch {author: "web"} - THEN создан пакет «Скачать вложение скрин 1.png (AURORA-1650)» из одной
fetch_attachment, без хвостовогоrefresh; ответstate = fetchingсcommand_idиbatch_id
Scenario: Повторный запрос во время скачивания
- WHEN по #31337 команда в
queued; MCP и веб одновременно вызываютPOST /attachments/31337/fetch - THEN оба ответа —
state = fetchingс той жеcommand_id; новых пакетов нет
Scenario: Файл уже у хаба
- WHEN #31337 в
state = ready;GET /attachments/31337/content - THEN 200,
Content-Type: image/png,Content-Disposition: attachment; filename*=UTF-8''%D1%81%D0%BA%D1%80%D0%B8%D0%BD%201.png, тело — файл; время изменения файла вfiles/обновлено
Scenario: Старый агент
- WHEN
agent_status.agent_version = 13;POST /attachments/31337/fetch - THEN 409
agent_too_old«агент v13 не умеет скачивать вложения — нужен v14», пакета нет
Scenario: Отчёт без файла в хранилище
- WHEN агент отчитался
done {file: <sha>, size: 100}, а файла<sha>вfiles/нет - THEN команда
failed«хаб: файл не найден в хранилище»;stateвложения —failed
Requirement: Ссылки на вложения в тексте
Хаб SHALL находить в описании задачи и в тексте каждого комментария ссылки на вложения задачи по вики-разметке Jira:
!<имя>! и !<имя>|<параметры>! (картинка; имя без !, | и переводов строк, не содержит ://), [^<имя>] и
[<текст>|^<имя>] (ссылка на вложение; имя без ] и переводов строк). Имя SHALL сравниваться с filename вложений
задачи точно (с учётом регистра); несколько вложений с одним именем — ссылка указывает на самое новое по created_at
(как в Jira); имени нет среди вложений — ссылка не учитывается. Ссылки внутри {code}/{noformat} тоже учитываются.
Scenario: Скриншот в комментарии
- WHEN у задачи вложения «скрин 1.png» (#31337) и «ОСВ 60.xlsx» (#31338); комментарий 101 — «см. !скрин 1.png|thumbnail! и [^ОСВ 60.xlsx], ещё раз !скрин 1.png!»; описание ссылается на «старый.png», которого нет
- THEN у комментария 101
attachment_ids = ["31337", "31338"];description_attachment_ids = []
Scenario: Два вложения с одним именем
- WHEN у задачи два вложения «скрин.png» (#10 от 14.10, #20 от 20.10); комментарий — «!скрин.png!»
- THEN у комментария
attachment_ids = ["20"]
Requirement: Каталог данных прежнего имени
Если SDLC_HUB_HOME не задан, ~/.local/share/sdlc-hub/ не существует, а ~/.local/share/jira-hub/ существует, любая
команда sdlc-hub, которой нужен каталог данных (serve, mcp, tokens, init, import, agent publish), SHALL
завершиться с кодом 2 и текстом в stderr: каталог данных лежит под прежним именем, перенести его —
scripts/cutover.sh (или mv ~/.local/share/jira-hub ~/.local/share/sdlc-hub при остановленном хабе). Команда MUST NOT
создавать новый каталог, переносить или менять прежний. Если существуют оба каталога, используется новый, в stderr —
предупреждение, что прежний можно удалить после проверки.
Scenario: Код обновлён, каталог не перенесён
- WHEN есть только
~/.local/share/jira-hub/, запущенsdlc-hub serve - THEN код выхода 2, в stderr — подсказка о переносе;
~/.local/share/sdlc-hub/не создан, прежний каталог не тронут
Scenario: Задан SDLC_HUB_HOME
- WHEN
SDLC_HUB_HOME=/tmp/x, есть~/.local/share/jira-hub/ - THEN команда работает с
/tmp/x, проверка прежнего каталога не выполняется
Requirement: Переключение живого хаба
scripts/cutover.sh SHALL переводить живую установку Mac mini со старого имени на новое. Без --apply скрипт SHALL
только печатать шаги и найденные проблемы, ничего не меняя. С --apply — по порядку, с остановкой на первой ошибке:
- проверки: основной checkout
jira-bridge/jira-hubчистый, наmainи совпадает сorigin/main(владелец сделалgit pull --ff-onlyпрямо перед запуском: скрипт приходит этим pull; неотслеживаемая прежняя сборкаsrc/jira_hub/без tracked-файлов не считается грязью); в Gitea доступенaep/sdlc-hub(владелец переименовал репозиторий; адрес выводится изoriginзаменой/aep/jira-hubна/aep/sdlc-hub, протокол сохраняется,CUTOVER_REMOTEпереопределяет); естьuv,make,node,npm,claude,launchctl;~/.local/share/sdlc-hub/иjira-bridge/sdlc-hubне существуют; - остановить launchd
ru.aepif.jira-hub, дождаться закрытия порта и переименовать его plist в*.pre-sdlc; - перенести каталог данных
~/.local/share/jira-hub→~/.local/share/sdlc-hub; - перенести checkout
jira-bridge/jira-hub→jira-bridge/sdlc-hub, удалить неотслеживаемыйsrc/jira_hub,git worktree repair,origin→aep/sdlc-hub, пересоздать.venv(uv sync),make web; - поставить и запустить launchd
ru.aepif.sdlc-hub(uv run --project <checkout> sdlc-hub serve, лог~/Library/Logs/sdlc-hub.log) и дождаться ответа хаба на порту из настроек; - в проектах-потребителях (по умолчанию
~/projects/awp-ca,~/projects/stocks_drrkи каждый~/projects/*/.mcp.json, где естьjira-hub, напримерsfm_foxpro; сухой прогон печатает полный список): в.mcp.jsonзаменить записиjira-hub→sdlc-jira(sdlc-hub mcp jira) иjira-hub-wiki→sdlc-wiki(sdlc-hub mcp wiki) с новым путём; в.claude/settings*.json— имена серверов (enabledMcpjsonServersи т. п.) и разрешенияmcp__jira-hub__→mcp__sdlc-jira__,mcp__jira-hub-wiki__→mcp__sdlc-wiki__; вCLAUDE.mdи.claude/skills/**— те же имена серверов и инструментов. Прочие файлы проектов (заметки, отчёты) не трогаются. Перед правкой файл копируется рядом с суффиксом.pre-sdlc; коммиты скрипт не делает; - в user scope Claude Code заменить
jira-hubнаsdlc-jira(claude mcp remove/add). Каждый выполненный шаг SHALL печататься; в конце — сводка, что осталось владельцу (/mcpreconnect в открытых сессиях,codegraph initв checkout и worktree, удалить копии.pre-sdlcпосле проверки). Шаги откатываются в обратном порядке по инструкции из README.
Scenario: Сухой прогон
- WHEN
bash scripts/cutover.shбез аргументов - THEN напечатаны шаги и проверки, файловая система, launchd и конфигурации Claude не изменены
Scenario: Репозиторий в Gitea не переименован
- WHEN
bash scripts/cutover.sh --apply,aep/sdlc-hubв Gitea не найден - THEN скрипт останавливается на шаге 1 с подсказкой, launchd и каталоги не тронуты
Requirement: Модуль пакета
GET /batches и GET /batches/{id} SHALL отдавать у пакета поле module — имя модуля, которому принадлежит тип первой
команды пакета (по описаниям модулей в реестре); пакет без команд или с типом, которого не знает ни один модуль, —
module = null. Фильтр GET /batches?module=<имя> SHALL отдавать только пакеты модуля; неизвестный модуль — 404.
Scenario: Модуль у пакетов
- WHEN в очереди пакет «Автообновление» (
refresh, авторhub) и пакетwiki_syncпроектаAWPCA;GET /batches - THEN у первого
module = "jira", у второгоmodule = "wiki"
Scenario: Фильтр по модулю
- WHEN
GET /batches?module=wiki&project=AWPCA - THEN только пакеты модуля wiki проекта
AWPCA
Scenario: Неизвестный модуль
- WHEN
GET /batches?module=nope - THEN 404
Requirement: Приём досок и спринтов
POST /agent/state (агент v15) SHALL принимать необязательные поля:
- у задачи —
rank(строка илиnull),story_points(число илиnull),sprint_ids(список целых); присланныеrankиstory_pointsзаписываются вissue; присланныйsprint_idsзаменяет все строкиissue_sprintзадачи; задача без поля свои значения сохраняет; - в пачке —
boards: список{id, type, columns, kanban_backlog, sprints};columns— список{name, statuses}илиnull,kanban_backlog—true/false/null,sprints— список{id, name, state, start_date, end_date, complete_date, goal}. Доска изboardsSHALL заменять свою строкуjira_boardи все строкиjira_sprintэтой доски; доски, которых нет вboards, не меняются. Форму хаб SHALL проверять до записи, какcomments: нарушение — 422bad_stateс ключом задачи или номером доски и именем поля, пачка не применяется.stateспринта внеactive/future/closed,sprint_idsне список целых (в том числеnull) — нарушение;rank: nullиstory_points: null— очистить поле. Тип доски изboards(scrum/kanban) хаб SHALL записывать и вjira_project.board_typeпроектов этой доски;nullтип не меняет. Пачка может быть пустой (issues: []) и нести толькоboards.
Scenario: Пачка с досками
- WHEN пачка
refreshнесётboards: [{id: 4216, type: "scrum", columns: […], kanban_backlog: null, sprints: [101 closed, 102 active, 103 future]}], раньше у доски были спринты 101 и 102 - THEN у доски 4216 строки
jira_sprint101, 102, 103 с присланными полями; колонки заменены
Scenario: Агент v14
- WHEN пачка без
boards, задачи безrank,story_points,sprint_ids - THEN данные досок и эти поля задач не меняются
Requirement: Маршрут планирования
GET /agile?project=<метка> (клиентский токен) SHALL отдавать для проекта (неизвестный — 404; без project — 422):
{board, sprints, backlog, board_issues}.
board—{id, type, columns, kanban_backlog, fetched_at}доски проекта; доски нет —null(остальные поля —[]/null); доска есть, а её данных от агента нет —{id, type, columns: null, kanban_backlog: null, fetched_at: null}.sprints— все спринты доски:{id, name, state, start_date, end_date, complete_date, goal, count, count_done, unestimated, points, points_done, issues};issues— свои задачи проекта в этом спринте (поissue_sprint);count/count_done— их число и число выполненных,points/points_done— суммаstory_pointsвсех и выполненных,unestimated— число без оценки. Порядок:activeпоstart_date,futureпо id,closedпоcomplete_date, новые сверху. У kanban-доски —[].backlog— scrum: свои задачи не в активном и не в будущем спринте и не выполненные; kanban: свои задачи в статусах первой колонки, еслиkanban_backlog = true, иначеnull.board_issues— только kanban: свои задачи в статусах остальных колонок (приkanban_backlogнеtrue— и первой) и без колонки; выполненные — только сjira_updatedне старше 14 дней; scrum —null. Строка задачи —{key, summary, issue_type, status, priority, assignee, epic_key, story_points, rank, done}; списки — поrank(строки сравниваются как есть;null— в конце), затем поkey.
Scenario: Спринт и бэклог
- WHEN у
AWPCAактивный спринт 102 с задачами A (3 SP, в последней колонке), B (без оценки), в бэклоге C и выполненная D без спринта - THEN спринт 102:
count = 2,count_done = 1,points = 3,points_done = 3,unestimated = 1;backlog— только C
Scenario: Проект без доски
- WHEN
GET /agile?project=STOCKS_DRRK, у проекта нет доски - THEN 200,
board = null,sprints = [],backlog = null,board_issues = null