Andrey Epifancev 5ef6c937b5 Спека OpenSpec: несколько проектов-меток в мосте (#61)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:59:33 +04:00

16 KiB
Raw Blame History

ADDED Requirements

Requirement: Маршруты проектов

Хаб SHALL предоставлять под клиентским токеном:

  • GET /projects → список {label, name, board_id, board_type, created_at, issues_count} по created_at, затем label; issues_count — задачи зеркала с меткой проекта.
  • POST /projects {label, name, board_id?, author?} → 201 {project, batch_id}: в одной транзакции проект записывается и ставится пакет «Новый проект <метка>» из одного refresh по базовой JQL, уже включающей новую метку; автор пакета — author (по умолчанию api). Неверные поля (см. data-model, «Проекты») — 422 с указанием поля; метка, совпадающая с существующей без учёта регистра, — 409. При ошибке ни проекта, ни пакета нет.

Scenario: Новый проект

  • WHEN POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120, author: "viewer"}, есть проект AWPCA
  • THEN 201; GET /projects отдаёт два проекта, у STOCKS_DRRK board_type = 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: "AWPCA", name: "…"}, проект AWPCA есть
  • THEN 409, пакет не поставлен

Scenario: Без названия

  • WHEN POST /projects {label: "STOCKS_DRRK", name: " "}
  • THEN 422 с указанием поля name

MODIFIED Requirements

Requirement: Маршруты интерфейсов

Хаб SHALL предоставлять интерфейсам (MCP, веб-приложение) следующие маршруты под клиентским токеном:

  • GET /issues с фильтрами project (разрез по проекту, см. data-model, «Проекты»: свои и соседи, neighbor относительно проекта; неизвестный проект — 404), sprint, assignee, work_kind, fact_status, status, label, epic, neighbor, q (подстрока темы), hide_closed (1 / true — без задач, чей status входит в настройку closed_statuses; задача без статуса не закрыта), limit/offset — строки зеркала (все поля issue) с fact_status и work_kind из заметок; сокращение до карточки — дело интерфейса. Без 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 команд по ней, comments: [{comment_id, author, created, updated, body}] — все комментарии, новые сверху (по created, затем comment_id, по убыванию).
  • PUT /issues/{key}/note — заметки (частичное обновление, updated_by из тела).
  • POST /batches {title, author, project?, commands: [...]} → {id, estimate_seconds}; 422 при неизвестном типе, неверных параметрах (в том числе неверного типа значения), временном ключе без ensure_issue, двух и более ensure_issue с одинаковой summary (сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся темы), по проверкам постановки из commands (тип bug без настройки, файл вложения не загружен, доска для ensure_sprint), а также по проверкам пакета проекта (см. commands), если передан project.
  • 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_sprint 41 failed, в её params board_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 и 32 queued; 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: Автообновление зеркала

Если 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 с JQL project = AURORA AND labels in (AWPCA, STOCKS_DRRK)

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), fact_status, 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-1620 labels = ["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