234 lines
33 KiB
Markdown
234 lines
33 KiB
Markdown
## Context
|
||
|
||
`main` 7911dc7, схема v4, агент v12. Устройство, на которое опираются правки:
|
||
|
||
- **Зеркало** — `mirror.apply_state` делает upsert строки `issue` (прежние значения теряются) и заменяет комментарии
|
||
задачи присланными (`issue_comment`, время `created`/`updated` — строка Jira как есть, например
|
||
`2026-10-22T13:05:11.000+0300`). Поле `jira_updated` (Jira `updated`) уже приходит в состоянии.
|
||
- **Полный прогон** — `refresh {jql: base_jql}` (`queue.is_full_refresh`); его ставят автообновление
|
||
(`queue.maybe_auto_refresh`, по умолчанию раз в 60 минут, `auto_refresh_minutes`), `POST /refresh {full}` и MCP
|
||
`refresh full`. Хвост пакета — `refresh {keys}` по ключам пакета. `base_jql` вычисляется из проектов и меняется при
|
||
создании проекта.
|
||
- **Выдача** — `queue.deliver_params` дописывает параметры при выдаче (у `refresh` — `boards`), в базе `params` без них.
|
||
`report_result` по `refresh` пишет `sync_run.finished_at` и типы досок из `result.boards`.
|
||
- **Агент** (`jira-agent/src/agent.py`, v12, Python 3.9 stdlib): `do_refresh` → `search_all(jql)` с полями зеркала
|
||
(включая `updated`), дочитывание соседей (`search_tolerant`, `key in (...)`), имена спринтов, отправка
|
||
`POST /agent/state` пачками по 100. Неизвестные параметры команды агент игнорирует (`p.get`).
|
||
- **Спринт в зеркале** — имя активного или будущего спринта доски проекта; у задач закрытых спринтов `sprint = null`.
|
||
Активный и будущий в зеркале не различаются.
|
||
- **MCP** — `mcp_server.py` ходит только через `client`; `issue` форматирует `GET /issues/{key}` (комментарии — 5
|
||
последних).
|
||
- **Время** — в базе ISO-8601 UTC (`db.now()`), кроме дат без времени и времени комментариев.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:** журнал изменений задач из Jira в хабе; лента событий за период в API и MCP; история задачи в
|
||
`GET /issues/{key}` и MCP `issue`; стартовая загрузка по незакрытым задачам текущего спринта; регулярная загрузка только
|
||
по изменившимся задачам; ни одно изменение не теряется молча.
|
||
|
||
**Non-Goals и Anti-Goals:** см. proposal.md.
|
||
|
||
## Decisions
|
||
|
||
1. **Таблица `issue_change`, схема v5.** Колонки: `history_id` (TEXT, id записи журнала Jira), `seq` (INTEGER,
|
||
порядковый номер поля в записи, с 0), `key`, `at` (ISO-8601 UTC — время записи, приведённое хабом), `created` (время
|
||
Jira как есть), `author` (отображаемое имя, иначе логин, иначе `NULL`), `field` (логическое имя, см. решение 3),
|
||
`field_id` (id поля Jira или `NULL`), `field_name` (имя поля Jira как есть), `from_value`, `from_string`, `to_value`,
|
||
`to_string` (как есть, могут быть `NULL`), `fetched_at`. PK `(history_id, seq)`; индексы `(at)` и `(key, at)`.
|
||
Запись — `INSERT OR IGNORE`: повтор записи журнала ничего не меняет. Пишет таблицу только приём журнала от агента;
|
||
API и MCP — только читают (как зеркало).
|
||
*Почему по строке на поле, а не на запись журнала:* лента фильтрует по виду поля, одна запись Jira часто меняет
|
||
статус и резолюцию разом.
|
||
2. **Время комментариев в UTC.** Колонка `issue_comment.created_at` (ISO-8601 UTC, приведённое из `created`; не
|
||
разобралось — `NULL`), заполняется в `apply_state`; миграция v4 → v5 заполняет её у имеющихся строк. Строка `created`
|
||
остаётся как есть (её показывают интерфейсы). Время журнала приводится так же (`at`). Разбор: ISO-8601 со смещением
|
||
`+0300` / `+03:00` / `Z`, с дробной частью или без; без смещения — UTC.
|
||
3. **Логическое имя поля.** Хаб вычисляет `field` при приёме: `status`, `description`, `summary`, `assignee`,
|
||
`priority`, `duedate`, `labels`, `resolution` — по `field_id`, а без него — по `field_name` (встроенные поля Jira
|
||
называются так же); `planned_start`, `planned_end`, `epic_link` — по `field_id`, совпавшему с `field_ids` /
|
||
`epic_link.field_id` настроек; остальное — `field_name` как есть. Хаб, а не агент: соответствие логических имён и id
|
||
полей уже живёт в настройках хаба, агент шлёт сырые данные.
|
||
4. **Виды событий ленты.** `status` — поле `status`; `description` — поле `description`; `dates` — поля `duedate`,
|
||
`planned_start`, `planned_end`; `comment` — комментарий с `created_at` в окне (из `issue_comment`, не из журнала);
|
||
`other` — прочие поля журнала. По умолчанию лента отдаёт `status`, `description`, `dates`, `comment`; `other` — только
|
||
по запросу (`kinds=all` или явно). Остальные поля журнала сохраняются всегда — приходят бесплатно, понадобятся вебу.
|
||
5. **Отметка «с какого момента» — из `sync_run`, без новой настройки.** Колонка `sync_run.history_at` (время приёма
|
||
итога журнала, `NULL` — журнал по прогону не принят). Хаб проставляет её только у полного прогона (`refresh` по
|
||
базовой JQL), завершившегося `done` с `result.history` без `error` (решение 11). Отметка — `started_at` последнего
|
||
`sync_run` с `history_at IS NOT NULL` **независимо от `query`** (новый проект не обнуляет отметку прежних). Параметр
|
||
`history` при выдаче `refresh`:
|
||
- полный прогон, отметка есть — `{since: <отметка − 15 минут>}` (UTC, ISO-8601 с `Z`); если `query` прогона-отметки
|
||
не равна текущей `base_jql` (после него заведён проект) — ещё и `jql` стартовой загрузки (решение 6): прежние
|
||
проекты дочитываются от отметки, новый получает стартовую историю;
|
||
- полный прогон, отметки нет — `{jql: <стартовая>}`;
|
||
- `refresh {keys}` и `refresh` по небазовой JQL — `{since}`, если отметка есть, иначе `history` не дописывается
|
||
(стартовую загрузку делает только полный прогон).
|
||
Запас 15 минут покрывает расхождение часов Mac mini и сервера Jira и время самого прогона; повторы отбрасываются по
|
||
PK. Отметка сдвигается **только** полным прогоном с принятым журналом: прогон упал, регулярная часть журнала не
|
||
дочитана, агент старый — следующий прогон возьмёт прежнюю отметку и дочитает пропущенное (анти-цель «молча теряет»).
|
||
*Отвергнуто:* отдельная настройка-отметка (её правят руками и через `settings_set`, сдвиг надо делать атомарно с
|
||
итогом прогона); сравнение `jira_updated` в хабе с ответом агенту списком изменившихся (хаб уже перезаписал строку, если
|
||
журнал потом не дочитан — изменение потеряно); отметка по `query` = текущая `base_jql` (после нового проекта прежние
|
||
проекты теряли изменения вне спринта между отметкой и прогоном — находка рецензента).
|
||
6. **Стартовая загрузка.** `history.jql = (<base_jql>) AND sprint in openSprints() AND statusCategory != Done`. Агент
|
||
тянет журнал этих задач целиком (без отсечения по времени). Решение владельца: «максимум незакрытые в текущем
|
||
спринте». `openSprints()` — активные спринты, будущие не входят. Категория статуса, а не настройка `closed_statuses`:
|
||
статус из настройки, которого нет в Jira, даёт 400 на всю JQL, и стартовая загрузка не прошла бы никогда. Стартовая
|
||
загрузка — разовая и «по возможности»: её ошибка не мешает отметке (решение 11) и не повторяется. Проект на
|
||
канбан-доске без спринтов в стартовую загрузку не попадает — его история начнётся с первого регулярного прогона.
|
||
7. **Агент v13: регулярный режим (`history.since`).** После отправки состояния (все пачки `POST /agent/state`) агент
|
||
отбирает из выгруженных задач (база и соседи) те, у кого Jira `updated >= since` (сравнение моментов времени с
|
||
учётом смещения), и по ним делает `search` `jql = key in (...)`, `fields = key`, `expand = changelog`, по 100 ключей.
|
||
Из журнала каждой задачи берёт записи с `created >= since`. Изменившихся задач нет — ни одного лишнего вызова Jira.
|
||
Решение владельца: без второго «слепого» запроса по `updated >= …`, отбор по уже выгруженному.
|
||
8. **Агент v13: неполный журнал.** Если у задачи `changelog.total` больше числа записей в ответе `search`, агент дочитывает
|
||
журнал `GET /rest/api/2/issue/{key}?fields=key&expand=changelog`; если и там неполный — передаёт что есть, пишет в
|
||
лог (ключ, сколько получено из скольких) и добавляет ключ в `result.history.incomplete`. Отметку неполный журнал не
|
||
держит: если Jira обрезает журнал всегда, удержание остановило бы ленту навсегда; неполнота видна в результате
|
||
команды. Jira Server/DC, по ожиданию, отдаёт журнал целиком; проверить на живом агенте (план, живая проверка).
|
||
Элемент, который хаб отверг бы (пустые `created` или `field_name`, запись без `id`), агент отбрасывает с записью в
|
||
лог — иначе один кривой элемент давал бы 422 на пачку в каждом прогоне и отметка не сдвинулась бы никогда.
|
||
9. **Агент v13: id поля.** `field_id` — `fieldId` элемента журнала, если Jira его отдаёт; иначе для `fieldtype = custom`
|
||
агент ищет id по имени поля в `GET /rest/api/2/field` (один вызов на `refresh`, только если такой элемент встретился);
|
||
для `fieldtype = jira` — `field` как есть. Не нашлось — `null`.
|
||
10. **Приём журнала — `POST /agent/history`.** Тело `{command_id, changes: [...]}`, элемент — `{history_id, seq, key,
|
||
created, author, field_id, field_name, from, from_string, to, to_string}`. Агент шлёт пачками по 500 элементов;
|
||
хаб проверяет форму всей пачки до записи (`history_id`, `key`, `created`, `field_name` — непустые строки, `seq` —
|
||
целое ≥ 0, остальные — строка или `null`; нарушение — 422 `bad_history` с номером элемента и полем, пачка не
|
||
применяется) и пишет `INSERT OR IGNORE`. Ответ `{accepted, duplicates}`. Отдельный маршрут, а не поле в
|
||
`/agent/state`: журнал тянется после состояния, у него свои пачки, контракт состояния не меняется.
|
||
11. **Итог журнала — в результате `refresh`.** `result.history = {issues: <задач с журналом>, changes: <элементов
|
||
отправлено>, incomplete: [ключи с неполным журналом]}` плюс, при ошибке, `error` (регулярная часть, `since`) или
|
||
`backfill_error` (стартовая загрузка, `jql`). Ответ не 2xx от Jira или хаба на шаге журнала (`HttpError`, у хаба — с
|
||
префиксом «хаб: ») не роняет `refresh`: агент сразу, без повтора команды, возвращает `done` с текстом ошибки — зеркало
|
||
уже обновлено, ломать и перекачивать его ради журнала нельзя (анти-цель); повторит следующий прогон, отметка ведь
|
||
не сдвинулась. Обрыв связи с Jira или хабом (`ConnectionError`) на шаге журнала — как в остальном `refresh` (повтор,
|
||
затем `failed`). Без `history` в параметрах агент журнал не читает и ключа `history` в результате нет. Хаб ставит
|
||
`sync_run.history_at` при `done` полного прогона и `history` без `error`; `backfill_error` и `incomplete` отметку не
|
||
держат (стартовая загрузка разовая — решение 6; неполнота — решение 8).
|
||
11a. **Журнал в `sync_log`.** `GET /sync-runs` отдаёт `history_at`; MCP `sync_log` выводит у прогона «журнал: принят
|
||
<время>» или «журнал: нет». Текст ошибки журнала — в результате команды (`batch_status`).
|
||
12. **Лента — `GET /changes`.** Параметры: `since`, `until` (ISO-8601 со смещением или `Z`; без `until` — сейчас; без
|
||
`since` — `until − 24 ч`), `project` (только свои задачи проекта; соседи — нет: в ленте проекта чужие задачи —
|
||
шум), `key` (одна задача, без ограничения проектом), `kinds` (через запятую из `status`, `description`, `dates`,
|
||
`comment`, `other`, или `all`; по умолчанию все, кроме `other`), `limit` (по умолчанию 500, до 2000). Ответ
|
||
`{since, until, changes: [...], truncated}`, элемент — `{at, key, summary, status, kind, field, author, from, to,
|
||
from_value, to_value}` (`from`/`to` — `from_string`/`to_string`, а если они `null` — `from_value`/`to_value`;
|
||
`from_value`/`to_value` — сырые значения журнала: у дат Jira строка журнала может быть локализованной, а значение —
|
||
ISO, им пользуется MCP; у комментария `from = null`, `to` — текст, `field = "comment"`, `*_value = null`); порядок — `at` по убыванию, затем `key`, `history_id`, `seq`. Неизвестный проект
|
||
— 404, неверное время или вид — 422 `bad_request` с именем параметра.
|
||
13. **История задачи — в `GET /issues/{key}`.** Поле `changes` — все элементы журнала задачи (все виды, включая `other`),
|
||
в формате элемента ленты без `summary`/`status`, новые сверху. Комментарии там уже есть отдельно.
|
||
14. **MCP `changes`.** Параметры `hours` (число), `since`, `until`, `kinds`, `key`. Без `hours` и `since` — с начала
|
||
сегодняшнего дня по местному времени машины MCP. Привязка: в сессии проекта P — `project = P`; с `key` — без
|
||
проекта; без привязки и без `key` — ответ «сессия не привязана к проекту…» (как `issues`). Вид ответа — по задачам
|
||
(задачи — по времени последнего события, новые сверху): строка «КЛЮЧ тема — статус», под ней события по времени
|
||
(«ЧЧ:ММ автор: статус «В работе» → «Готово»»; дата перед временем, если событие не сегодня; описание — «описание
|
||
изменено» без текста; срок — «срок 25.10 → 30.10» (дата — из начала `*_value` вида `ГГГГ-ММ-ДД`, иначе строка как
|
||
есть); комментарий — первые 200 символов с «…»). Пусто — «за период
|
||
изменений нет». `truncated` — строка «показаны последние N событий, сузьте период».
|
||
15. **MCP `issue`.** После комментариев — «изменения (N):» и 5 последних элементов `changes` в том же виде, «ещё M», если
|
||
их больше; без журнала — строки нет (у старых задач журнала может не быть).
|
||
16. **Миграция v4 → v5.** В той же транзакции с копией базы, что и прежние: `issue_change` и индексы создаёт `SCHEMA`;
|
||
шаг — `ALTER TABLE issue_comment ADD COLUMN created_at TEXT`, `ALTER TABLE sync_run ADD COLUMN history_at TEXT`,
|
||
заполнение `issue_comment.created_at` разбором `created` (Python-шаг, как `migrate_v3`). У имеющихся `sync_run`
|
||
`history_at = NULL` → первый `refresh` после выкатки хаба — стартовая загрузка.
|
||
17. **Порядок выкатки.** Хаб можно выкатить раньше агента: агент v12 игнорирует `history`, результат без `history` —
|
||
отметка не сдвигается, `history_at` пуст, каждый полный `refresh` выдаётся со стартовой JQL, но агент её не исполняет.
|
||
После публикации v13 первый полный прогон делает стартовую загрузку. Агент v13 со старым хабом (без `history` в
|
||
параметрах) журнал не читает.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **Объём стартовой загрузки.** Журнал задач открытого спринта целиком; у задач с длинной историей описания — большие
|
||
тексты (`from_string`/`to_string` описаний), идут через общую папку. Пачки по 500 элементов; если пачка окажется
|
||
слишком тяжёлой для реле — уменьшить в агенте.
|
||
- **Расхождение часов > 15 минут** между Mac mini и сервером Jira даёт пропуски. Цена — потеря событий без сигнала;
|
||
принимаем, часы синхронизируются по NTP.
|
||
- **Удалённый комментарий** исчезает из ленты задним числом (лента читает `issue_comment`).
|
||
- **Журнал соседей.** Соседи, уже известные хабу, полным прогоном не перечитываются (поведение `refresh` v12), поэтому
|
||
их изменения попадают в журнал только при точечном обновлении. Принимаем: лента проекта показывает свои задачи.
|
||
|
||
## Migration Plan
|
||
|
||
1. PR хаба: схема v5, приём журнала, `history` в выдаче `refresh`, `GET /changes`, `changes` в `GET /issues/{key}`,
|
||
MCP. После merge — `git pull`, перезапуск хаба владельцем, `/mcp` → reconnect.
|
||
2. PR агента: v13. После merge — `jira-hub agent publish ../jira-agent/src/agent.py`; `agent_status` показывает v13;
|
||
`min_agent_version` не поднимать (хаб работает и со старым агентом).
|
||
3. Живая проверка: полный `refresh` (выдан с `history.jql`) → в `result.history` элементы, в ленте события задач
|
||
спринта; следующий полный `refresh` (выдан с `history.since`) → лишних вызовов Jira нет при отсутствии изменений; правка задачи в Jira → событие в
|
||
`changes` с автором и временем; лог агента — нет строк о неполном журнале (иначе — решение 8 под пересмотр).
|
||
|
||
Откат: агент — публикация v12 невозможна (только вверх), выкатывается v14 без журнала; хаб — предыдущий коммит,
|
||
таблица и колонки v5 остаются и не мешают (старый код их не читает), `schema_version = 5` старый хаб не понизит.
|
||
|
||
## Open Questions
|
||
|
||
Нет.
|
||
|
||
## Decisions made without discussion
|
||
|
||
1. 2026-10-22 — **строка журнала — одно поле одной записи Jira (`issue_change`), PK `(history_id, seq)`.** Обоснование:
|
||
фильтр ленты по виду поля; идемпотентность по id Jira. Цена ошибки: если `seq` у одной записи Jira окажется
|
||
неустойчивым между запросами, возможны дубли полей одной записи — видно в ленте, лечится сменой ключа на
|
||
`(history_id, field_name, to_value)`.
|
||
2. 2026-10-22 — **сохраняются все поля журнала, лента по умолчанию показывает четыре вида.** Обоснование: данные
|
||
приходят бесплатно, веб и история задачи покажут больше. Цена ошибки: рост базы — текстами описаний.
|
||
3. 2026-10-22 — **отметка «с какого момента» — из `sync_run.history_at` любого полного прогона с принятым журналом
|
||
(без сравнения `query`), сдвигается только им; запас 15 минут; после нового проекта — `since` и `jql` вместе;
|
||
неполные `refresh` без отметки журнала не получают.** Обоснование: анти-цель «молча теряет» (находки рецензента:
|
||
новый проект не должен обнулять отметку прежних; хвост пакета не должен тянуть стартовую загрузку). Цена ошибки:
|
||
при расхождении часов > 15 минут — пропуски; агенту два режима в одном `refresh`.
|
||
4. 2026-10-22 — **стартовая загрузка — JQL `sprint in openSprints() AND statusCategory != Done`, журнал целиком,
|
||
разово.** Обоснование: формулировка владельца; активный спринт в зеркале не отличим от будущего; `closed_statuses`
|
||
со статусом, которого нет в Jira, дал бы 400 навсегда. Цена ошибки: статус с категорией не Done, который владелец
|
||
считает закрытым, попадёт в стартовую загрузку (лишний журнал); канбан-проекты без стартовой истории; упавшая
|
||
стартовая загрузка не повторяется (видно в `backfill_error`).
|
||
5. 2026-10-22 — **ошибка Jira или хаба (не 2xx) при чтении журнала не роняет `refresh` и не повторяет команду**
|
||
(`done` с `history.error`, отметка не сдвигается). Обоснование: зеркало важнее ленты; повтор команды перекачал бы
|
||
состояние ради журнала; следующий прогон дочитает. Цена ошибки: при разовом 5xx лента отстаёт на один интервал
|
||
автообновления.
|
||
6. 2026-10-22 — **логическое имя поля вычисляет хаб, агент шлёт `field_id` и `field_name`.** Обоснование: соответствие id
|
||
полей — в настройках хаба. Цена ошибки: если Jira не отдаёт `fieldId` и агент не нашёл id по имени — плановые даты
|
||
попадут в `other`; видно на живой проверке.
|
||
7. 2026-10-22 — **комментарии в ленте — из `issue_comment` по новой колонке `created_at` (UTC), а не событиями
|
||
журнала.** Обоснование: комментариев нет в журнале Jira; они уже приходят с автором и временем. Цена ошибки:
|
||
удалённый в Jira комментарий пропадает из ленты.
|
||
8. 2026-10-22 — **лента проекта — только свои задачи, без соседей.** Обоснование: соседи — задачи других меток, в ленте
|
||
проекта это шум. Цена ошибки: изменение блокирующей задачи другого проекта не видно в ленте (видно через `key`).
|
||
9. 2026-10-22 — **MCP `changes` по умолчанию — с начала сегодняшнего дня по местному времени.** Обоснование: «что
|
||
закрылось сегодня» — первый сценарий владельца. Цена ошибки: лишний параметр в вопросе «за 2 часа» (`hours: 2`).
|
||
10. 2026-10-22 — **история задачи — полем `changes` в `GET /issues/{key}`, а не отдельным маршрутом.** Обоснование: так
|
||
уже отдаются комментарии; карточка и MCP `issue` получают всё одним запросом. Цена ошибки: тяжелее ответ карточки
|
||
у задач с длинной историей.
|
||
11. 2026-10-22 — **событие «задача создана» не входит в ленту.** Обоснование: владелец его не называл, в журнале Jira его
|
||
нет. Цена ошибки: новые задачи дня не видны в ленте; добавляется по `issue.created` без агента.
|
||
12. 2026-10-22 — **журнал приёмом `POST /agent/history` пачками по 500 элементов; ответ `{accepted, duplicates}`.**
|
||
Обоснование: не менять контракт `/agent/state`; размер пачки — по опыту реле с состоянием. Цена ошибки: пачки
|
||
описаний тяжелее пачек состояния — при сбоях реле уменьшить.
|
||
13. 2026-10-22 — **неполный журнал задачи не держит отметку; ключи — в `result.history.incomplete` и в логе.**
|
||
Обоснование: если Jira обрезает журнал всегда, удержание остановило бы ленту навсегда. Цена ошибки: потерянные
|
||
старые записи у задач с очень длинной историей; видно в результате команды и на живой проверке.
|
||
14. 2026-10-22 — **агент отбрасывает элементы, которые хаб отверг бы** (пустые `created`, `field_name`, нет `id`), с
|
||
записью в лог. Обоснование: один кривой элемент иначе давал бы 422 на пачку в каждом прогоне. Цена ошибки: такой
|
||
элемент не попадёт в ленту.
|
||
15. 2026-10-22 — **элемент ленты несёт и строки, и сырые значения (`from_value`/`to_value`); MCP показывает даты по
|
||
значению.** Обоснование: строка журнала у дат Jira может быть локализованной. Цена ошибки: ответ чуть тяжелее.
|
||
16. 2026-10-22 — **`sync_log` показывает, принят ли журнал прогона (`history_at`).** Обоснование: дизайн обещал видимость
|
||
ошибок журнала; текст ошибки — в результате команды. Цена ошибки: одна колонка и строка в MCP.
|
||
17. 2026-10-22 — **R-1: шаги миграции v4 → v5 добавляют колонку, только если её нет (`_add_column`).** Обоснование:
|
||
базы v1/v2 получают `issue_comment` из `SCHEMA` уже с `created_at`, голый `ALTER` падал бы «duplicate column».
|
||
Цена ошибки: нет — для базы v4 поведение то же.
|
||
18. 2026-10-22 — **R-2: прежний тест формата `sync_log` переписан на строку с «, журнал: нет».** Обоснование: спека
|
||
`mcp` добавляет журнал к каждой строке. Цена ошибки: нет.
|
||
19. 2026-10-22 — **R-7 (по I-1 финального ревью): агент выполняет стартовую часть журнала первой, регулярная не
|
||
запрашивает уже прочитанные задачи; задача помечается прочитанной только после разбора её журнала.** Обоснование:
|
||
при `history = {since, jql}` задача из обеих выборок теряла записи старше `since` (их отсекала регулярная часть, а
|
||
стартовая её пропускала), хотя полный журнал уже был получен; спека `agent` уточнена. Цена ошибки: нет лишних
|
||
вызовов Jira; если стартовая часть упала на задаче, её добирает регулярная (только записи после `since`).
|
||
20. 2026-10-22 — **R-8 (по M-1/M-2 финального ревью, подняты до Important по эффекту): время за краем диапазона
|
||
`datetime` — «не разобралось» (`None`), а не исключение; `GET /changes` отвечает 422 на время без смещения, MCP
|
||
`changes` считает такое время местным.** Обоснование: исключение роняло миграцию (хаб не стартует) или навсегда
|
||
стопорило отметку журнала; время без смещения из чата молча сдвигало окно ленты на 3 часа. Цена ошибки: запрос API
|
||
без смещения теперь 422 — интерфейсов, которые так шлют, нет.
|