diff --git a/openspec/changes/change-history/.openspec.yaml b/openspec/changes/archive/2026-10-22-change-history/.openspec.yaml similarity index 100% rename from openspec/changes/change-history/.openspec.yaml rename to openspec/changes/archive/2026-10-22-change-history/.openspec.yaml diff --git a/openspec/changes/change-history/design.md b/openspec/changes/archive/2026-10-22-change-history/design.md similarity index 100% rename from openspec/changes/change-history/design.md rename to openspec/changes/archive/2026-10-22-change-history/design.md diff --git a/openspec/changes/change-history/proposal.md b/openspec/changes/archive/2026-10-22-change-history/proposal.md similarity index 100% rename from openspec/changes/change-history/proposal.md rename to openspec/changes/archive/2026-10-22-change-history/proposal.md diff --git a/openspec/changes/change-history/specs/agent/spec.md b/openspec/changes/archive/2026-10-22-change-history/specs/agent/spec.md similarity index 100% rename from openspec/changes/change-history/specs/agent/spec.md rename to openspec/changes/archive/2026-10-22-change-history/specs/agent/spec.md diff --git a/openspec/changes/change-history/specs/commands/spec.md b/openspec/changes/archive/2026-10-22-change-history/specs/commands/spec.md similarity index 100% rename from openspec/changes/change-history/specs/commands/spec.md rename to openspec/changes/archive/2026-10-22-change-history/specs/commands/spec.md diff --git a/openspec/changes/change-history/specs/data-model/spec.md b/openspec/changes/archive/2026-10-22-change-history/specs/data-model/spec.md similarity index 100% rename from openspec/changes/change-history/specs/data-model/spec.md rename to openspec/changes/archive/2026-10-22-change-history/specs/data-model/spec.md diff --git a/openspec/changes/change-history/specs/hub-api/spec.md b/openspec/changes/archive/2026-10-22-change-history/specs/hub-api/spec.md similarity index 100% rename from openspec/changes/change-history/specs/hub-api/spec.md rename to openspec/changes/archive/2026-10-22-change-history/specs/hub-api/spec.md diff --git a/openspec/changes/change-history/specs/mcp/spec.md b/openspec/changes/archive/2026-10-22-change-history/specs/mcp/spec.md similarity index 100% rename from openspec/changes/change-history/specs/mcp/spec.md rename to openspec/changes/archive/2026-10-22-change-history/specs/mcp/spec.md diff --git a/openspec/specs/agent/spec.md b/openspec/specs/agent/spec.md index f93d996..9d356a9 100644 --- a/openspec/specs/agent/spec.md +++ b/openspec/specs/agent/spec.md @@ -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` diff --git a/openspec/specs/commands/spec.md b/openspec/specs/commands/spec.md index b748a58..c270f95 100644 --- a/openspec/specs/commands/spec.md +++ b/openspec/specs/commands/spec.md @@ -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: }`; +- `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` diff --git a/openspec/specs/data-model/spec.md b/openspec/specs/data-model/spec.md index 31aa820..a8f0681 100644 --- a/openspec/specs/data-model/spec.md +++ b/openspec/specs/data-model/spec.md @@ -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 = ` или `… labels in (, …)` и завести проекты с этими метками (название проекта @@ -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` diff --git a/openspec/specs/hub-api/spec.md b/openspec/specs/hub-api/spec.md index bcf3a04..49a9891 100644 --- a/openspec/specs/hub-api/spec.md +++ b/openspec/specs/hub-api/spec.md @@ -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`), вторым — смена статуса diff --git a/openspec/specs/mcp/spec.md b/openspec/specs/mcp/spec.md index 8668e0d..5a498b9 100644 --- a/openspec/specs/mcp/spec.md +++ b/openspec/specs/mcp/spec.md @@ -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»