295 lines
30 KiB
Markdown
295 lines
30 KiB
Markdown
# commands Specification
|
||
|
||
## Purpose
|
||
Контракт команд моста: каждая команда — желаемое состояние одной задачи Jira (`ensure_*`) или чтение состояния (`refresh`). Здесь типы команд и их параметры, временные ключи внутри пакета, порядок выполнения, политика темпа, обработка ошибок, повторная выдача и подтверждение по состоянию зеркала.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Команда — желаемый результат на одной задаче
|
||
Каждая команда SHALL описывать желаемое состояние одной задачи Jira и выполняться агентом как «прочитать → сравнить →
|
||
записать только разницу». Повторное выполнение любой команды с тем же результатом в Jira SHALL завершаться `done`
|
||
без записи. Типы команд и их контракт:
|
||
|
||
| Тип | Параметры | Что читает | Что пишет |
|
||
|---|---|---|---|
|
||
| `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` | `GET /issue/{key}` | `PUT /issue/{key}` только с отличающимися полями; эпик — по `epic_link.mode` |
|
||
| `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_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: {"<id>": "scrum" | "kanban" | null}` для досок, чей тип агент запрашивал |
|
||
|
||
Доску для `ensure_sprint` хаб SHALL определять при постановке: проект задачи — проекты, чьи метки есть у задачи в
|
||
зеркале (для временного ключа — в `labels` её `ensure_issue` в пакете, после дописывания метки проекта); `board_id` —
|
||
доска этого проекта. 422 при постановке: задача ни в одном проекте; в нескольких проектах с разными досками; у проекта
|
||
нет доски; тип доски `kanban` («у проекта P канбан-доска, спринтов нет»); тип доски ещё не известен («тип доски проекта P
|
||
ещё не известен — дождитесь обновления из Jira»). `boards` у `refresh` хаб SHALL дописывать при каждой выдаче (в базе
|
||
не хранится): все доски проектов без повторов. По результату `refresh` хаб SHALL записать присланные типы `scrum` и
|
||
`kanban` в проекты с этой доской; `null` тип не меняет.
|
||
|
||
Семантика `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`
|
||
|
||
### Requirement: Временные ключи и ссылки внутри пакета
|
||
Параметр-ключ любой команды MAY быть временным `new:<n>`. Хаб SHALL выдавать агенту такую команду только после `done`
|
||
команды `ensure_issue` с тем же `temp_key` в том же пакете, подставив настоящий ключ. Если `ensure_issue` завершилась
|
||
`failed` или `cancelled`, зависимые команды SHALL получить `skipped` с ошибкой «создание задачи new:<n> отменено», если
|
||
`ensure_issue` отменена, иначе «задача new:<n> не создана». Временный ключ вне пакета с его `ensure_issue` SHALL
|
||
отвергаться при создании пакета (422).
|
||
|
||
#### Scenario: Создать и привязать
|
||
- **WHEN** пакет: `ensure_issue {temp_key: new:60, …}`, `ensure_link {source: new:60, target: AURORA-1537, …}`
|
||
- **THEN** вторая команда выдаётся агенту с `source = AURORA-1590` после завершения первой
|
||
|
||
#### Scenario: Создание отменено
|
||
- **WHEN** пакет: `ensure_issue {temp_key: new:901, …}` (отменена с `force: true`), `ensure_comment {key: new:901, …}`
|
||
- **THEN** `ensure_comment` — `skipped` с ошибкой «создание задачи new:901 отменено»
|
||
|
||
#### Scenario: Создание упало
|
||
- **WHEN** `ensure_issue {temp_key: new:902, …}` завершилась `failed`, зависимая `ensure_link {source: new:902, …}`
|
||
- **THEN** `ensure_link` — `skipped` с ошибкой «задача new:902 не создана»
|
||
|
||
### Requirement: Пакет и порядок выполнения
|
||
Пакет SHALL создаваться одним вызовом с автором, заголовком и списком команд; хаб SHALL назначить `seq` по порядку списка
|
||
и добавить в хвост `refresh` по всем ключам пакета (кроме пакетов, состоящих только из `refresh`). Хаб SHALL проверять
|
||
типы параметров: текстовые параметры и ключи — строки (`null` у необязательного параметра — «не задан»; агент
|
||
трактует его по контракту типа, для `ensure_fields` — очистка поля), `target` команды
|
||
`ensure_status` — строка или список строк, `labels`, `labels_add`, `labels_remove`, `keys` — списки строк, `replace` —
|
||
логическое значение; нарушение — 422 с именем параметра. Пакет с двумя и более `ensure_issue` с одинаковой `summary`
|
||
(после обрезки пробелов по краям) SHALL отвергаться (422). Очередь SHALL выдавать команды в порядке
|
||
`(batch.created_at, seq)`, одну в полёте. Пакет и его `queued` команды MAY быть отменены; `claimed` команда MUST NOT
|
||
отменяться автоматически.
|
||
|
||
#### Scenario: Оценка длительности
|
||
- **WHEN** создаётся пакет из 3 `ensure_fields` и 2 `ensure_comment`
|
||
- **THEN** `estimate_seconds` = сумма пауз по политике для последовательности типов + пауза `refresh`
|
||
|
||
#### Scenario: Две задачи с одной темой
|
||
- **WHEN** пакет содержит `ensure_issue {temp_key: new:1, summary: "Отчёт"}` и `ensure_issue {temp_key: new:2, summary: " Отчёт "}`
|
||
- **THEN** 422 с темой «Отчёт», пакет не создан
|
||
|
||
#### Scenario: Параметр не того типа
|
||
- **WHEN** пакет содержит `ensure_fields {key: X, labels_add: "AWPCA"}`
|
||
- **THEN** 422 с указанием `labels_add`, пакет не создан
|
||
|
||
### Requirement: Политика темпа и пауза на команде
|
||
Хаб SHALL хранить `pacing` как минимальные зазоры в секундах по типу команды (по умолчанию: `ensure_issue` и
|
||
`ensure_fields` 120, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint`, `ensure_attachment` 30, `refresh` 2). При выдаче команды
|
||
хаб SHALL сообщить `pause_seconds` = зазор по типу выдаваемой команды; агент SHALL выдержать его от времени своего
|
||
последнего вызова Jira. Внутри одной команды с несколькими записями (`ensure_link` с `replace`, путь переходов) агент
|
||
SHALL выдерживать зазор типа команды между записями.
|
||
|
||
#### Scenario: Нет следующей команды — нет паузы
|
||
- **WHEN** очередь пуста
|
||
- **THEN** `claim` возвращает пустой ответ, агент не спит сверх долгого запроса
|
||
|
||
### Requirement: Ошибки и подтверждение по состоянию
|
||
Ошибка команды SHALL записываться в `error` (HTTP-код, текст Jira); хаб SHALL хранить не больше 1500 символов `error`,
|
||
обрезая присланное. Пакет после ошибки продолжается.
|
||
|
||
**Повторная выдача.** Агент запрашивает новую команду, только когда у него нет незавершённой (см. `agent`), поэтому
|
||
команда в `claimed` к моменту следующего `claim` считается не доставленной или потерянной агентом. При `claim` хаб SHALL
|
||
сначала выдать снова самую старую (по `claimed_at`, затем `id`) команду в `claimed`: тот же `id` и параметры,
|
||
`pause_seconds` по текущей политике темпа, новый `claimed_at`, `deliveries + 1`. Если у неё уже `deliveries >= 3`, хаб SHALL
|
||
перевести её в `failed` с `error = "агент не отчитался после 3 выдач"` и перейти к следующей (довыдача другой `claimed`
|
||
или следующая `queued`). Довыдача — доставка той же выдачи, а не повтор команды: новых команд хаб MUST NOT создавать
|
||
сам, повтор после `failed` — новая команда с теми же параметрами по запросу интерфейса. Если агент всё же отчитался
|
||
`done` с ключом по `ensure_issue`, переведённой в `failed` по пределу, хаб SHALL записать ключ в результат и перенести
|
||
заметку, не меняя статус (как для отменённой, см. `hub-api`).
|
||
|
||
**Подтверждение по состоянию.** Если после любого `refresh` желаемый результат команды в `claimed` виден в зеркале, хаб
|
||
SHALL перевести её в `confirmed_by_state`: `ensure_fields` — все поля совпадают (строки сравниваются без различия
|
||
`\r\n` и `\n`; `null` и пустая строка равны); `ensure_status` — статус равен последнему в пути; `ensure_link` — связь
|
||
есть и конфликтов нет; `ensure_sprint` — спринт совпадает. `ensure_issue`, `ensure_comment`, `ensure_attachment` и `refresh` по
|
||
состоянию не подтверждаются (вложений в зеркале нет): потерянный отчёт по ним закрывается повторной выдачей (агент находит созданную задачу поиском дубля)
|
||
или отменой человеком.
|
||
|
||
#### Scenario: Отчёт потерян, факт на месте
|
||
- **WHEN** `ensure_sprint {key: X, sprint: "Спринт 2"}` в `claimed`, после выполненного `refresh` зеркало показывает X в «Спринт 2»
|
||
- **THEN** команда `confirmed_by_state`
|
||
|
||
#### Scenario: Описание с CRLF в зеркале
|
||
- **WHEN** `ensure_fields {key: X, description: "a\nb"}` в `claimed`, после `refresh` в зеркале у X `description = "a\r\nb"`
|
||
- **THEN** команда `confirmed_by_state`
|
||
|
||
#### Scenario: Ответ на claim потерян
|
||
- **WHEN** команда 40 выдана (`deliveries = 1`), ответ до агента не дошёл, агент снова прислал `claim`
|
||
- **THEN** `claim` отдаёт команду 40, `deliveries = 2`; в очереди есть `queued` команды, но выдаётся 40
|
||
|
||
#### Scenario: Предел выдач
|
||
- **WHEN** команда 41 в `claimed` с `deliveries = 3`, агент снова прислал `claim`, в очереди есть команда 42
|
||
- **THEN** 41 — `failed` «агент не отчитался после 3 выдач»; `claim` отдаёт 42
|
||
|
||
#### Scenario: Потерянный отчёт о создании задачи
|
||
- **WHEN** `ensure_issue {temp_key: new:5, summary: "Отчёт"}` выполнена агентом, отчёт не дошёл, агент прислал `claim`
|
||
- **THEN** команда выдаётся снова; агент находит созданную задачу поиском дубля и отчитывается `done` с её ключом; второй
|
||
задачи нет, заметка `new:5` перенесена на ключ
|
||
|
||
### Requirement: Описание параметров и ошибки валидации
|
||
Описание параметров каждого типа команды (обязательные, необязательные, формат значения, условие на набор) SHALL
|
||
храниться в коде хаба в одном месте с правилами валидации; описание инструмента MCP `batch_create` SHALL собираться из
|
||
него (см. `mcp`). Ошибка валидации «нет <параметры>» и «лишние параметры <параметры>» SHALL заканчиваться ожидаемым
|
||
набором параметров типа: «ожидаются <обязательные>» и, если у типа есть необязательные, «необязательные <список>».
|
||
|
||
#### Scenario: Неверное имя параметра
|
||
- **WHEN** `POST /batches` с `ensure_status {key: X, status: "Готово"}`
|
||
- **THEN** 422 «команда 1 (ensure_status): нет target; ожидаются key, target»
|
||
|
||
#### Scenario: Лишний параметр у типа с необязательными
|
||
- **WHEN** `POST /batches` с `ensure_link {source: X, target: Y, link_type: "связанные", force: true}`
|
||
- **THEN** 422 «команда 1 (ensure_link): лишние параметры force; ожидаются source, target, link_type; необязательные replace»
|
||
|
||
### Requirement: Проверки ensure_issue с типом bug и ensure_attachment при постановке
|
||
При постановке пакета хаб SHALL отвергать 422 без создания пакета: `ensure_issue` с `issue_type = bug`, если в настройках
|
||
нет `issue_types.bug` с непустым `name` или `id` («нет настройки issue_types.bug»); `ensure_attachment`, если `file` — не
|
||
64 hex-символа в нижнем регистре, файла с таким sha256 нет в хранилище хаба («файл <sha> не загружен: POST /files»),
|
||
`filename` пустой (или из одних пробелов) или содержит `/`, `\`, `"` или управляющие символы (код < 32 и 127,
|
||
в том числе CR и LF): такие имена ломают `multipart/form-data`. Клиент `size` не передаёт (лишний параметр — 422). При выдаче агенту
|
||
(`claim`, в том числе повторной) хаб SHALL добавить к параметрам `ensure_attachment` `size` — текущий размер файла в
|
||
хранилище, или `size = null`, если файла уже нет; в базе параметры команды остаются без `size`, поэтому повтор команды
|
||
(`POST /commands/{id}/retry`) проходит проверку как новая.
|
||
|
||
#### Scenario: Ошибка без настройки типа
|
||
- **WHEN** `POST /batches` с `ensure_issue {issue_type: bug, …}`, в `issue_types` только `task` и `epic`
|
||
- **THEN** 422 «команда 1: нет настройки issue_types.bug», пакет не создан
|
||
|
||
#### Scenario: Вложение без загруженного файла
|
||
- **WHEN** `POST /batches` с `ensure_attachment {key: X, file: <64 hex>, filename: "a.xlsx"}`, файла нет в хранилище
|
||
- **THEN** 422 «команда 1: файл <sha> не загружен: POST /files», пакет не создан
|
||
|
||
#### Scenario: Ошибка с вложением одним пакетом
|
||
- **WHEN** пакет `[ensure_issue {temp_key: new:1, issue_type: bug, …}, ensure_sprint {key: new:1, sprint: "Спринт 2"},
|
||
ensure_attachment {key: new:1, file: <sha>, filename: "ОСВ 60.xlsx"}]`, файл загружен
|
||
- **THEN** пакет создан; `ensure_attachment` выдаётся после завершения `ensure_issue` с подставленным ключом и `size` файла
|
||
|
||
### Requirement: Пакет проекта
|
||
Пакет, поставленный с `project = P` (см. `hub-api`), SHALL проходить при постановке, до записи, проверки проекта;
|
||
любая неудача — 422 с ключом задачи и причиной, пакет не создаётся:
|
||
- `P` нет среди проектов — 422;
|
||
- `ensure_issue`: если в `labels` нет P, хаб SHALL дописать P в конец `labels` (в базе параметры хранятся уже с ней;
|
||
без `labels` — `labels = [P]`); метка, совпадающая с P без учёта регистра, но не точно, SHALL заменяться на P; метка
|
||
другого проекта в `labels` — 422;
|
||
- `ensure_fields`: P в `labels_remove` или метка другого проекта в `labels_add` — 422;
|
||
- `ensure_fields`, `ensure_comment`, `ensure_status`, `ensure_sprint`, `ensure_attachment`: `key` SHALL быть временным
|
||
ключом `ensure_issue` этого пакета или задачей зеркала с меткой P, иначе 422 «<key> не в проекте P»;
|
||
- `ensure_link`: хотя бы один из `source`, `target` — временный ключ этого пакета или задача зеркала с меткой P;
|
||
- `refresh` — без проверок проекта.
|
||
«Метка другого проекта» и «P в `labels_remove`» SHALL сравниваться без учёта регистра; принадлежность задачи проекту
|
||
(метка P у задачи зеркала) — точным совпадением, как в разрезе проекта. Пакет без `project` эти проверки не проходит,
|
||
метки хаб в нём не дописывает.
|
||
|
||
#### Scenario: Метка проекта дописана
|
||
- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_issue {temp_key: new:1, summary: "Сверка остатков", labels: ["kind-dev"]}`
|
||
- **THEN** пакет создан; в параметрах команды `labels = ["kind-dev", "STOCKS_DRRK"]`
|
||
|
||
#### Scenario: Чужая метка проекта
|
||
- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_issue {…, labels: ["awpca"]}`, есть проект `AWPCA`
|
||
- **THEN** 422 «метка другого проекта: awpca», пакет не создан
|
||
|
||
#### Scenario: Задача другого проекта
|
||
- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_fields {key: AURORA-1538, duedate: …}`, у AURORA-1538 метка только `AWPCA`
|
||
- **THEN** 422 «AURORA-1538 не в проекте STOCKS_DRRK»
|
||
|
||
#### Scenario: Связь с соседом
|
||
- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_link {source: AURORA-1 (метка STOCKS_DRRK), target: AURORA-1538 (метка AWPCA), …}`
|
||
- **THEN** пакет создан
|
||
|
||
### Requirement: Журнал изменений в refresh
|
||
Отметка журнала — `started_at` последнего `sync_run` с `history_at IS NOT NULL` (любой `query`). JQL стартовой загрузки
|
||
— `(<базовая JQL>) AND sprint in openSprints() AND statusCategory != Done`. Хаб SHALL при каждой выдаче `refresh`
|
||
(первой и повторной) дописывать в параметры `history` (в базе `params` без него, как `boards`):
|
||
- полный прогон (`refresh` по базовой JQL), отметка есть — `{since: <отметка − 15 минут, ISO-8601 UTC с Z>}`; если
|
||
`query` прогона-отметки не равна текущей базовой JQL — также `jql: <JQL стартовой загрузки>`;
|
||
- полный прогон, отметки нет — `{jql: <JQL стартовой загрузки>}`;
|
||
- `refresh {keys}` и `refresh` по небазовой JQL — `{since: …}`, если отметка есть; иначе `history` не дописывается;
|
||
- без базовой JQL (нет проектов) — `history` не дописывается.
|
||
|
||
Результат `refresh` MAY содержать `history` (см. `agent`, «Журнал изменений»); хаб SHALL принимать результат и без
|
||
него. Отметку (`sync_run.history_at`) хаб SHALL проставлять по правилам `data-model`, «Отметка журнала в журнале
|
||
синхронизаций».
|
||
|
||
#### Scenario: Первый refresh после выкатки
|
||
- **WHEN** проект `AWPCA`, прогонов с `history_at` нет; выдаётся полный `refresh`
|
||
- **THEN** в выданных параметрах `history = {jql: '(project = AURORA AND labels in ("AWPCA")) AND sprint in
|
||
openSprints() AND statusCategory != Done'}`; в базе `params` без `history`
|
||
|
||
#### Scenario: Регулярный refresh
|
||
- **WHEN** последний полный прогон с `history_at` начат в `2026-10-22T09:00:00+00:00` по текущей базовой JQL; выдаётся
|
||
автообновление
|
||
- **THEN** `history = {since: "2026-10-22T08:45:00Z"}`
|
||
|
||
#### Scenario: Журнал не дочитан
|
||
- **WHEN** прогон в 10:00 завершился `done` с `history.error`, прогон в 09:00 — с принятым журналом; выдаётся следующий
|
||
полный `refresh`
|
||
- **THEN** `history.since` — от прогона 09:00
|
||
|
||
#### Scenario: Новый проект
|
||
- **WHEN** прогон с `history_at` в 09:00 по JQL только с `AWPCA`; затем создан проект `STOCKS_DRRK`; выдаётся полный
|
||
`refresh`
|
||
- **THEN** `history = {since: "…T08:45:00Z", jql: <стартовая по новой базовой JQL>}`
|
||
|
||
#### Scenario: Хвост пакета
|
||
- **WHEN** выдаётся хвостовой `refresh {keys: [X]}`, отметка есть
|
||
- **THEN** `history = {since: …}`; по завершении `done` с `result.history` у его `sync_run` `history_at = NULL`
|
||
|
||
#### Scenario: Хвост пакета до первой отметки
|
||
- **WHEN** выдаётся хвостовой `refresh {keys: [X]}`, прогонов с `history_at` нет
|
||
- **THEN** в выданных параметрах нет `history`
|