22 KiB
22 KiB
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)
- Поле
commentв том жеsearch.STATE_FIELDSполучаетcomment; новых вызовов Jira на обычную задачу нет. Jira Server отдаётfields.comment = {comments: [...], total, maxResults, startAt}. - Дочитывание. Если
len(comments) < total, агент читает комментарии этой задачиGET /issue/{key}/comment(startAt,maxResults = 100) до конца и берёт их вместо частичного списка. Вызовы учитываются в темпе и 429 как любые вызовы Jira. - Форма.
comments: [{id, author, created, updated, body}]в порядке Jira (старые первыми):id— строка,author—author.displayName, иначеauthor.name, иначеnull;created/updated— строки Jira как есть;body— текст как есть ("", если в Jira пусто). Полеcommentsесть у каждой задачи, прочитанной агентом v10 (пустой список — комментариев нет). - Версия и выкатка.
AGENT_VERSION = 10; после merge PR агента —jira-hub agent publish src/agent.py(пишет в каталог живого хаба: выполняет владелец или агент по его отдельной команде).
Хаб
- Таблица
issue_comment(key, comment_id, author, created, updated, body, PRIMARY KEY (key, comment_id)); выборка поkeyидёт по первичному ключу, отдельный индекс не нужен. Схема v3; миграция v2 → v3 создаёт таблицу (копия базыschema-v2— существующим механизмом). - Запись.
apply_state: для задачи с полемcomments(список) комментарии этой задачи удаляются и вставляются присланные — в одной транзакции с задачей; задача без поляcommentsсвоих комментариев не теряет (агент до v10). - Проверка формы в
h_agent_stateдо записи:comments— список объектов;id— непустая строка;body— строка;author,created,updated— строка илиnull. Нарушение — 422bad_stateс ключом задачи и полем, пачка не применяется целиком (агент получает ошибку хаба в отчётеrefresh). GET /issues/{key}: добавляютсяcomments(новые сверху: поcreatedубыв., затемcomment_idубыв.),epic_summary(тема задачи-эпика из зеркала илиnull), у каждой связи —summaryиstatusсвязанной задачи (LEFT JOIN issue,null, если её нет в зеркале). Прежние поля и их форма не меняются.- MCP
issue: после заметок — «комментарии (N):» и 5 последних (автор, время, текст до 300 символов с «…»), затем «ещё M», если их больше 5; нет комментариев — «комментариев нет».
Веб-приложение: окно
- Фоновая страница. Ссылки на карточку (ключ в списке, «ⓘ» на доске, связи в карточке) передают в
location.state{background, depth, direct}:background— адрес страницы под окном (pathname + search),depth— сколько записей истории занято карточками поверх неё (1 при открытии со страницы, +1 при переходе по связи),direct— окно открыто прямым адресом (переносится в переходы по связям). Маршрут верхнего уровня —{path: "*", element: <Layout/>};Layoutвсегда рисует страницы черезuseRoutes(PAGE_ROUTES, pageLocation)с явным адресом:backgroundпри открытом окне, иначе текущий адрес. Явный адрес нужен всегда: так дерево компонентов страницы не меняется при открытии окна (страница не пересоздаётся), аuseSearchParams/useLocationвнутри фоновой страницы видят её собственный адрес, и фильтры списка не сбрасываются. Состояние страницы (фильтры, загруженные порции, прокрутка, раскладка доски, опрос пакета) сохраняется.ErrorBoundaryстраницы получает ключ по адресу страницы, у окна — свой. - Прямой адрес
/app/issues/{key}безstate(новая вкладка, закладка):Layoutзаменяет запись истории тем же адресом соstate = {background: /issues, depth: 0, direct: true}; под окном — список задач без фильтров. Перезагрузка страницы с открытым окном сохраняетstate(браузер хранит его в записи истории). - Закрытие (✕, Esc, щелчок по фону):
depth = 0—navigate("/issues", {replace: true});depth ≥ 1—navigate(-depth), браузер возвращается к записи страницы под окном, и «назад» после закрытия ведёт на шаг раньше неё; если окно было открыто прямым адресом (direct), запись прямого адреса, на которую вернулись, заменяется на/issues(replace). Закрытие проходит через блокировщик заметок (п. 14). Вкладки «Доска»/«Задачи» в шапке подсвечиваются по адресу страницы под окном. - Элемент
<dialog>сshowModal(): фокус удерживается в окне, фон недоступен, Esc вызываетcancel(обрабатываем сами — закрытие по п. 12 с проверкой заметок; еслиcancelне отменяемый — браузер закрыл окно сам без активации пользователя, — по событиюcloseокно открывается снова и закрытие идёт тем же путём). Щелчок по фону — щелчок по самому<dialog>вне области содержимого. Прокрутка страницы под окном на время открытия блокируется (overflow: hiddenуbody), содержимое окна прокручивается внутри, шапка окна закреплена. Ширина до 1000 px, высота до 90vh; на экранах уже 700 px — на всю ширину. - Несохранённые заметки. Подтверждение «Есть несохранённые правки заметки. Уйти без сохранения?» при закрытии окна,
переходе по связи и уходе в раздел; условие блокировки — смена
pathnameпри правках (как сейчас), закрытие по п. 12 проходит через тот же блокировщик. Отказ — окно остаётся с введённым текстом. - Доска под окном. Сохранение заметки уже инвалидирует
boardи["issues"](useSaveNote): фоновая страница смонтирована, поэтому перечитывается и показывает новые «вид работы» и «статус по факту». Отдельного кода нет — поведение закрепляется тестом.
Веб-приложение: раскладка карточки
- Шапка: ключ; статус Jira (плашка); статус по факту (плашка цветом класса из
status_labels, если есть); «Открыть в Jira ↗»; ✕; тема; строка «исполнитель · срок · спринт · эпик: <тема эпика>» — пустые части пропускаются, эпик без темы в зеркале — ключом. Срок —planned_end, иначеduedate; «просрочено» (красным) — срок раньше сегодняшней даты и статус по факту не относится к классамfact-done,fact-cancelled. - Связи по смыслу. По
link_typesнастроек: входящиеблокирует начало/блокирует завершение— группа «Зависит от», исходящие — «Блокирует»; у строки метка «начало» / «завершение».связанныев обе стороны — «Связана с». Тип, которого нет вlink_types, — «Прочие» с именем типа Jira и направлением («→» исходящая, «←» входящая). Строка: ключ (ссылка — открывает связанную задачу в окне), тема, статус Jira (плашка); связанной задачи нет в зеркале — ключ и «нет в зеркале». Пустая группа не показывается; связей нет — «связей нет». Порядок групп: «Зависит от», «Блокирует», «Связана с», «Прочие»; внутри — по ключу. - Описание —
pre-wrapбез разбора разметки; выше 8 строк сворачивается с кнопкой «развернуть» / «свернуть» (кнопка только если текст не помещается). Нет описания — раздела нет. - Комментарии — «Комментарии (N)» с кнопкой «Скрыть» / «Показать»; новые сверху; автор, время (
fmtStamp), текстpre-wrap. Выбор «скрыто» — один на все карточки, вlocalStoragejira-hub-comments-hidden("1"— скрыты); недоступныйlocalStorage— показаны. Нет комментариев — «комментариев нет» (кнопки нет). - Заметки — по умолчанию режим чтения: «Вид работы», «Статус по факту», «Сделано», «Осталось», «Заметка владельца»
(пустые — «—»), строка «изменено <время>, <автор>»; кнопка «Править» показывает нынешнюю форму
NoteForm(поля, «Сохранить», «Отмена»); после успешного сохранения — снова режим чтения; «Отмена» без правок — сразу, с правками — подтверждение «Отменить правки заметки?». Заметки ещё нет — режим чтения с «—» и «Править». - Прочие поля: тип, приоритет, автор, метки, плановое начало, обновлена в Jira, «сосед по связям» — непустые.
- Команды моста (N) — свёрнуты (
<details>), внутри нынешняя таблица; команд нет — «команд по задаче не было» и раздел не сворачивается. - Колонки: под шапкой слева — «Связи», «Описание», «Комментарии»; справа (≈320 px) «Заметки», «Прочие поля», «Команды моста». Уже 900 px — одна колонка: заметки, связи, описание, комментарии, прочие поля, команды.
- Состояния окна: загрузка — «загрузка …»; 404 — «Задачи нет в зеркале хаба.» и «Закрыть»; хаб не отвечает — сообщение и «Повторить», ранее загруженные данные остаются; поверх ошибки при данных — предупреждение в шапке окна.
Проверки
- Тестовый хаб:
web/scripts/seed-extras.json— дополнения к фикстуре доски только для тестового хаба: у AURORA-9001 описание длиннее 8 строк, три комментария и две команды (пакет автораseed, команды завершены).seed-hub.pyприменяет их после фикстуры. Связи не добавляются (они меняют доску и её e2e); группа «Прочие» и связь на задачу вне зеркала проверяются в vitest, а связь на задачу вне зеркала (lower-1) уже есть в фикстуре. Фикстура доски и её golden-эталоны не меняются. - 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
- PR jira-agent (v10) → merge →
jira-hub agent publish→agent_status«обновление: ok v9 → v10». - PR jira-hub (#37) → merge →
git pull --ff-only,make webв основном checkout → перезапуск хаба владельцем (launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub): миграция v2 → v3 с копией базы →/mcpreconnect. - Ближайший
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 дизайна был только «Править»/«Сохранить») | Из режима правки нужен выход без сохранения, не закрывая окно | Лишний вопрос при отмене |