Архив OpenSpec: change-history (#70)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
91745374dc
commit
534e43865b
@ -420,3 +420,76 @@ README агента SHALL описывать: первую установку (
|
||||
#### Scenario: Спринт по доске из команды
|
||||
- **WHEN** `ensure_sprint {key: AURORA-1538, sprint: "Спринт 2", board_id: 4216}`
|
||||
- **THEN** агент ищет «Спринт 2» среди активных и будущих спринтов доски 4216 и ставит задачу в него
|
||||
|
||||
### Requirement: Журнал изменений
|
||||
Если в параметрах `refresh` есть `history`, агент (с v13) SHALL после отправки всех пачек состояния прочитать журнал
|
||||
изменений (changelog) Jira и отправить его хабу. `history` может содержать `since`, `jql` или оба:
|
||||
- `since` (регулярная часть) — из задач, выгруженных этим `refresh` (база и соседи), агент SHALL отобрать те, у которых
|
||||
Jira `updated` не раньше `since` (сравнение моментов времени с учётом смещения), и прочитать их журнал запросом
|
||||
`search` `jql = key in (...)`, `fields = key`, `expand = changelog`, по 100 ключей; из журнала каждой задачи взять
|
||||
записи с `created` не раньше `since`. Таких задач нет — вызовов Jira на этом шаге нет.
|
||||
- `jql` (стартовая загрузка) — агент SHALL выполнить `search` по `history.jql` с `fields = key`, `expand = changelog`
|
||||
(постранично, `maxResults = 100`) и взять журнал найденных задач целиком. Стартовая часть выполняется первой;
|
||||
регулярная часть не запрашивает журнал задач, уже прочитанных стартовой (их журнал уже взят целиком).
|
||||
|
||||
Если у задачи `changelog.total` больше числа записей в ответе, агент SHALL дочитать журнал
|
||||
`GET /rest/api/2/issue/{key}?fields=key&expand=changelog`; если и он неполный — передать что есть, записать в лог ключ
|
||||
и «получено N из M» и добавить ключ в `history.incomplete` результата.
|
||||
|
||||
Элемент для хаба — одно поле записи журнала: `{history_id, seq, key, created, author, field_id, field_name, from,
|
||||
from_string, to, to_string}`: `history_id` — id записи строкой; `seq` — номер элемента в `items` записи, с 0; `created` —
|
||||
время записи как есть; `author` — отображаемое имя автора, иначе логин, иначе `null`; `field_name` — `field` элемента;
|
||||
`field_id` — `fieldId` элемента, а если его нет: для `fieldtype = custom` — id поля, найденный по имени в
|
||||
`GET /rest/api/2/field` (не больше одного такого вызова за `refresh`), для прочих — `field`; не найден — `null`; `from`,
|
||||
`fromString`, `to`, `toString` — как есть, нестроковое значение — строкой. Запись без `id` или с пустым `created` и
|
||||
элемент с пустым `field` агент SHALL отбросить с записью в лог (ключ задачи). Элементы SHALL отправляться
|
||||
`POST /agent/history` пачками до 500; повторы при отправке — как у пачек состояния.
|
||||
|
||||
Результат `refresh` SHALL содержать `history = {issues: <задач с хотя бы одним элементом>, changes: <элементов
|
||||
отправлено>, incomplete: [<ключи>]}`. Ответ не 2xx (`HttpError`) от Jira или хаба MUST NOT переводить `refresh` в
|
||||
`failed` и MUST NOT вызывать повтор команды: агент SHALL записать ошибку в лог и вернуть `done`, добавив в `history`
|
||||
текст ошибки (у ошибки хаба — префикс «хаб: ») — `error` при ошибке регулярной части или отправки элементов,
|
||||
`backfill_error` при ошибке `search` стартовой загрузки (регулярная часть при этом выполняется). Обрыв связи с Jira или
|
||||
хабом (`ConnectionError`) на шаге журнала обрабатывается, как в остальном `refresh`. Без `history` в параметрах агент
|
||||
журнал не читает, ключа `history` в результате нет.
|
||||
|
||||
#### Scenario: Ничего не менялось
|
||||
- **WHEN** `refresh {jql: base, history: {since: "2026-10-22T08:45:00Z"}}`, у всех 180 задач `updated` раньше `since`
|
||||
- **THEN** вызовов `search` с `expand = changelog` нет; результат `history = {issues: 0, changes: 0, incomplete: []}`
|
||||
|
||||
#### Scenario: Две задачи изменились
|
||||
- **WHEN** тот же `refresh`, у X и Y `updated` позже `since`; у X в журнале записи от вчера и от 09:10 UTC (смена статуса и
|
||||
резолюции), у Y — от 09:30 UTC (правка описания)
|
||||
- **THEN** один `search` `key in (X,Y)` с `expand = changelog`; хабу отправлены 3 элемента (у X — `seq` 0 и 1 одной записи,
|
||||
вчерашняя запись не отправлена); результат `history = {issues: 2, changes: 3, incomplete: []}`
|
||||
|
||||
#### Scenario: Стартовая загрузка
|
||||
- **WHEN** `refresh {jql: base, history: {jql: "(base) AND sprint in openSprints() AND statusCategory != Done"}}`, по ней
|
||||
12 задач
|
||||
- **THEN** `search` по `history.jql` с `expand = changelog`; журнал 12 задач отправлен целиком
|
||||
|
||||
#### Scenario: Задача в обеих выборках
|
||||
- **WHEN** `history = {since, jql}`, у X записи журнала от 2020 года и от сегодня, X изменилась после `since` и входит в
|
||||
выборку `history.jql`
|
||||
- **THEN** хабу отправлены обе записи X; журнал X запрошен одним `search` (стартовым)
|
||||
|
||||
#### Scenario: Плановая дата без fieldId
|
||||
- **WHEN** элемент журнала `{field: "Плановое окончание", fieldtype: "custom"}` без `fieldId`; в `GET /field` у поля с этим
|
||||
именем id `customfield_10201`
|
||||
- **THEN** у элемента `field_id = "customfield_10201"`; `GET /field` вызван один раз за `refresh`
|
||||
|
||||
#### Scenario: Ошибка Jira при чтении журнала
|
||||
- **WHEN** `search` с `expand = changelog` для регулярной части ответил 500
|
||||
- **THEN** состояние в хабе обновлено; `refresh` — `done` с `history.error`, повтора команды нет; в логе — ошибка
|
||||
|
||||
#### Scenario: Ошибка стартовой загрузки
|
||||
- **WHEN** `history = {since: …, jql: …}`, `search` по `history.jql` ответил 400, у X `updated` позже `since`
|
||||
- **THEN** журнал X отправлен; `refresh` — `done`, в `history` — `backfill_error` с текстом Jira, `error` нет
|
||||
|
||||
#### Scenario: Неполный журнал
|
||||
- **WHEN** у X `changelog.total = 250`, и `search`, и `GET /issue/X?expand=changelog` вернули 100 записей
|
||||
- **THEN** отправлены 100 записей X; в логе «X: получено 100 из 250»; `history.incomplete = ["X"]`
|
||||
|
||||
#### Scenario: Хаб без журнала
|
||||
- **WHEN** `refresh {jql: base}` без `history`
|
||||
- **THEN** журнал не читается; в результате нет ключа `history`
|
||||
|
||||
@ -250,3 +250,45 @@ SHALL перевести её в `confirmed_by_state`: `ensure_fields` — вс
|
||||
#### 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`
|
||||
|
||||
@ -6,15 +6,15 @@
|
||||
## Requirements
|
||||
|
||||
### Requirement: Схема базы хаба
|
||||
База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_comment`, `issue_note`, `batch`,
|
||||
`command`, `sync_run`, `agent_status`, `setting`, `project`. Схема SHALL создаваться и мигрироваться самим хабом при
|
||||
старте по номеру версии в `setting.schema_version` (текущая — 4). Миграция существующей базы SHALL выполняться в одной
|
||||
База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_comment`, `issue_change`, `issue_note`,
|
||||
`batch`, `command`, `sync_run`, `agent_status`, `setting`, `project`. Схема SHALL создаваться и мигрироваться самим хабом при
|
||||
старте по номеру версии в `setting.schema_version` (текущая — 5). Миграция существующей базы SHALL выполняться в одной
|
||||
транзакции с блокировкой записи; в той же транзакции, после повторной проверки версии, хаб SHALL скопировать файл базы в
|
||||
`backups/<дата-время>-schema-v<старая>.sqlite` рядом с базой — одна копия на миграцию, сколько бы соединений ни
|
||||
открывалось одновременно. Каталог данных хаба и `backups/` SHALL иметь права `0700` (в базе токены): хаб выставляет их
|
||||
при создании и при каждом старте; если выставить не удалось — предупреждение в лог, работа продолжается. Денежных и
|
||||
дробных полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени) и времени
|
||||
комментариев (строки Jira как есть).
|
||||
дробных полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени), строк времени
|
||||
Jira как есть (`issue_comment.created`/`updated`, `issue_change.created`) и значений полей журнала.
|
||||
|
||||
Миграция v3 → v4 SHALL: создать `project` и колонку `batch.project` (у существующих пакетов `NULL`); разобрать прежнюю
|
||||
настройку `base_jql` вида `… labels = <M>` или `… labels in (<M1>, …)` и завести проекты с этими метками (название проекта
|
||||
@ -25,6 +25,10 @@
|
||||
перезапущен хаб прежней версии, который их читает. Не разобранная `base_jql` — проектов нет, предупреждение в лог с
|
||||
прежним значением.
|
||||
|
||||
Миграция v4 → v5 SHALL: создать `issue_change` с индексами; добавить колонки `issue_comment.created_at` и
|
||||
`sync_run.history_at`; заполнить `issue_comment.created_at` у имеющихся комментариев приведением `created` к UTC (не
|
||||
разобралось — `NULL`). У имеющихся `sync_run` `history_at = NULL`.
|
||||
|
||||
#### Scenario: Первый запуск
|
||||
- **WHEN** хаб стартует с несуществующим файлом базы
|
||||
- **THEN** файл создаётся, таблицы и индексы на месте, `setting.schema_version` = текущая; каталог данных с правами `0700`
|
||||
@ -32,18 +36,24 @@
|
||||
#### Scenario: Миграция базы v1
|
||||
- **WHEN** хаб стартует с базой `schema_version = 1`, в которой есть задачи, заметки и команда в `claimed`
|
||||
- **THEN** в `backups/` копия базы v1; добавлена `command.deliveries` (у `claimed` — 1, у остальных — 0); добавлена пустая
|
||||
`issue_comment`; данные на месте; `schema_version = 4`
|
||||
`issue_comment`; данные на месте; `schema_version = 5`
|
||||
|
||||
#### Scenario: Миграция базы v2
|
||||
- **WHEN** хаб стартует с базой `schema_version = 2`, в которой есть задачи, связи и заметки
|
||||
- **THEN** в `backups/` копия базы v2; добавлена пустая `issue_comment`; данные на месте; `schema_version = 4`
|
||||
- **THEN** в `backups/` копия базы v2; добавлена пустая `issue_comment`; данные на месте; `schema_version = 5`
|
||||
|
||||
#### Scenario: Миграция базы v3
|
||||
- **WHEN** хаб стартует с базой `schema_version = 3`, `base_jql = "project = AURORA AND labels = AWPCA"`, `board_id = 4216`,
|
||||
в `sync_run` полный прогон с этой `query`
|
||||
- **THEN** в `backups/` копия базы v3; проект `AWPCA` «АРМ КА» с `board_id = 4216`, `board_type = NULL`; ключи `base_jql`
|
||||
и `board_id` в `setting` на месте; `query` того прогона — `project = AURORA AND labels in ("AWPCA")`; задачи, заметки,
|
||||
пакеты на месте; `schema_version = 4`
|
||||
пакеты на месте; `schema_version = 5`
|
||||
|
||||
#### Scenario: Миграция базы v4
|
||||
- **WHEN** хаб стартует с базой `schema_version = 4`, у задачи X комментарий с `created = "2026-10-22T13:05:11.000+0300"`,
|
||||
в `sync_run` полный прогон
|
||||
- **THEN** в `backups/` копия базы v4; есть пустая `issue_change`; у комментария `created_at = "2026-10-22T10:05:11+00:00"`,
|
||||
`created` прежний; у прогона `history_at = NULL`; `schema_version = 5`
|
||||
|
||||
#### Scenario: refresh в очереди при миграции
|
||||
- **WHEN** в базе v3 стоит `queued` `refresh {jql: "project = AURORA AND labels = AWPCA"}`
|
||||
@ -228,3 +238,63 @@ label, через `, `; без проектов базовой JQL
|
||||
#### Scenario: Та же метка другим регистром
|
||||
- **WHEN** есть проект `AWPCA`, создаётся проект `awpca`
|
||||
- **THEN** отказ «проект с такой меткой уже есть», проект один
|
||||
|
||||
### Requirement: Журнал изменений задач
|
||||
Таблица `issue_change` SHALL хранить журнал изменений задач Jira по строке на поле записи журнала: `history_id` (id
|
||||
записи журнала Jira), `seq` (номер поля в записи, с 0), `key`, `at` (время записи в ISO-8601 UTC, приведённое хабом),
|
||||
`created` (время Jira как есть), `author`, `field` (логическое имя), `field_id`, `field_name`, `from_value`,
|
||||
`from_string`, `to_value`, `to_string`, `fetched_at`; PK `(history_id, seq)`, индексы по `at` и по `(key, at)`.
|
||||
Записывать в `issue_change` SHALL только приём журнала от агента (`POST /agent/history`); повторно присланная строка с
|
||||
тем же `(history_id, seq)` MUST NOT менять сохранённую. HTTP API интерфейсов и MCP MUST NOT писать в `issue_change`.
|
||||
|
||||
Логическое имя `field` SHALL вычисляться хабом при приёме: `status`, `description`, `summary`, `assignee`, `priority`,
|
||||
`duedate`, `labels`, `resolution` — если так называется `field_id`, а при его отсутствии — `field_name`; `planned_start`,
|
||||
`planned_end` — если `field_id` равен id этого поля в настройке `field_ids`; `epic_link` — если `field_id` равен
|
||||
`epic_link.field_id`; иначе — `field_name` как есть.
|
||||
|
||||
Время Jira (`created` журнала, `created` комментария) SHALL приводиться к UTC разбором ISO-8601 со смещением вида
|
||||
`+0300`, `+03:00` или `Z`, с дробной частью секунд или без; время без смещения считается UTC; неразбираемое — `NULL` в
|
||||
приведённой колонке (строка журнала с `at = NULL` в ленту не попадает).
|
||||
|
||||
Таблица `issue_comment` SHALL хранить также `created_at` — `created`, приведённое к UTC, заполняемое при каждой записи
|
||||
комментариев.
|
||||
|
||||
#### Scenario: Повтор записи журнала
|
||||
- **WHEN** агент дважды прислал элемент `{history_id: "5001", seq: 0, key: X, field_name: "status", …}`
|
||||
- **THEN** в `issue_change` одна строка `(5001, 0)`; второй приём ничего не изменил
|
||||
|
||||
#### Scenario: Плановая дата по id поля
|
||||
- **WHEN** в настройках `field_ids.planned_end = "customfield_10201"`, агент прислал элемент с `field_id =
|
||||
"customfield_10201"`, `field_name = "Плановое окончание"`
|
||||
- **THEN** у строки `field = "planned_end"`
|
||||
|
||||
#### Scenario: Неизвестное поле
|
||||
- **WHEN** агент прислал элемент с `field_id = null`, `field_name = "Fix Version"`
|
||||
- **THEN** у строки `field = "Fix Version"`
|
||||
|
||||
#### Scenario: Время со смещением
|
||||
- **WHEN** элемент журнала с `created = "2026-10-22T09:00:00.000+0300"`
|
||||
- **THEN** у строки `at = "2026-10-22T06:00:00+00:00"`, `created` — как прислано
|
||||
|
||||
### Requirement: Отметка журнала в журнале синхронизаций
|
||||
Таблица `sync_run` SHALL хранить `history_at` — время, когда хаб принял итог журнала по этому прогону (`NULL` — журнал
|
||||
по прогону не принят). Хаб SHALL проставлять `history_at` при отчёте полного прогона (`refresh` по базовой JQL) со
|
||||
статусом `done` и `result.history` без `error`; у прочих `refresh`, при `failed`, `cancelled`, без `result.history` или с
|
||||
`history.error` — оставлять `NULL`. `history.backfill_error` и непустой `history.incomplete` отметку не держат.
|
||||
|
||||
#### Scenario: Журнал принят
|
||||
- **WHEN** `refresh` по базовой JQL завершился `done` с `result.history = {issues: 3, changes: 7, incomplete: []}`
|
||||
- **THEN** у его `sync_run` проставлен `history_at`
|
||||
|
||||
#### Scenario: Ошибка стартовой загрузки не держит отметку
|
||||
- **WHEN** `refresh` по базовой JQL завершился `done` с `result.history = {issues: 1, changes: 2, incomplete: [],
|
||||
backfill_error: "HTTP 400 …"}`
|
||||
- **THEN** у его `sync_run` проставлен `history_at`
|
||||
|
||||
#### Scenario: Ошибка журнала
|
||||
- **WHEN** `refresh` завершился `done` с `result.history = {error: "HTTP 500 …"}`
|
||||
- **THEN** у его `sync_run` `history_at = NULL`
|
||||
|
||||
#### Scenario: Старый агент
|
||||
- **WHEN** агент v12 завершил `refresh` `done` без `result.history`
|
||||
- **THEN** у его `sync_run` `history_at = NULL`
|
||||
|
||||
@ -460,3 +460,82 @@ sha256 (без расширения и имени вложения: имя за
|
||||
#### Scenario: Без названия
|
||||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: " "}`
|
||||
- **THEN** 422 с указанием поля `name`
|
||||
|
||||
### 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), запрос с `since` 10: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`), вторым — смена статуса
|
||||
|
||||
@ -135,3 +135,48 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
|
||||
#### Scenario: Опечатка в проекте
|
||||
- **WHEN** MCP запущен с `--project STOKS`, такого проекта нет; вызван `issues`
|
||||
- **THEN** ответ «проект STOKS не заведён в хабе: …»
|
||||
|
||||
### Requirement: Лента изменений в MCP
|
||||
Инструмент `changes` SHALL отдавать ленту изменений (`GET /changes`) с параметрами `hours` (число > 0), `since`,
|
||||
`until`, `kinds`, `key`. `hours` и `since` вместе — ответ «либо hours, либо since». Без `hours` и `since` — с начала
|
||||
текущих суток по местному времени машины MCP-сервера; с `hours` — `since = сейчас − hours`. `since`/`until` без
|
||||
смещения считаются местным временем машины MCP-сервера (хаб время без смещения отвергает). В сессии, привязанной к
|
||||
проекту P, без `key` — `project = P`; с `key` — без проекта. Без привязки и без `key` — ответ «сессия не привязана к
|
||||
проекту: запустите MCP с --project <метка> (список — projects)», в хаб не ходить; с `key` — работает.
|
||||
|
||||
Вид ответа: первая строка — период по местному времени («изменения с 22.10 00:00 по 22.10 14:05»); затем события,
|
||||
сгруппированные по задаче; задачи — по времени последнего события, новые сверху; у задачи строка «КЛЮЧ тема (до 80
|
||||
символов) — текущий статус», под ней события по времени, новые сверху, каждое — «ЧЧ:ММ автор: <что>», с датой «ДД.ММ»
|
||||
перед временем, если событие не в текущих сутках. `<что>`: статус — «статус «было» → «стало»»; описание — «описание
|
||||
изменено»; срок и плановые даты — «срок / плановое начало / плановое окончание ДД.ММ → ДД.ММ» (дата — из начала
|
||||
`from_value`/`to_value` вида `ГГГГ-ММ-ДД`, иначе `from`/`to` как есть; пустое значение — «—»);
|
||||
комментарий — «комментарий: » и первые 200 символов текста с «…»; прочие поля — «<поле>: «было» → «стало»» (значения до
|
||||
80 символов с «…»). Событий нет — «за период изменений нет». `truncated` — последняя строка «показаны последние N
|
||||
событий, сузьте период». Ошибка 422 хаба — её текст.
|
||||
|
||||
Инструмент `issue` SHALL после комментариев выводить «изменения (N):» и 5 последних элементов `changes` задачи в том же
|
||||
виде событий (с датой), «ещё M», если их больше 5; при `changes = []` — строки нет.
|
||||
|
||||
Инструмент `sync_log` SHALL выводить у каждого прогона «журнал: принят ДД.ММ ЧЧ:ММ» (по `history_at`, местное время)
|
||||
или «журнал: нет».
|
||||
|
||||
#### Scenario: Что закрылось сегодня
|
||||
- **WHEN** сессия `AWPCA`, `changes {kinds: "status"}` в 14:05; сегодня X переведена в «Готово» в 11:15
|
||||
- **THEN** ответ содержит строку X с темой и статусом «Готово» и «11:15 Петров Пётр: статус «В работе» → «Готово»»
|
||||
|
||||
#### Scenario: За два часа
|
||||
- **WHEN** сессия `AWPCA`, `changes {hours: 2}`
|
||||
- **THEN** запрос `GET /changes?project=AWPCA&since=<сейчас − 2 ч>`; в ответе события статусов, описаний, сроков и
|
||||
комментариев
|
||||
|
||||
#### Scenario: Без привязки
|
||||
- **WHEN** MCP без `--project`, `changes {}`
|
||||
- **THEN** ответ «сессия не привязана к проекту: …», запросов к `GET /changes` нет
|
||||
|
||||
#### Scenario: Пустой период
|
||||
- **WHEN** `changes {hours: 1}`, событий нет
|
||||
- **THEN** ответ — строка периода и «за период изменений нет»
|
||||
|
||||
#### Scenario: История в карточке
|
||||
- **WHEN** `issue {key: X}`, у X 7 элементов журнала
|
||||
- **THEN** ответ содержит «изменения (7):», 5 последних событий и «ещё 2»
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user