169 lines
22 KiB
Markdown
169 lines
22 KiB
Markdown
## Context
|
||
|
||
`main` 13e6c16. Устройство, на которое опираются правки:
|
||
|
||
- **Агент** (`jira-agent/src/agent.py`, v9): `refresh` читает задачи `search` с `fields = STATE_FIELDS` (+ плановые даты и
|
||
поле эпика), `to_state` собирает словарь задачи, пачки по 100 уходят в `POST /agent/state`. Комментарии агент читает
|
||
только в `ensure_comment` (`GET /issue/{key}/comment`, постранично по 100).
|
||
- **Хаб**: `mirror.apply_state` пишет в `issue` только столбцы `ISSUE_COLUMNS` — лишние поля задачи (в том числе будущее
|
||
`comments`) молча пропускаются, поэтому агент v10 совместим с нынешним хабом. `mirror.get_issue` отдаёт задачу, связи
|
||
(`target_key`/`source_key`, `link_type`), заметку, 10 последних команд. Схема базы — v2 (`db.SCHEMA_VERSION`),
|
||
миграция с копией базы — `db.migrate`. Тело запроса ограничено 16 МБ (`api.MAX_BODY`).
|
||
- **Веб-приложение**: `createBrowserRouter(routes, {basename: "/app"})`; `Layout` с `<Outlet>`; маршруты `board`,
|
||
`issues`, `issues/:key` (страница `IssuePage`). Ссылки на карточку — ключ в `IssuesPage` и «ⓘ» в `board/components/
|
||
Card.tsx`. `NoteForm` — форма заметок с `useBlocker` (смена `pathname` при правках требует подтверждения) и
|
||
`beforeunload`. `useSaveNote` после успеха обновляет кэш задачи и инвалидирует `["issues"]` и `board`.
|
||
Прокрутка доски при возврате на `/app/board` восстанавливается (M-3 #29).
|
||
- **Справочники**: `link_types` настроек — логическое имя → тип Jira (`блокирует начало` → `ASUPR_Блокер_Начала`,
|
||
`блокирует завершение` → `ASUPR_Блокер_Завершения`, `связанные` → `ASUPR_Связанные`); `status_labels` — статус по факту →
|
||
класс (`fact-done`, `fact-cancelled`, …).
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:** карточка открывается окном поверх списка и доски без потери их состояния; по шапке видно состояние задачи,
|
||
по связям — что держит задачу и что ждёт её, с темой и статусом связанных; комментарии задачи из Jira в карточке со
|
||
скрытием; агент v10 и хаб совместимы в любом порядке выкатки.
|
||
|
||
**Non-Goals:** см. proposal.md.
|
||
|
||
## Decisions
|
||
|
||
### Агент v10 (решения агентской части — `jira-agent/openspec/changes/agent-v10-comments/design.md`)
|
||
|
||
1. **Поле `comment` в том же `search`.** `STATE_FIELDS` получает `comment`; новых вызовов Jira на обычную задачу нет.
|
||
Jira Server отдаёт `fields.comment = {comments: [...], total, maxResults, startAt}`.
|
||
2. **Дочитывание.** Если `len(comments) < total`, агент читает комментарии этой задачи `GET /issue/{key}/comment`
|
||
(`startAt`, `maxResults = 100`) до конца и берёт их вместо частичного списка. Вызовы учитываются в темпе и 429 как
|
||
любые вызовы Jira.
|
||
3. **Форма.** `comments: [{id, author, created, updated, body}]` в порядке Jira (старые первыми): `id` — строка,
|
||
`author` — `author.displayName`, иначе `author.name`, иначе `null`; `created`/`updated` — строки Jira как есть;
|
||
`body` — текст как есть (`""`, если в Jira пусто). Поле `comments` есть у каждой задачи, прочитанной агентом v10
|
||
(пустой список — комментариев нет).
|
||
4. **Версия и выкатка.** `AGENT_VERSION = 10`; после merge PR агента — `jira-hub agent publish src/agent.py` (пишет в
|
||
каталог живого хаба: выполняет владелец или агент по его отдельной команде).
|
||
|
||
### Хаб
|
||
|
||
5. **Таблица `issue_comment`** `(key, comment_id, author, created, updated, body, PRIMARY KEY (key, comment_id))`; выборка
|
||
по `key` идёт по первичному ключу, отдельный индекс не нужен. Схема v3; миграция v2 → v3 создаёт таблицу (копия базы `schema-v2` — существующим механизмом).
|
||
6. **Запись.** `apply_state`: для задачи с полем `comments` (список) комментарии этой задачи удаляются и вставляются
|
||
присланные — в одной транзакции с задачей; задача без поля `comments` своих комментариев не теряет (агент до v10).
|
||
7. **Проверка формы** в `h_agent_state` до записи: `comments` — список объектов; `id` — непустая строка; `body` — строка;
|
||
`author`, `created`, `updated` — строка или `null`. Нарушение — 422 `bad_state` с ключом задачи и полем, пачка не
|
||
применяется целиком (агент получает ошибку хаба в отчёте `refresh`).
|
||
8. **`GET /issues/{key}`**: добавляются `comments` (новые сверху: по `created` убыв., затем `comment_id` убыв.),
|
||
`epic_summary` (тема задачи-эпика из зеркала или `null`), у каждой связи — `summary` и `status` связанной задачи
|
||
(`LEFT JOIN issue`, `null`, если её нет в зеркале). Прежние поля и их форма не меняются.
|
||
9. **MCP `issue`**: после заметок — «комментарии (N):» и 5 последних (автор, время, текст до 300 символов с «…»), затем
|
||
«ещё M», если их больше 5; нет комментариев — «комментариев нет».
|
||
|
||
### Веб-приложение: окно
|
||
|
||
10. **Фоновая страница.** Ссылки на карточку (ключ в списке, «ⓘ» на доске, связи в карточке) передают в
|
||
`location.state` `{background, depth, direct}`: `background` — адрес страницы под окном (`pathname + search`),
|
||
`depth` — сколько записей истории занято карточками поверх неё (1 при открытии со страницы, +1 при переходе по
|
||
связи), `direct` — окно открыто прямым адресом (переносится в переходы по связям). Маршрут верхнего уровня —
|
||
`{path: "*", element: <Layout/>}`; `Layout` всегда рисует страницы через `useRoutes(PAGE_ROUTES, pageLocation)` с
|
||
явным адресом: `background` при открытом окне, иначе текущий адрес. Явный адрес нужен всегда: так дерево
|
||
компонентов страницы не меняется при открытии окна (страница не пересоздаётся), а `useSearchParams`/`useLocation`
|
||
внутри фоновой страницы видят её собственный адрес, и фильтры списка не сбрасываются. Состояние страницы (фильтры,
|
||
загруженные порции, прокрутка, раскладка доски, опрос пакета) сохраняется. `ErrorBoundary` страницы получает ключ по
|
||
адресу страницы, у окна — свой.
|
||
11. **Прямой адрес** `/app/issues/{key}` без `state` (новая вкладка, закладка): `Layout` заменяет запись истории тем же
|
||
адресом со `state = {background: /issues, depth: 0, direct: true}`; под окном — список задач без фильтров.
|
||
Перезагрузка страницы с открытым окном сохраняет `state` (браузер хранит его в записи истории).
|
||
12. **Закрытие** (✕, Esc, щелчок по фону): `depth = 0` — `navigate("/issues", {replace: true})`; `depth ≥ 1` —
|
||
`navigate(-depth)`, браузер возвращается к записи страницы под окном, и «назад» после закрытия ведёт на шаг раньше
|
||
неё; если окно было открыто прямым адресом (`direct`), запись прямого адреса, на которую вернулись, заменяется на
|
||
`/issues` (`replace`). Закрытие проходит через блокировщик заметок (п. 14). Вкладки «Доска»/«Задачи» в шапке
|
||
подсвечиваются по адресу страницы под окном.
|
||
13. **Элемент `<dialog>`** с `showModal()`: фокус удерживается в окне, фон недоступен, Esc вызывает `cancel` (обрабатываем
|
||
сами — закрытие по п. 12 с проверкой заметок; если `cancel` не отменяемый — браузер закрыл окно сам без активации
|
||
пользователя, — по событию `close` окно открывается снова и закрытие идёт тем же путём). Щелчок по фону — щелчок по
|
||
самому `<dialog>` вне области содержимого.
|
||
Прокрутка страницы под окном на время открытия блокируется (`overflow: hidden` у `body`), содержимое окна
|
||
прокручивается внутри, шапка окна закреплена. Ширина до 1000 px, высота до 90vh; на экранах уже 700 px — на всю
|
||
ширину.
|
||
14. **Несохранённые заметки.** Подтверждение «Есть несохранённые правки заметки. Уйти без сохранения?» при закрытии окна,
|
||
переходе по связи и уходе в раздел; условие блокировки — смена `pathname` при правках (как сейчас), закрытие по
|
||
п. 12 проходит через тот же блокировщик. Отказ — окно остаётся с введённым текстом.
|
||
15. **Доска под окном.** Сохранение заметки уже инвалидирует `board` и `["issues"]` (`useSaveNote`): фоновая страница
|
||
смонтирована, поэтому перечитывается и показывает новые «вид работы» и «статус по факту». Отдельного кода нет —
|
||
поведение закрепляется тестом.
|
||
|
||
### Веб-приложение: раскладка карточки
|
||
|
||
16. **Шапка**: ключ; статус Jira (плашка); статус по факту (плашка цветом класса из `status_labels`, если есть);
|
||
«Открыть в Jira ↗»; ✕; тема; строка «исполнитель · срок · спринт · эпик: <тема эпика>» — пустые части пропускаются,
|
||
эпик без темы в зеркале — ключом. Срок — `planned_end`, иначе `duedate`; «просрочено» (красным) — срок раньше
|
||
сегодняшней даты и статус по факту не относится к классам `fact-done`, `fact-cancelled`.
|
||
17. **Связи по смыслу.** По `link_types` настроек: входящие `блокирует начало` / `блокирует завершение` — группа «Зависит
|
||
от», исходящие — «Блокирует»; у строки метка «начало» / «завершение». `связанные` в обе стороны — «Связана с».
|
||
Тип, которого нет в `link_types`, — «Прочие» с именем типа Jira и направлением («→» исходящая, «←» входящая).
|
||
Строка: ключ (ссылка — открывает связанную задачу в окне), тема, статус Jira (плашка); связанной задачи нет в
|
||
зеркале — ключ и «нет в зеркале». Пустая группа не показывается; связей нет — «связей нет». Порядок групп: «Зависит
|
||
от», «Блокирует», «Связана с», «Прочие»; внутри — по ключу.
|
||
18. **Описание** — `pre-wrap` без разбора разметки; выше 8 строк сворачивается с кнопкой «развернуть» / «свернуть»
|
||
(кнопка только если текст не помещается). Нет описания — раздела нет.
|
||
19. **Комментарии** — «Комментарии (N)» с кнопкой «Скрыть» / «Показать»; новые сверху; автор, время (`fmtStamp`), текст
|
||
`pre-wrap`. Выбор «скрыто» — один на все карточки, в `localStorage` `jira-hub-comments-hidden` (`"1"` — скрыты);
|
||
недоступный `localStorage` — показаны. Нет комментариев — «комментариев нет» (кнопки нет).
|
||
20. **Заметки** — по умолчанию режим чтения: «Вид работы», «Статус по факту», «Сделано», «Осталось», «Заметка владельца»
|
||
(пустые — «—»), строка «изменено <время>, <автор>»; кнопка «Править» показывает нынешнюю форму `NoteForm`
|
||
(поля, «Сохранить», «Отмена»); после успешного сохранения — снова режим чтения; «Отмена» без правок — сразу, с
|
||
правками — подтверждение «Отменить правки заметки?». Заметки ещё нет — режим чтения с «—» и «Править».
|
||
21. **Прочие поля**: тип, приоритет, автор, метки, плановое начало, обновлена в Jira, «сосед по связям» — непустые.
|
||
22. **Команды моста (N)** — свёрнуты (`<details>`), внутри нынешняя таблица; команд нет — «команд по задаче не было» и
|
||
раздел не сворачивается.
|
||
23. **Колонки**: под шапкой слева — «Связи», «Описание», «Комментарии»; справа (≈320 px) «Заметки», «Прочие
|
||
поля», «Команды моста». Уже 900 px — одна колонка: заметки, связи, описание, комментарии, прочие поля, команды.
|
||
24. **Состояния окна**: загрузка — «загрузка <key>…»; 404 — «Задачи <key> нет в зеркале хаба.» и «Закрыть»; хаб не
|
||
отвечает — сообщение и «Повторить», ранее загруженные данные остаются; поверх ошибки при данных — предупреждение
|
||
в шапке окна.
|
||
|
||
### Проверки
|
||
|
||
25. **Тестовый хаб**: `web/scripts/seed-extras.json` — дополнения к фикстуре доски только для тестового хаба: у AURORA-9001
|
||
описание длиннее 8 строк, три комментария и две команды (пакет автора `seed`, команды завершены). `seed-hub.py`
|
||
применяет их после фикстуры. Связи не добавляются (они меняют доску и её e2e); группа «Прочие» и связь на задачу вне
|
||
зеркала проверяются в vitest, а связь на задачу вне зеркала (`lower-1`) уже есть в фикстуре. Фикстура доски и её
|
||
golden-эталоны не меняются.
|
||
26. **e2e** (Playwright на :8791): открытие из списка (фильтр и прокрутка списка после закрытия на месте), по «ⓘ» с доски
|
||
(прокрутка и раскладка доски на месте), переход по связи и «назад», Esc / ✕ / фон, «назад» после закрытия не
|
||
открывает карточку, прямой адрес и перезагрузка с открытым окном, подтверждение при несохранённой заметке, ширина
|
||
600 px, «Скрыть» комментарии переживает перезагрузку.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **Фоновая страница в `Layout`** — нестандартный для data router приём (две отрисовки маршрутов). Риск: `useBlocker`
|
||
только один на страницу — блокировщик заметок остаётся в окне, фоновая страница блокировщиков не имеет. Проверяется
|
||
тестами п. 14 и e2e.
|
||
- **`navigate(-depth)`** полагается на то, что все `depth` шагов истории — наши переходы. Ручное изменение адреса внутри
|
||
окна сбрасывает `state` → `depth = 0`, закрытие ведёт в список задач (`replace`). Принято.
|
||
- **Объём состояния**: комментарии увеличивают пачку `POST /agent/state`; при 100 задачах и типичных комментариях это
|
||
сотни килобайт при пределе 16 МБ. Принято без разбиения.
|
||
- **Просрочка по статусу по факту** (п. 16): задача, закрытая в Jira без заметки, покажется просроченной. Цена — лишняя
|
||
красная пометка; списка «закрытых» статусов Jira в настройках нет.
|
||
|
||
## Migration Plan
|
||
|
||
1. PR jira-agent (v10) → merge → `jira-hub agent publish` → `agent_status` «обновление: ok v9 → v10».
|
||
2. PR jira-hub (#37) → merge → `git pull --ff-only`, `make web` в основном checkout → перезапуск хаба владельцем
|
||
(`launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub`): миграция v2 → v3 с копией базы → `/mcp` reconnect.
|
||
3. Ближайший `refresh` (час или «Обновить из Jira») заполняет комментарии.
|
||
Откат хаба: копия `backups/<…>-schema-v2.sqlite` и предыдущий коммит; агент v10 со старым хабом работает (п. Context).
|
||
|
||
## Open Questions
|
||
|
||
Нет.
|
||
|
||
## Decisions made without discussion
|
||
|
||
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|
||
|---|---|---|---|---|
|
||
| R-1 | 2026-10-15 | Требования к агенту v10 — дельтой `specs/agent` в этом change jira-hub; в jira-agent — change `agent-v10-comments` только с решениями агентской части (design.md) | Заведённый порядок: спека агента живёт в `jira-hub/openspec/specs/agent/`, так сделаны v9 (hub#30 + agent#24) и предыдущие; «один change на репозиторий» соблюдается — у агента свой change и свой PR | Низкая: перенос дельты в другой change — правка документов |
|
||
| R-2 | 2026-10-15 | Просрочка — срок раньше сегодня и статус по факту не `fact-done`/`fact-cancelled`; статус Jira не учитывается | Списка «закрытых» статусов Jira в настройках нет, а свой словарь в приложении запрещён требованием «Справочники из настроек хаба» | Лишняя красная пометка у закрытой в Jira задачи без заметки |
|
||
| R-3 | 2026-10-15 | В карточке показываются все комментарии задачи, без постраничности; в MCP — 5 последних | В задачах проекта комментариев единицы; окно прокручивается внутри | При десятках длинных комментариев — длинная прокрутка окна; лечится «Скрыть» |
|
||
| R-4 | 2026-10-15 | «Команды моста» свёрнуты всегда при открытии, выбор не запоминается; «Скрыть» комментарии запоминается | Команды нужны редко и по конкретному поводу; комментарии владелец просил скрывать — это предпочтение | Лишний щелчок, если команды смотрят часто |
|
||
| R-5 | 2026-10-15 | Отмена правки заметки — кнопка «Отмена» с подтверждением при правках (в разделе 1 дизайна был только «Править»/«Сохранить») | Из режима правки нужен выход без сохранения, не закрывая окно | Лишний вопрос при отмене |
|