Andrey Epifancev c00b0bd4b2 Спека sp-estimation-board: оценка через доску (#113)
ensure_fields со story_points получает board_id доски проекта задачи; агент v16 пишет
оценку через /rest/agile/1.0/issue/{key}/estimation — поле не на экране редактирования.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:07:56 +04:00

16 KiB
Raw Blame History

MODIFIED Requirements

Requirement: Команда — желаемый результат на одной задаче

Каждая команда SHALL описывать желаемое состояние одной задачи Jira и выполняться агентом как «прочитать → сравнить → записать только разницу». Повторное выполнение любой команды с тем же результатом в Jira SHALL завершаться done без записи. fetch_attachment в Jira не пишет: она читает файл вложения и передаёт его хабу; повтор безопасен. Типы команд и их контракт:

Тип Параметры Что читает Что пишет
ensure_issue temp_key (new:<n>), issue_type (task/epic/bug), summary, description, priority, duedate, planned_start, planned_end, labels, epic_key, assignee поиск project = <proj> AND reporter = currentUser() AND created >= -7d, точное сравнение summary при отсутствии — POST /issue с типом из issue_types.<issue_type> (для эпика — с epic_name_field_id); результат key
ensure_fields key, любое подмножество: summary, description, priority, duedate, planned_start, planned_end, labels_add, labels_remove, epic_key, assignee, story_points (число ≥ 0 или null — очистить); при story_points board_id хаб дописывает при постановке (от клиента — 422) GET /issue/{key} — если есть поля, кроме оценки; оценка — GET /rest/agile/1.0/issue/{key}/estimation?boardId=<board_id> PUT /issue/{key} только с отличающимися полями, кроме оценки; эпик — по epic_link.mode; оценка отличается — PUT /rest/agile/1.0/issue/{key}/estimation?boardId=<board_id> с {value} (поле оценки доски, экран редактирования Jira не нужен); команда без board_id — оценка через PUT /issue/{key} в поле story points, как в v15 (см. agent, «Спринты, ранг и оценки»)
ensure_comment key, marker, body GET /issue/{key}/comment комментарий с marker в начале: PUT если текст отличается, POST если нет
ensure_status key, target (имя статуса или список-путь) GET /issue/{key} (статус), GET /issue/{key}/transitions на каждом шаге POST /transitions по шагам пути; при отсутствии перехода — failed с перечнем доступных
ensure_link source, target, link_type (логическое имя из link_types), replace (bool) GET /issue/{source} (issuelinks) нет нужной и нет конфликта — POST /issueLink; конфликт (другой тип из link_types или обратное направление) и replace — DELETE лишней, затем POST; конфликт без replace — failed с описанием
ensure_sprint key, sprint (имя или backlog); board_id хаб дописывает при постановке (от клиента — 422) GET /issue/{key} (поле спринта), список спринтов доски board_id (активные и будущие) POST /rest/agile/1.0/sprint/{id}/issue или POST /rest/agile/1.0/backlog/issue
ensure_sprint_meta sprint (имя), необязательные rename_to, start_date, end_date (ГГГГ-ММ-ДД), goal; board_id хаб дописывает при постановке (от клиента — 422) спринты доски board_id (все состояния) спринта с именем sprint нет — POST /rest/agile/1.0/sprint (originBoardId, имя rename_to или sprint, даты, цель); есть — POST /rest/agile/1.0/sprint/{id} только с отличающимися полями; спринт закрыт — failed «спринт закрыт»; результат {sprint_id, created}
ensure_sprint_state sprint, state (active или closed), move_open_to (имя активного или будущего спринта той же доски или backlog; при closed обязателен, при active — 422); board_id — как у ensure_sprint_meta спринты доски (все состояния), задачи спринта и колонки доски active: спринт будущий — POST /sprint/{id} state = active (нет дат у спринта — failed «у спринта нет дат: задайте ensure_sprint_meta»); closed: задачи спринта не в последней колонке доски — в move_open_to, затем state = closed; уже в нужном состоянии — ничего; результат {sprint_id, moved: [ключ…]}
ensure_rank key и ровно одно из before, after (ключ другой задачи) ранг обеих задач key уже выше before (ниже after) — ничего; иначе PUT /rest/agile/1.0/issue/rank (rankBeforeIssue / rankAfterIssue)
ensure_attachment key, file (sha256 файла в хранилище хаба, 64 hex-символа), filename (имя вложения в Jira, без /, \, " и управляющих символов); size хаб дописывает при выдаче GET /issue/{key}?fields=attachment вложения с тем же filename и размером нет — файл у хаба (GET /agent/files/{file}), затем POST /issue/{key}/attachments (multipart); есть — ничего; результат attachment_id
refresh keys (список) или jql; boards ([{id, type}] — доски проектов, type — scrum, kanban или null) хаб дописывает при выдаче search с полями зеркала, по 100 на страницу; плюс дочитывание соседей по issuelinks; доски с неизвестным типом — GET /rest/agile/1.0/board/{id} ничего в Jira; состояние уходит в хаб; результат — `boards: {"": "scrum"
fetch_attachment key, attachment_id (id вложения Jira, строка из цифр) GET /issue/{key}?fields=attachment (имя и размер вложения) ничего в Jira; содержимое — GET <base_url>/secure/attachment/{id}/{имя}, затем хабу POST /agent/files; результат {attachment_id, file, size}

Доску для ensure_sprint и для ensure_fields с story_points хаб SHALL определять при постановке: проект задачи — проекты, чьи метки есть у задачи в зеркале (для временного ключа — в labels её ensure_issue в пакете, после дописывания метки проекта); board_id — доска этого проекта. 422 при постановке: задача ни в одном проекте; в нескольких проектах с разными досками; у проекта нет доски; тип доски kanban («у проекта P канбан-доска, спринтов нет»; у ensure_fields — «у проекта P канбан-доска, оценка — только на scrum-доске»); тип доски ещё не известен («тип доски проекта P ещё не известен — дождитесь обновления из Jira»). boards у refresh хаб SHALL дописывать при каждой выдаче (в базе не хранится): все доски проектов без повторов. По результату refresh хаб SHALL записать присланные типы scrum и kanban в проекты с этой доской; null тип не меняет.

Доску для ensure_sprint_meta и ensure_sprint_state хаб SHALL определять при постановке по проекту пакета (project пакета): пакет без проекта — 422 «команды спринта — только в пакете проекта»; у проекта нет доски, доска kanban или тип ещё не известен — 422 с теми же текстами, что у ensure_sprint. ensure_rank в пакете проекта принадлежит проекту по key; before/after — любая задача. Повтор команды (command_retry) SHALL снимать board_id у ensure_sprint_meta, ensure_sprint_state и ensure_fields, как у ensure_sprint: доска определяется заново. Хвостовой refresh пишущего пакета с ensure_sprint_meta или ensure_sprint_state SHALL быть refresh {jql: <JQL проекта пакета>} (project = <project_key> AND labels in ("<метка>")); иначе — как раньше, по ключам команд. Подтверждения по состоянию зеркала у ensure_sprint_meta, ensure_sprint_state, ensure_rank нет.

ensure_sprint_meta SHALL искать спринт по имени среди всех спринтов доски: закрытый — failed «спринт закрыт»; несколько с этим именем — failed «несколько спринтов с именем …: <id…>»; rename_to на имя другого спринта доски — failed «имя занято». ensure_sprint_state: closed для будущего спринта — failed «спринт не начат»; active для закрытого — failed «спринт закрыт»; результат ensure_rank — {changed}.

Семантика ensure_link: source блокирует/ссылается на target; агент в POST /issueLink SHALL ставить source в inwardIssue, target в outwardIssue (проверено на Jira банка 11.10.2026).

Текст описания (ensure_fields) и тело комментария (ensure_comment) агент SHALL сравнивать с текущим значением в Jira без различия переводов строк \r\n и \n; записывается текст команды как есть. Jira хранит присланный текст, а правленный в веб-интерфейсе — с \r\n (проверено на Jira банка 13.10.2026).

Scenario: Повтор уже выполненной команды

  • WHEN ensure_fields {key: AURORA-1533, duedate: 2026-10-23} выполняется, а в Jira срок уже 23.10
  • THEN агент не делает PUT, отчёт done, jira_calls = 1

Scenario: Описание отличается только переводами строк

  • WHEN ensure_fields {key: X, description: "а\nб"}, а в Jira описание X — "а\r\nб"
  • THEN агент не делает PUT, отчёт done с changed = []

Scenario: Замена связи

  • WHEN ensure_link {source: AURORA-1537, target: AURORA-1538, link_type: "связанные", replace: true} и в Jira между ними «блокирует начало» 1537→1538
  • THEN DELETE старой связи, пауза по политике, POST новой; отчёт done с обеими операциями

Scenario: Связь-конфликт без replace

  • WHEN та же команда с replace: false
  • THEN failed, в error найденная связь, Jira не изменена

Scenario: Переход по пути

  • WHEN ensure_status {key: X, target: ["Разработка", "Готово"]} и X в «Ожидание разработки»
  • THEN два POST /transitions с паузой между ними; если X уже «Готово» — done без записи

Scenario: Создание ошибки

  • WHEN ensure_issue {temp_key: new:1, issue_type: bug, summary: "ОСВ 60 не распознана", labels: [AWPCA]}, в настройках issue_types.bug = {name: "Ошибка"} без id
  • THEN агент создаёт задачу с issuetype = {name: "Ошибка"}; отчёт done с ключом

Scenario: Вложение уже есть

  • WHEN ensure_attachment {key: X, file: <sha>, filename: "ОСВ 60.xlsx"}, у X в Jira есть вложение «ОСВ 60.xlsx» того же размера
  • THEN агент не скачивает файл у хаба и не загружает его в Jira; отчёт done с attachment_id найденного, jira_calls = 1

Scenario: Вложение с тем же именем, другого размера

  • WHEN та же команда, у X есть «ОСВ 60.xlsx» другого размера
  • THEN агент загружает файл вторым вложением с тем же именем (Jira это допускает); отчёт done с новым attachment_id

Scenario: Создание с проверкой дубля

  • WHEN ensure_issue повторяется после падения агента, а задача с той же темой от нас уже создана час назад
  • THEN агент не создаёт вторую, отчёт done с ключом найденной

Scenario: Спринт задачи проекта

  • WHEN у AURORA-1538 метка AWPCA, проект AWPCA с доской 4216 типа scrum; ensure_sprint {key: AURORA-1538, sprint: "Спринт 2"}
  • THEN пакет создан, в параметрах команды board_id = 4216

Scenario: Спринт на канбане

  • WHEN у AURORA-1 метка STOCKS_DRRK, доска проекта — kanban; ensure_sprint {key: AURORA-1, sprint: "Спринт 2"}
  • THEN 422 «у проекта STOCKS_DRRK канбан-доска, спринтов нет»

Scenario: Тип доски определён

  • WHEN проект STOCKS_DRRK с доской 5120 и board_type = null; выдаётся refresh, агент вернул boards: {"5120": "kanban"}
  • THEN при выдаче в параметрах boards есть {id: 5120, type: null}; после отчёта у проекта board_type = kanban

Scenario: Оценка задачи проекта

  • WHEN у AURORA-1692 метка AWPCA, доска проекта 4216 типа scrum; ensure_fields {key: AURORA-1692, story_points: 1}
  • THEN пакет создан, в параметрах команды board_id = 4216; агент читает и пишет оценку через /rest/agile/1.0/issue/AURORA-1692/estimation?boardId=4216, PUT /issue не делает

Scenario: Поля без оценки — без доски

  • WHEN ensure_fields {key: AURORA-1, duedate: 2026-10-30}, задача вне проектов
  • THEN пакет создан, board_id в параметрах нет

Scenario: Оценка задачи вне проектов

  • WHEN ensure_fields {key: AURORA-1, story_points: 2}, у AURORA-1 нет меток проектов
  • THEN 422 «команда 1: AURORA-1 не входит ни в один проект — доску для оценки не определить»

Scenario: Оценка на канбане

  • WHEN у AURORA-1 метка STOCKS_DRRK, доска проекта — kanban; ensure_fields {key: AURORA-1, story_points: 2}
  • THEN 422 «команда 1: у проекта STOCKS_DRRK канбан-доска, оценка — только на scrum-доске»