## ADDED Requirements ### 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`; без `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`), вторым — смена статуса