diff --git a/openspec/changes/sprints-backlog/.openspec.yaml b/openspec/changes/sprints-backlog/.openspec.yaml new file mode 100644 index 0000000..1f5d280 --- /dev/null +++ b/openspec/changes/sprints-backlog/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-24 diff --git a/openspec/changes/sprints-backlog/design.md b/openspec/changes/sprints-backlog/design.md new file mode 100644 index 0000000..0cb841d --- /dev/null +++ b/openspec/changes/sprints-backlog/design.md @@ -0,0 +1,172 @@ +## Context + +Основа — `main` `a2bf3fc` (схема v8 после fact-status-removal #104). Issue #108, ветка `feat/108-sprints-backlog`. +Агент — jira-agent v14 (`../jira-agent`), его решения — в design-only change `agent-v15-sprints` репозитория jira-agent. + +Что есть сейчас (разведка 24.10.2026): +- у задачи в зеркале `issue.sprint` — имя одного спринта строкой; агент собирает имена со спринтов досок `scrum` + (активные и будущие, `GET /rest/agile/1.0/board/{id}/sprint?state=active,future` и `/sprint/{id}/issue`); у задач + закрытых спринтов `sprint = NULL` — принятое ограничение; +- спринтов как сущностей (id, состояние, даты, цель), ранга задач, оценок и колонок доски хаб не знает; +- `ensure_sprint {key, sprint}` переносит задачу в спринт или бэклог, `board_id` хаб дописывает по проекту задачи; +- доска проекта — `jira_project.board_id`, `board_type` (`scrum`/`kanban`/`NULL`), тип пишет только результат `refresh`; +- живые данные: доска только у `AWPCA` (4216, scrum), спринты `AWPCA_Sprint_2_VGO` (73 задачи) и + `AWPCA_Sprint_3_Corrections` (37), 188 своих задач вне спринтов; 11 статусов; у `STOCKS_DRRK` и `sfm_kedr` досок нет; +- оценок в AURORA пока нет (`field_ids` без story points); веб — `/app/jira/board` («Доска» — доска связей), + `/app/jira/issues`, `/app/jira/feed`. + +## Goals / Non-Goals + +**Goals:** в вебе — бэклог, доска спринта и kanban по образцу Jira, только просмотр; история закрытых спринтов; оценки +(story points) в зеркале, на экранах и в MCP; из чата — создание и правка спринта, запуск и завершение, ранжирование, +оценка. + +**Non-Goals и Anti-Goals:** см. proposal.md. + +## Decisions + +1. **Веб — только просмотр** (решение владельца 24.10.2026: «веб только смотреть… вэб это только вью»). Ни + перетаскивания, ни кнопок, меняющих Jira, на новых экранах нет; все изменения — пакетами команд из MCP. +2. **Экраны по типу доски проекта** (решение владельца): scrum — «Бэклог» и «Спринт»; kanban — «Kanban»; без доски — + пунктов нет. Тип доски неизвестен (`NULL`) — пунктов нет, как без доски. +3. **Kanban-бэклог — раздел страницы «Kanban», а не отдельный пункт меню** (решение агента): признак kanban-бэклога + известен только из данных доски (`GET /agile`), а меню строится по `GET /projects`; отдельный пункт мигал бы при + загрузке. Если бэклог у доски включён — над колонками свёрнутый список «Бэклог (N)» по рангу. +4. **«Доска» переименовывается в «Связи»** (решение владельца): подпись в меню и заголовке вкладки — «Связи», адрес — + `/app/jira/links`; `/app/jira/board` и прежний `/app/board` перенаправляют на него с сохранением строки запроса. +5. **Спринты — сущности зеркала.** Таблица модуля jira `jira_sprint` (id Jira, доска, имя, состояние `active` / + `future` / `closed`, даты начала, конца и завершения, цель) и `issue_sprint` (задача ↔ все её спринты, любого + состояния). `issue.sprint` (имя текущего спринта) остаётся как есть — фильтры списка задач и доска связей его + используют. История закрытых спринтов (решение владельца: «лучше иметь исторические данные») — из `issue_sprint`: + Jira хранит у задачи все её спринты, агент передаёт их id. +6. **Колонки доски — из настроек доски Jira** (`GET /rest/agile/1.0/board/{id}/configuration`): `jira_board` хранит + колонки `[{name, statuses: [имя статуса…]}]` и признак kanban-бэклога. Статус задачи без колонки — колонка «Без + колонки» в конце (видна, только если в ней есть задачи). +7. **«Выполнено» — статус в последней колонке доски** (правило Jira для scrum-доски). Для закрытого спринта — + по текущему статусу задачи (решение агента): статуса на момент закрытия хаб не знает (журнал изменений покрывает + только открытые спринты и с 22.10.2026). Задача, выполненная после закрытия спринта, в прошлом спринте покажется + выполненной — принятое ограничение. +8. **Свои задачи проекта.** Экраны и MCP показывают только свои задачи проекта (метка проекта), соседей нет. Спринты — + все спринты доски проекта, в том числе без своих задач. +9. **Ранг — строка Jira (LexoRank) как есть**, порядок — сравнение строк; задачи без ранга — в конце, по ключу. +10. **Оценка — story points**, число `REAL` (`issue.story_points`, `NULL` — нет оценки). Поле Jira: настройка + `field_ids.story_points`, если задана; иначе агент находит поле сам (решение агента в `agent-v15-sprints`). + Суммы по спринту: всего и выполнено; задача без оценки в сумму не входит, а число таких задач показывается. +11. **Новые типы команд**, все «проверь, потом сделай», темп по умолчанию 30 с: + - `ensure_sprint_meta {sprint, rename_to?, start_date?, end_date?, goal?}` — спринт доски проекта с этим именем + (среди активных и будущих) существует и имеет эти поля; нет — создаётся; + - `ensure_sprint_state {sprint, state: "active" | "closed", move_open_to?}` — спринт в этом состоянии; при + `closed` невыполненные задачи (не в последней колонке) сначала переносятся в `move_open_to` (имя активного или + будущего спринта или `backlog`; для `closed` обязателен); + - `ensure_rank {key, before | after}` — задача выше (`before`) или ниже (`after`) другой задачи; уже так — без + записи. + `board_id` у `ensure_sprint_meta` и `ensure_sprint_state` хаб дописывает при постановке из проекта пакета (команды + не про задачу — проекта задачи нет). Пакет без проекта, проект без доски, kanban-доска, неизвестный тип доски — 422 + с тем же текстом, что у `ensure_sprint`. +12. **Оценка ставится через `ensure_fields`** (новое поле `story_points`: число ≥ 0 или `null` — очистить), отдельной + команды нет. +13. **Данные досок — в каждом `refresh`.** Агент v15 к первой пачке состояния `refresh` прикладывает `boards` — + по каждой доске из параметров команды: тип, колонки, kanban-бэклог, все спринты (любого состояния); хаб заменяет + данные доски целиком. У задач — `rank`, `story_points`, `sprint_ids`. Результат команд видно после хвостового `refresh`, который + хаб уже дописывает к пишущему пакету (`hooks.tail`, по ключам задач команд); пакет с `ensure_sprint_meta` или + `ensure_sprint_state` получает хвост `refresh {jql: }` — у этих команд нет ключей, а + перенесённые при закрытии задачи заранее неизвестны. `refresh` без задач (все ключи пропали) агент v15 всё равно + отправляет одной пустой пачкой с `boards`. +14. **Совместимость с агентом v14.** Без `boards` и новых полей хаб ничего не теряет: прежние данные досок и задач + остаются; экраны без данных доски показывают «Нужен агент v15 и обновление из Jira». Новые типы команд агенту v14 — + `failed` «неизвестный тип» на его стороне; `min_agent_version` хаб не поднимает (поднимет владелец после выкатки v15). +15. **API — один маршрут `GET /agile?project=<метка>`** (данные доски, спринты со своими задачами и суммами, бэклог). + Веб и MCP читают его; отдельных маршрутов на спринт и бэклог нет. +16. **Схема v9**: `issue.rank`, `issue.story_points`, таблицы `jira_board`, `jira_sprint`, `issue_sprint`. + +17. **Kanban без бэклога** (`kanban_backlog` `false` или `null`): первая колонка — обычная колонка доски, её задачи — в + `board_issues`; `backlog = null`. +18. **`ensure_sprint_meta` ищет спринт по имени среди всех спринтов доски**: закрытый — `failed` «спринт закрыт»; + несколько с этим именем — `failed` с их id; `rename_to` на имя другого спринта доски — `failed` «имя занято». + `ensure_sprint_state {state: closed}` для будущего спринта — `failed` «спринт не начат»; `active` для закрытого — + `failed` «спринт закрыт». Результаты: `ensure_sprint_meta` — `{sprint_id, created}`, `ensure_sprint_state` — + `{sprint_id, moved}`, `ensure_rank` — `{changed}`. Подтверждения по зеркалу у новых типов нет (`check_confirmed` — + нет): повторная выдача без отчёта выполняется агентом заново, это безопасно по «проверь, потом сделай». +19. **Проектный пакет:** `ensure_rank` принадлежит проекту по `key` (своя задача или временный ключ пакета); `before` / + `after` — любая задача зеркала. `ensure_sprint_meta`/`ensure_sprint_state` в пакете проекта проверяются по доске + проекта (решение 11), не по задаче. `command_retry` снимает `board_id` у всех трёх типов со спринтом, как у + `ensure_sprint`. +20. **Форма новых полей состояния:** `story_points: null` и `rank: null` — «очистить»; `sprint_ids: []` — у задачи нет + спринтов; `sprint_ids: null` или не-список целых — 422 `bad_state`. +21. **Меню модуля зависит от проекта:** пункт раздела модуля веба может нести условие по проекту (тип доски из `GET + /projects`); пока проекты не загружены, условные пункты скрыты. Смена проекта на странице, которой у нового проекта + нет, оставляет адрес и показывает «У проекта нет …-доски» (как прямой переход). +22. **Тип доски — одна правда, `jira_project.board_type`:** тип из `boards` пачки состояния хаб записывает и в + `jira_board.type`, и в `jira_project.board_type` проектов этой доски (как результат `refresh`, только `scrum`/ + `kanban`). Меню и 422 постановки читают `jira_project`, экраны — `GET /agile`, тип в нём — из `jira_project`. +23. **Три состояния данных доски на экранах:** данных нет (`fetched_at = null`) — «Нужен агент v15 и обновление из + Jira»; данные есть, колонок нет (агент не смог прочитать настройки) — «Не удалось прочитать настройки доски из + Jira» и обновление; колонки есть — экран. Без колонок `done` у всех задач — `false`. +24. **Даты:** в командах — `ГГГГ-ММ-ДД` (агент переводит в формат Jira); в `jira_sprint` — строки Jira как есть + (ISO-8601 со смещением); веб показывает их датой «ДД.ММ» в часовом поясе браузера; «осталось N дн.» — от сегодня до + `end_date`. +25. **История закрытых спринтов — из базовой JQL:** базовая JQL проектов не отсекает закрытые задачи (`labels in + (…)`), полный `refresh` возвращает и их — членство всех своих задач в закрытых спринтах приходит первым же полным + `refresh` агента v15. +26. **«Доска связей» → «Связи»** (сейчас пункт меню называется «Доска связей», адрес `/jira/board`); `/app/board` + перенаправляет сразу на `/app/jira/links`. Документы, где адрес упомянут (README, CLAUDE.md репозитория, e2e), — + в том же change. + +## Контракт данных + +`POST /agent/state` (агент v15), необязательные поля: +- у задачи: `rank` (строка или `null`), `story_points` (число или `null`), `sprint_ids` (список целых — все спринты + задачи); присланное `sprint_ids` заменяет все строки `issue_sprint` задачи, задача без поля свои строки сохраняет; +- в пачке: `boards: [{id, type, columns: [{name, statuses: [строка]}] | null, kanban_backlog: bool | null, + sprints: [{id, name, state, start_date, end_date, complete_date, goal}]}]`; доска из `boards` заменяет свою строку + `jira_board` и все свои строки `jira_sprint`. Даты спринтов — строки Jira как есть или `null`. Пустой `refresh` — + одна пачка `{issues: [], links: [], boards}`. + +`GET /agile?project=P` → +``` +{board: {id, type, columns, kanban_backlog, fetched_at} | null, + sprints: [{id, name, state, start_date, end_date, complete_date, goal, + count, count_done, unestimated, points, points_done, issues: [строка]}], + backlog: [строка] | null, + board_issues: [строка] | null} +``` +Строка задачи: `key, summary, issue_type, status, priority, assignee, epic_key, story_points, rank, done` (`done` — +статус в последней колонке). Порядок спринтов: активные (по дате начала), будущие (по id), закрытые (по дате +завершения, новые сверху). `backlog`: scrum — свои задачи не в активном и не в будущем спринте и не выполненные; +kanban — свои задачи в статусах первой колонки, если kanban-бэклог включён, иначе `null`. `board_issues` (только +kanban) — свои задачи в статусах остальных колонок (при выключенном или неизвестном бэклоге — и первой) и без колонки; +выполненные — только с `jira_updated` за последние 14 дней. Всё +по рангу. + +## Risks / Trade-offs + +- **Поля Jira банка не проверены**: имя поля story points, тип поля ранга, признак kanban-бэклога в + `board/{id}/configuration` Jira Server. Проверка — на живой Jira при выкатке агента; не найдено — агент передаёт + `null`, экраны показывают «—», признак kanban-бэклога `null` — раздела «Бэклог» нет. +- **Объём закрытых спринтов**: агент читает список всех спринтов доски каждым `refresh` (постранично, без задач) — на + доске с десятками спринтов это несколько запросов; задачи закрытых спринтов не дочитываются, членство — из поля + спринта задач. +- **Story points на экране редактирования Jira**: если поле не на экране AURORA, `ensure_fields story_points` получит + ошибку Jira — `failed` с её текстом; нужна настройка Jira (у владельца/администратора). + +## Decisions made without discussion + +- 24.10.2026 — R-1 (design #3): kanban-бэклог — раздел страницы «Kanban», не пункт меню. Цена ошибки — перенести + раздел в отдельную страницу. +- 24.10.2026 — R-2 (design #7): «выполнено» у закрытого спринта — по текущему статусу. Цена ошибки — считать по + журналу изменений, когда он появится по закрытым спринтам. +- 24.10.2026 — R-3 (design #8): только свои задачи проекта на экранах и в MCP. Цена ошибки — добавить соседей флагом. +- 24.10.2026 — R-4 (design #10): поле оценки — `field_ids.story_points` или поиск агентом. Цена ошибки — настройка руками. +- 24.10.2026 — R-5 (design #11): имена и параметры команд `ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank`; + «выше/ниже» — отношение, а не «вплотную». Цена ошибки — переименование типов до выкатки. +- 24.10.2026 — R-6 (design #13): данные досок — в каждом `refresh`, в том числе по ключам. Цена ошибки — лишние + запросы к Jira (несколько на доску). +- 24.10.2026 — R-7 (design #15): один маршрут `GET /agile`. Цена ошибки — разделить маршрут. +- 24.10.2026 — R-8 (контракт): kanban — выполненные задачи за 14 дней. Цена ошибки — число в настройке. +- 24.10.2026 — R-9 (design #6): колонка «Без колонки» для статусов вне настроек доски. Цена ошибки — нет. +- 24.10.2026 — R-10 (design #13): хвост пакета со спринтами — `refresh` по JQL проекта; пустой `refresh` шлёт пачку с + `boards`. Цена ошибки — лишнее чтение проекта (~3 запроса `search`). +- 24.10.2026 — R-11 (design #17–#20): правила kanban без бэклога, поиска спринта по имени, ошибок состояний, проектной + принадлежности `ensure_rank`, формы полей. Цена ошибки — правка проверок. +- 24.10.2026 — R-12 (design #21–#26): условные пункты меню, тип доски в `jira_project`, три состояния данных доски, + даты в поясе браузера, история из базовой JQL, переименование «Доска связей». Цена ошибки — правка веба. diff --git a/openspec/changes/sprints-backlog/proposal.md b/openspec/changes/sprints-backlog/proposal.md new file mode 100644 index 0000000..c6a9bf7 --- /dev/null +++ b/openspec/changes/sprints-backlog/proposal.md @@ -0,0 +1,51 @@ +## Why + +Владелец 23.10.2026 наметил для модуля jira спринты и бэклог «как в Jira». 24.10.2026 (brainstorming #108) уточнил: +в первую очередь — визуализация в вебе, похожая на Jira (бэклог и доска спринта, для kanban-досок — kanban), и +действия из чата через MCP: создавать спринты, запускать и завершать их, ранжировать задачи в бэклоге. Веб — только +просмотр. Нужна история закрытых спринтов и оценки: в AURORA пока не оценивали, владелец хочет ретроспективно прикинуть +оценки по прошедшему спринту, чтобы дальше планировать покером. + +Сейчас хаб знает спринт задачи только как имя строкой, у задач закрытых спринтов имя теряется; ни спринтов (даты, +состояние, цель), ни ранга, ни оценок, ни колонок доски в зеркале нет. + +## What Changes + +- **Зеркало (схема v9):** спринты досок (`jira_sprint`), членство задач во всех их спринтах (`issue_sprint`), колонки + и kanban-бэклог доски (`jira_board`), у задачи — ранг (`rank`) и оценка (`story_points`). +- **Агент v15** (design-only change `agent-v15-sprints` в jira-agent): в `refresh` читает настройки досок, все спринты, + ранг, story points и все спринты задачи; выполняет новые команды. +- **Команды:** `ensure_sprint_meta` (создать или поправить спринт: имя, даты, цель), `ensure_sprint_state` (запустить + или завершить, при завершении — куда невыполненные), `ensure_rank` (выше/ниже другой задачи); `ensure_fields` — + поле `story_points`. +- **API:** `GET /agile?project=` — доска, спринты (активные, будущие, закрытые) со своими задачами и суммами оценок, + бэклог, задачи kanban-доски. +- **MCP `sdlc-jira`:** `sprints`, `sprint`, `backlog` — чтение с оценками и суммами; новые типы команд — через + `batch_create` с подсказками параметров. +- **Веб модуля jira:** страницы «Бэклог», «Спринт» (scrum) и «Kanban» (kanban), только просмотр; пункты меню — по типу + доски проекта, без доски — нет; «Доска» → «Связи» (`/app/jira/links`, прежние адреса перенаправляют). + +## Non-Goals + +- Правки Jira из веба (перетаскивание, кнопки спринтов, смена статуса) — веб только смотрит. +- Отчёты Jira (burndown, velocity), покер планирования, эпики на бэклоге отдельной панелью, версии (fix version). +- Статус задачи на момент закрытия спринта (по журналу) — «выполнено» по текущему статусу. +- Соседи (задачи других проектов) на новых экранах. +- Подъём `min_agent_version` — владелец после выкатки v15. + +## Anti-Goals + +- Экраны, которые выглядят как Jira, но расходятся с ней по составу: спринт с задачами не того проекта, бэклог с + выполненными задачами, порядок не по рангу Jira. +- Команда спринта, которая при повторе делает второй спринт или второй перенос. +- Пункт меню, ведущий на пустую страницу у проекта без доски. +- Потеря работы хаба со старым агентом v14 до выкатки v15. + +## Impact + +- Хаб: `modules/jira/{schema,mirror,api,mcp,commands,hooks,settings}.py`, `core/db.py` (v9), веб + `web/src/modules/jira/**` (новые страницы, переименование, меню), спеки `data-model`, `commands`, `agent`, `hub-api`, + `mcp`, `web-app`. +- jira-agent: v15 (`src/agent.py`), свой design-only change. +- Выкатка: хаб (kickstart, `make web`, `/mcp` reconnect) → `agent publish` v15 → проверка полей Jira банка → + `field_ids.story_points`, если агент поле не нашёл. diff --git a/openspec/changes/sprints-backlog/specs/agent/spec.md b/openspec/changes/sprints-backlog/specs/agent/spec.md new file mode 100644 index 0000000..b357f85 --- /dev/null +++ b/openspec/changes/sprints-backlog/specs/agent/spec.md @@ -0,0 +1,27 @@ +## ADDED Requirements + +### Requirement: Спринты, ранг и оценки +Агент v15 SHALL в каждом `refresh` (по JQL и по ключам) для каждой доски из `boards` команды с типом `scrum` или +`kanban` (известным или только что определённым) прочитать настройки доски (`GET /rest/agile/1.0/board/{id}/configuration`: +колонки и их статусы — по именам статусов; признак kanban-бэклога, если Jira его отдаёт, иначе `null`) и, у `scrum`, +все спринты доски любого состояния (`GET /rest/agile/1.0/board/{id}/sprint` постранично) и приложить к первой пачке +состояния `boards` (см. `hub-api`, «Приём досок и спринтов»). Ошибка чтения настроек или спринтов доски — доска с +`columns: null` и прежними правилами для спринтов (v12), причина в лог; `refresh` продолжается. + +Каждая задача в состоянии (с v15) SHALL нести `rank` — значение поля ранга Jira (поле типа LexoRank), `story_points` — +значение поля оценки (`field_ids.story_points`, если задано, иначе поле Jira с именем «Story Points»; поля нет — +`null`), и `sprint_ids` — id всех спринтов задачи из поля спринта Jira (любого состояния). Поле не найдено — значение +`null` у всех задач и запись в лог один раз за `refresh`. + +Агент v15 SHALL выполнять `ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank` и `story_points` в +`ensure_fields` по контракту `commands`; сравнение — до записи, повтор без изменений — `done` без записи. + +#### Scenario: Доска с закрытыми спринтами +- **WHEN** `refresh {jql: …, boards: [{id: 4216, type: "scrum"}]}`, у доски закрытые спринты 1–2, активный 3, будущий 4 +- **THEN** первая пачка состояния несёт `boards` с доской 4216, четырьмя спринтами и колонками; у задачи спринта 2, + перенесённой в 3, — `sprint_ids: [2, 3]` + +#### Scenario: Нет поля оценки +- **WHEN** в Jira нет поля «Story Points», `field_ids.story_points` не задан +- **THEN** у всех задач `story_points: null`; `ensure_fields {key: X, story_points: 3}` — `failed` «поле оценки не найдено + в Jira: задайте field_ids.story_points» diff --git a/openspec/changes/sprints-backlog/specs/commands/spec.md b/openspec/changes/sprints-backlog/specs/commands/spec.md new file mode 100644 index 0000000..9e7dd4f --- /dev/null +++ b/openspec/changes/sprints-backlog/specs/commands/spec.md @@ -0,0 +1,129 @@ +## MODIFIED Requirements + +### Requirement: Команда — желаемый результат на одной задаче +Каждая команда SHALL описывать желаемое состояние одной задачи Jira и выполняться агентом как «прочитать → сравнить → +записать только разницу». Повторное выполнение любой команды с тем же результатом в Jira SHALL завершаться `done` +без записи. `fetch_attachment` в Jira не пишет: она читает файл вложения и передаёт его хабу; повтор безопасен. Типы команд и их контракт: + +| Тип | Параметры | Что читает | Что пишет | +|---|---|---|---| +| `ensure_issue` | `temp_key` (`new:`), `issue_type` (`task`/`epic`/`bug`), `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels`, `epic_key`, `assignee` | поиск `project = AND reporter = currentUser() AND created >= -7d`, точное сравнение `summary` | при отсутствии — `POST /issue` с типом из `issue_types.` (для эпика — с `epic_name_field_id`); результат `key` | +| `ensure_fields` | `key`, любое подмножество: `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels_add`, `labels_remove`, `epic_key`, `assignee`, `story_points` (число ≥ 0 или `null` — очистить) | `GET /issue/{key}` | `PUT /issue/{key}` только с отличающимися полями; эпик — по `epic_link.mode`; оценка — в поле story points (см. `agent`, «Спринты, ранг и оценки»); поля нет — `failed` «поле оценки не найдено в Jira: задайте field_ids.story_points» | +| `ensure_comment` | `key`, `marker`, `body` | `GET /issue/{key}/comment` | комментарий с `marker` в начале: `PUT` если текст отличается, `POST` если нет | +| `ensure_status` | `key`, `target` (имя статуса или список-путь) | `GET /issue/{key}` (статус), `GET /issue/{key}/transitions` на каждом шаге | `POST /transitions` по шагам пути; при отсутствии перехода — `failed` с перечнем доступных | +| `ensure_link` | `source`, `target`, `link_type` (логическое имя из `link_types`), `replace` (bool) | `GET /issue/{source}` (`issuelinks`) | нет нужной и нет конфликта — `POST /issueLink`; конфликт (другой тип из `link_types` или обратное направление) и `replace` — `DELETE` лишней, затем `POST`; конфликт без `replace` — `failed` с описанием | +| `ensure_sprint` | `key`, `sprint` (имя или `backlog`); `board_id` хаб дописывает при постановке (от клиента — 422) | `GET /issue/{key}` (поле спринта), список спринтов доски `board_id` (активные и будущие) | `POST /rest/agile/1.0/sprint/{id}/issue` или `POST /rest/agile/1.0/backlog/issue` | +| `ensure_sprint_meta` | `sprint` (имя), необязательные `rename_to`, `start_date`, `end_date` (`ГГГГ-ММ-ДД`), `goal`; `board_id` хаб дописывает при постановке (от клиента — 422) | спринты доски `board_id` (все состояния) | спринта с именем `sprint` нет — `POST /rest/agile/1.0/sprint` (`originBoardId`, имя `rename_to` или `sprint`, даты, цель); есть — `POST /rest/agile/1.0/sprint/{id}` только с отличающимися полями; спринт закрыт — `failed` «спринт закрыт»; результат `{sprint_id, created}` | +| `ensure_sprint_state` | `sprint`, `state` (`active` или `closed`), `move_open_to` (имя активного или будущего спринта той же доски или `backlog`; при `closed` обязателен, при `active` — 422); `board_id` — как у `ensure_sprint_meta` | спринты доски (все состояния), задачи спринта и колонки доски | `active`: спринт будущий — `POST /sprint/{id}` `state = active` (нет дат у спринта — `failed` «у спринта нет дат: задайте ensure_sprint_meta»); `closed`: задачи спринта не в последней колонке доски — в `move_open_to`, затем `state = closed`; уже в нужном состоянии — ничего; результат `{sprint_id, moved: [ключ…]}` | +| `ensure_rank` | `key` и ровно одно из `before`, `after` (ключ другой задачи) | ранг обеих задач | `key` уже выше `before` (ниже `after`) — ничего; иначе `PUT /rest/agile/1.0/issue/rank` (`rankBeforeIssue` / `rankAfterIssue`) | +| `ensure_attachment` | `key`, `file` (sha256 файла в хранилище хаба, 64 hex-символа), `filename` (имя вложения в Jira, без `/`, `\`, `"` и управляющих символов); `size` хаб дописывает при выдаче | `GET /issue/{key}?fields=attachment` | вложения с тем же `filename` и размером нет — файл у хаба (`GET /agent/files/{file}`), затем `POST /issue/{key}/attachments` (multipart); есть — ничего; результат `attachment_id` | +| `refresh` | `keys` (список) или `jql`; `boards` (`[{id, type}]` — доски проектов, `type` — `scrum`, `kanban` или `null`) хаб дописывает при выдаче | `search` с полями зеркала, по 100 на страницу; плюс дочитывание соседей по `issuelinks`; доски с неизвестным типом — `GET /rest/agile/1.0/board/{id}` | ничего в Jira; состояние уходит в хаб; результат — `boards: {"": "scrum" | "kanban" | null}` для досок, чей тип агент запрашивал | +| `fetch_attachment` | `key`, `attachment_id` (id вложения Jira, строка из цифр) | `GET /issue/{key}?fields=attachment` (имя и размер вложения) | ничего в Jira; содержимое — `GET /secure/attachment/{id}/{имя}`, затем хабу `POST /agent/files`; результат `{attachment_id, file, size}` | + +Доску для `ensure_sprint` хаб SHALL определять при постановке: проект задачи — проекты, чьи метки есть у задачи в +зеркале (для временного ключа — в `labels` её `ensure_issue` в пакете, после дописывания метки проекта); `board_id` — +доска этого проекта. 422 при постановке: задача ни в одном проекте; в нескольких проектах с разными досками; у проекта +нет доски; тип доски `kanban` («у проекта P канбан-доска, спринтов нет»); тип доски ещё не известен («тип доски проекта P +ещё не известен — дождитесь обновления из Jira»). `boards` у `refresh` хаб SHALL дописывать при каждой выдаче (в базе +не хранится): все доски проектов без повторов. По результату `refresh` хаб SHALL записать присланные типы `scrum` и +`kanban` в проекты с этой доской; `null` тип не меняет. + +Доску для `ensure_sprint_meta` и `ensure_sprint_state` хаб SHALL определять при постановке по проекту пакета +(`project` пакета): пакет без проекта — 422 «команды спринта — только в пакете проекта»; у проекта нет доски, доска +`kanban` или тип ещё не известен — 422 с теми же текстами, что у `ensure_sprint`. `ensure_rank` в пакете проекта +принадлежит проекту по `key`; `before`/`after` — любая задача. Повтор команды (`command_retry`) SHALL снимать `board_id` +у `ensure_sprint_meta` и `ensure_sprint_state`, как у `ensure_sprint`. Хвостовой `refresh` пишущего пакета с +`ensure_sprint_meta` или `ensure_sprint_state` SHALL быть `refresh {jql: }` (`project = + AND labels in ("<метка>")`); иначе — как раньше, по ключам команд. Подтверждения по состоянию зеркала у +`ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank` нет. + +`ensure_sprint_meta` SHALL искать спринт по имени среди всех спринтов доски: закрытый — `failed` «спринт закрыт»; +несколько с этим именем — `failed` «несколько спринтов с именем …: »; `rename_to` на имя другого спринта доски — +`failed` «имя занято». `ensure_sprint_state`: `closed` для будущего спринта — `failed` «спринт не начат»; `active` для +закрытого — `failed` «спринт закрыт»; результат `ensure_rank` — `{changed}`. + +Семантика `ensure_link`: `source` блокирует/ссылается на `target`; агент в `POST /issueLink` SHALL ставить `source` в +`inwardIssue`, `target` в `outwardIssue` (проверено на Jira банка 11.10.2026). + +Текст описания (`ensure_fields`) и тело комментария (`ensure_comment`) агент SHALL сравнивать с текущим значением в Jira +без различия переводов строк `\r\n` и `\n`; записывается текст команды как есть. Jira хранит присланный текст, а правленный +в веб-интерфейсе — с `\r\n` (проверено на Jira банка 13.10.2026). + +#### Scenario: Повтор уже выполненной команды +- **WHEN** `ensure_fields {key: AURORA-1533, duedate: 2026-10-23}` выполняется, а в Jira срок уже 23.10 +- **THEN** агент не делает `PUT`, отчёт `done`, `jira_calls = 1` + +#### Scenario: Описание отличается только переводами строк +- **WHEN** `ensure_fields {key: X, description: "а\nб"}`, а в Jira описание X — `"а\r\nб"` +- **THEN** агент не делает `PUT`, отчёт `done` с `changed = []` + +#### Scenario: Замена связи +- **WHEN** `ensure_link {source: AURORA-1537, target: AURORA-1538, link_type: "связанные", replace: true}` и в Jira между + ними «блокирует начало» 1537→1538 +- **THEN** `DELETE` старой связи, пауза по политике, `POST` новой; отчёт `done` с обеими операциями + +#### Scenario: Связь-конфликт без replace +- **WHEN** та же команда с `replace: false` +- **THEN** `failed`, в `error` найденная связь, Jira не изменена + +#### Scenario: Переход по пути +- **WHEN** `ensure_status {key: X, target: ["Разработка", "Готово"]}` и X в «Ожидание разработки» +- **THEN** два `POST /transitions` с паузой между ними; если X уже «Готово» — `done` без записи + +#### Scenario: Создание ошибки +- **WHEN** `ensure_issue {temp_key: new:1, issue_type: bug, summary: "ОСВ 60 не распознана", labels: [AWPCA]}`, в настройках + `issue_types.bug = {name: "Ошибка"}` без `id` +- **THEN** агент создаёт задачу с `issuetype = {name: "Ошибка"}`; отчёт `done` с ключом + +#### Scenario: Вложение уже есть +- **WHEN** `ensure_attachment {key: X, file: , filename: "ОСВ 60.xlsx"}`, у X в Jira есть вложение «ОСВ 60.xlsx» того + же размера +- **THEN** агент не скачивает файл у хаба и не загружает его в Jira; отчёт `done` с `attachment_id` найденного, `jira_calls = 1` + +#### Scenario: Вложение с тем же именем, другого размера +- **WHEN** та же команда, у X есть «ОСВ 60.xlsx» другого размера +- **THEN** агент загружает файл вторым вложением с тем же именем (Jira это допускает); отчёт `done` с новым `attachment_id` + +#### Scenario: Создание с проверкой дубля +- **WHEN** `ensure_issue` повторяется после падения агента, а задача с той же темой от нас уже создана час назад +- **THEN** агент не создаёт вторую, отчёт `done` с ключом найденной + +#### Scenario: Спринт задачи проекта +- **WHEN** у AURORA-1538 метка `AWPCA`, проект `AWPCA` с доской 4216 типа `scrum`; `ensure_sprint {key: AURORA-1538, sprint: "Спринт 2"}` +- **THEN** пакет создан, в параметрах команды `board_id = 4216` + +#### Scenario: Спринт на канбане +- **WHEN** у AURORA-1 метка `STOCKS_DRRK`, доска проекта — `kanban`; `ensure_sprint {key: AURORA-1, sprint: "Спринт 2"}` +- **THEN** 422 «у проекта STOCKS_DRRK канбан-доска, спринтов нет» + +#### Scenario: Тип доски определён +- **WHEN** проект `STOCKS_DRRK` с доской 5120 и `board_type = null`; выдаётся `refresh`, агент вернул `boards: {"5120": "kanban"}` +- **THEN** при выдаче в параметрах `boards` есть `{id: 5120, type: null}`; после отчёта у проекта `board_type = kanban` + +### Requirement: Политика темпа и пауза на команде +Хаб SHALL хранить `pacing` как минимальные зазоры в секундах по типу команды (по умолчанию: `ensure_issue` и +`ensure_fields` 120, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint`, `ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank`, `ensure_attachment` 30, `refresh` и `fetch_attachment` 2; тип, которого нет в +сохранённой настройке `pacing`, получает зазор по умолчанию). При выдаче команды +хаб SHALL сообщить `pause_seconds` = зазор по типу выдаваемой команды; агент SHALL выдержать его от времени своего +последнего вызова Jira. Внутри одной команды с несколькими записями (`ensure_link` с `replace`, путь переходов) агент +SHALL выдерживать зазор типа команды между записями. + +#### Scenario: Нет следующей команды — нет паузы +- **WHEN** очередь пуста +- **THEN** `claim` возвращает пустой ответ, агент не спит сверх долгого запроса + +#### Scenario: Создание спринта повтором не дублируется +- **WHEN** пакет проекта `AWPCA` (доска 4216, scrum) `ensure_sprint_meta {sprint: "AWPCA_Sprint_4", start_date: "2026-10-28", end_date: "2026-11-11", goal: "свод ВГО"}` выполнен, затем поставлен повторно +- **THEN** первый раз спринт создан (`created: true`), второй — `done` без записи (`created: false`) + +#### Scenario: Завершение спринта с переносом +- **WHEN** `ensure_sprint_state {sprint: "AWPCA_Sprint_3_Corrections", state: "closed", move_open_to: "AWPCA_Sprint_4"}`, в спринте 37 задач, 12 не в последней колонке доски +- **THEN** 12 задач перенесены в `AWPCA_Sprint_4`, спринт закрыт; результат `moved` — их ключи; повтор — `done` без записи + +#### Scenario: Спринт без проекта пакета +- **WHEN** `POST /batches` без `project` с `ensure_sprint_meta {sprint: "S"}` +- **THEN** 422 «команда 1 (ensure_sprint_meta): команды спринта — только в пакете проекта» + +#### Scenario: Ранг уже такой +- **WHEN** `ensure_rank {key: AURORA-1612, before: AURORA-1601}`, ранг 1612 меньше ранга 1601 +- **THEN** `done` без записи в Jira diff --git a/openspec/changes/sprints-backlog/specs/data-model/spec.md b/openspec/changes/sprints-backlog/specs/data-model/spec.md new file mode 100644 index 0000000..6448162 --- /dev/null +++ b/openspec/changes/sprints-backlog/specs/data-model/spec.md @@ -0,0 +1,160 @@ +## MODIFIED Requirements + +### Requirement: Схема базы хаба +База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_comment`, `issue_change`, `issue_attachment`, +`attachment_file`, `issue_note`, `issue_sprint`, `jira_board`, `jira_sprint`, `batch`, `command`, `sync_run`, `agent_status`, `setting`, `project`. Схема SHALL создаваться и мигрироваться самим хабом при +старте по номеру версии в `setting.schema_version` (текущая — 9). Миграция существующей базы SHALL выполняться в одной +транзакции с блокировкой записи; в той же транзакции, после повторной проверки версии, хаб SHALL скопировать файл базы в +`backups/<дата-время>-schema-v<старая>.sqlite` рядом с базой — одна копия на миграцию, сколько бы соединений ни +открывалось одновременно. Каталог данных хаба и `backups/` SHALL иметь права `0700` (в базе токены): хаб выставляет их +при создании и при каждом старте; если выставить не удалось — предупреждение в лог, работа продолжается. Денежных и +дробных полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени), строк времени +Jira как есть (`issue_comment.created`/`updated`, `issue_change.created`, `issue_attachment.created`) и значений полей журнала. + +Миграция v3 → v4 SHALL: создать `project` и колонку `batch.project` (у существующих пакетов `NULL`); разобрать прежнюю +настройку `base_jql` вида `… labels = ` или `… labels in (, …)` и завести проекты с этими метками (название проекта +`AWPCA` — «АРМ КА», остальных — метка; `board_type = NULL`); отдать прежний `board_id` проекту, если проект один, иначе +никому, с предупреждением в лог; у строк `sync_run` с `query`, равной прежней `base_jql`, и у команд `refresh` в +`queued`/`claimed` с `params.jql`, равной прежней `base_jql`, записать базовую JQL, вычисленную из проектов. Ключи +`base_jql` и `board_id` в `setting` миграция MUST NOT удалять: базу может открыть процесс новой версии раньше, чем +перезапущен хаб прежней версии, который их читает. Не разобранная `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`. + +Миграция v5 → v6 SHALL: создать пустые `issue_attachment` (с индексом по `key`) и `attachment_file`. Списки вложений +появляются с первым `refresh` агента v14. + +Миграция v7 → v8 SHALL: удалить колонку `issue_note.fact_status` (если она есть; значения удаляются безвозвратно, копия — +в `backups/` по общему правилу) и ключи `status_labels` и `transitions_map` из `setting`, если они есть, и поднять +`settings_version`, если хотя бы один из ключей был удалён. Прочие поля заметок, задачи и настройки миграция MUST NOT +менять. + +Миграция v8 → v9 SHALL: добавить колонки `issue.rank` и `issue.story_points` (у имеющихся задач `NULL`); таблицы +`jira_board`, `jira_sprint`, `issue_sprint` создаются схемой пустыми. Данные появляются с первым `refresh` агента v15. + +#### Scenario: Первый запуск +- **WHEN** хаб стартует с несуществующим файлом базы +- **THEN** файл создаётся, таблицы и индексы на месте, `setting.schema_version` = текущая; каталог данных с правами `0700` + +#### Scenario: Миграция базы v1 +- **WHEN** хаб стартует с базой `schema_version = 1`, в которой есть задачи, заметки и команда в `claimed` +- **THEN** в `backups/` копия базы v1; добавлена `command.deliveries` (у `claimed` — 1, у остальных — 0); добавлена пустая + `issue_comment`; данные на месте; `schema_version` = текущая + +#### Scenario: Миграция базы v2 +- **WHEN** хаб стартует с базой `schema_version = 2`, в которой есть задачи, связи и заметки +- **THEN** в `backups/` копия базы v2; добавлена пустая `issue_comment`; данные на месте; `schema_version` = текущая + +#### 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` = текущая + +#### 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`; затем миграция v5 → v6; `schema_version` = текущая + +#### Scenario: Миграция базы v5 +- **WHEN** хаб стартует с базой `schema_version = 5` +- **THEN** в `backups/` копия базы v5; есть пустые `issue_attachment` и `attachment_file`; прочие таблицы не тронуты; + `schema_version` = текущая + +#### Scenario: Миграция базы v7 +- **WHEN** хаб стартует с базой `schema_version = 7`, у задачи X заметка `fact_status = "В работе"`, `work_kind = + "Аналитика"`, в `setting` есть `status_labels` и `transitions_map`, `settings_version = 12` +- **THEN** в `backups/` копия базы v7; в `issue_note` нет колонки `fact_status`, у X `work_kind = "Аналитика"`, + `updated_at` прежний; ключей `status_labels` и `transitions_map` в `setting` нет; `settings_version = 13`; + `schema_version` = текущая + +#### Scenario: Миграция базы v8 +- **WHEN** хаб стартует с базой `schema_version = 8`, в зеркале задача X со спринтом «Спринт 2» +- **THEN** в `backups/` копия базы v8; у X `rank` и `story_points` — `NULL`, `sprint` прежний; таблицы `jira_board`, + `jira_sprint`, `issue_sprint` пусты; `schema_version = 9` + +#### Scenario: refresh в очереди при миграции +- **WHEN** в базе v3 стоит `queued` `refresh {jql: "project = AURORA AND labels = AWPCA"}` +- **THEN** после миграции его `params.jql` — `project = AURORA AND labels in ("AWPCA")`, и по завершении он считается + полным прогоном + +#### Scenario: Два соединения к базе v1 +- **WHEN** к базе `schema_version = 1` одновременно открываются два соединения +- **THEN** миграция выполнена один раз, в `backups/` одна копия `schema-v1` + +#### Scenario: Открытый каталог данных +- **WHEN** хаб стартует, каталог данных существует с правами `0755` +- **THEN** права каталога и `backups/` — `0700` + +### Requirement: Зеркало задачи +Таблица `issue` SHALL хранить задачу Jira как есть: `key` (PK), `id`, `issue_type`, `summary`, `description` (wiki), +`status`, `priority`, `assignee`, `reporter`, `duedate`, `planned_start`, `planned_end`, `epic_key`, `labels` (JSON-список), +`sprint`, `rank` (ранг Jira строкой как есть или `NULL`), `story_points` (оценка, число или `NULL`), +`jira_updated`, `fetched_at`, `neighbor` (0/1 — вне базовой JQL, дочитана по связи), `created`. Таблица `issue_link` +SHALL хранить связи `(source_key, target_key, link_type)` с направлением по семантике Jira: `source` блокирует/ссылается +на `target` (у `source` в GET это `outwardIssue`). Таблица `issue_comment` SHALL хранить комментарии задачи как есть: +`key`, `comment_id` (PK вместе с `key`), `author`, `created`, `updated`, `body` (wiki). Таблица `issue_attachment` SHALL +хранить список вложений задачи без содержимого: `attachment_id` (PK, id вложения Jira), `key`, `filename`, `size` (байт +или `NULL`), `mime_type`, `author`, `created` (время Jira как есть), `created_at` (UTC, приведённое из `created`; не +разобралось — `NULL`). Обновлять `issue`, `issue_link`, `issue_comment` и +`issue_attachment` SHALL только загрузка состояния от агента; HTTP API интерфейсов и MCP MUST NOT писать в них. Присланные +комментарии задачи SHALL заменять все её прежние комментарии; задача, присланная без поля `comments`, свои комментарии +SHALL сохранять. Так же со вложениями: присланное поле `attachments` заменяет все прежние вложения задачи, задача без +поля (агент до v14) свои вложения сохраняет. Полный прогон (`refresh` по базовой JQL), завершившийся `done`, SHALL помечать `neighbor = 1` задачи, не +обновлённые им, — только если прогон загрузил хотя бы одну задачу. + +#### Scenario: Загрузка состояния заменяет связи +- **WHEN** агент прислал состояние задачи X с двумя связями +- **THEN** все строки `issue_link` с `source_key = X` заменены на присланные; строки с `target_key = X`, не присланные + со стороны X, не трогаются до загрузки их источника + +#### Scenario: Соседняя задача +- **WHEN** в `issuelinks` задачи из базовой JQL есть ключ Y вне JQL +- **THEN** Y сохраняется с `neighbor = 1`; при следующем полном `refresh`, если Y вошла в JQL, `neighbor = 0` + +#### Scenario: Пустой полный прогон +- **WHEN** полный `refresh` завершился `done`, загрузив 0 задач +- **THEN** признак `neighbor` задач зеркала не меняется + +#### Scenario: Комментарий удалён в Jira +- **WHEN** у X в зеркале комментарии 101 и 102, агент прислал X с `comments = [{id: "102", …}]` +- **THEN** у X в `issue_comment` только 102 + +#### Scenario: Состояние от агента до v10 +- **WHEN** у X в зеркале три комментария, агент v9 прислал X без поля `comments` +- **THEN** поля X обновлены, три комментария на месте + +#### Scenario: Вложения в состоянии +- **WHEN** у X в зеркале вложения #1 и #2, агент v14 прислал X с `attachments = [{id: "2", …}, {id: "3", …}]` +- **THEN** у X в `issue_attachment` #2 (поля из состояния) и #3; #1 удалено; `attachment_file` не тронута + +#### Scenario: Состояние от агента до v14 +- **WHEN** у X в зеркале два вложения, агент v13 прислал X без поля `attachments` +- **THEN** поля X обновлены, два вложения на месте + +## ADDED Requirements + +### Requirement: Доски, спринты и членство задач +Модуль jira SHALL хранить данные досок проектов, присланные агентом в `refresh` (см. `hub-api`, «Приём досок и +спринтов»): +- `jira_board`: `board_id` (PK), `type` (`scrum` | `kanban` | `NULL`), `columns` (JSON-список `[{name, statuses: + [имя статуса]}]` в порядке доски или `NULL`), `kanban_backlog` (1 / 0 / `NULL` — неизвестно), `fetched_at` (UTC); +- `jira_sprint`: `sprint_id` (PK, id спринта Jira), `board_id`, `name`, `state` (`active` | `future` | `closed`), + `start_date`, `end_date`, `complete_date` (строки Jira как есть или `NULL`), `goal`; +- `issue_sprint`: `key`, `sprint_id` (PK — пара) — все спринты задачи любого состояния. +Писать их SHALL только загрузка состояния от агента. Выполненной на доске SHALL считаться задача, чей `status` входит в +`statuses` последней колонки доски; колонок нет (`NULL`) — выполненных нет. + +#### Scenario: Задача прошла два спринта +- **WHEN** агент прислал задачу X с `sprint_ids: [101, 102]`, спринт 101 закрыт, 102 активен +- **THEN** у X две строки `issue_sprint`; X в составе закрытого спринта 101 и активного 102 + +#### Scenario: Колонки доски +- **WHEN** у доски 4216 колонки «К выполнению» [`Backlog`, `Ожидает анализа`], «В работе» […], «Готово» [`Готово`, + `Протестировано`, `Снято`] +- **THEN** задача в статусе `Протестировано` выполнена, в статусе `Backlog` — нет diff --git a/openspec/changes/sprints-backlog/specs/hub-api/spec.md b/openspec/changes/sprints-backlog/specs/hub-api/spec.md new file mode 100644 index 0000000..e918b26 --- /dev/null +++ b/openspec/changes/sprints-backlog/specs/hub-api/spec.md @@ -0,0 +1,54 @@ +## ADDED Requirements + +### Requirement: Приём досок и спринтов +`POST /agent/state` (агент v15) SHALL принимать необязательные поля: +- у задачи — `rank` (строка или `null`), `story_points` (число или `null`), `sprint_ids` (список целых); присланные + `rank` и `story_points` записываются в `issue`; присланный `sprint_ids` заменяет все строки `issue_sprint` задачи; + задача без поля свои значения сохраняет; +- в пачке — `boards`: список `{id, type, columns, kanban_backlog, sprints}`; `columns` — список `{name, statuses}` или + `null`, `kanban_backlog` — `true`/`false`/`null`, `sprints` — список `{id, name, state, start_date, end_date, + complete_date, goal}`. Доска из `boards` SHALL заменять свою строку `jira_board` и все строки `jira_sprint` этой + доски; доски, которых нет в `boards`, не меняются. +Форму хаб SHALL проверять до записи, как `comments`: нарушение — 422 `bad_state` с ключом задачи или номером доски и +именем поля, пачка не применяется. `state` спринта вне `active`/`future`/`closed`, `sprint_ids` не список целых (в том +числе `null`) — нарушение; `rank: null` и `story_points: null` — очистить поле. Тип доски из `boards` (`scrum` / +`kanban`) хаб SHALL записывать и в `jira_project.board_type` проектов этой доски; `null` тип не меняет. Пачка может +быть пустой (`issues: []`) и нести только `boards`. + +#### Scenario: Пачка с досками +- **WHEN** пачка `refresh` несёт `boards: [{id: 4216, type: "scrum", columns: […], kanban_backlog: null, sprints: [101 + closed, 102 active, 103 future]}]`, раньше у доски были спринты 101 и 102 +- **THEN** у доски 4216 строки `jira_sprint` 101, 102, 103 с присланными полями; колонки заменены + +#### Scenario: Агент v14 +- **WHEN** пачка без `boards`, задачи без `rank`, `story_points`, `sprint_ids` +- **THEN** данные досок и эти поля задач не меняются + +### Requirement: Маршрут планирования +`GET /agile?project=<метка>` (клиентский токен) SHALL отдавать для проекта (неизвестный — 404; без `project` — 422): +`{board, sprints, backlog, board_issues}`. +- `board` — `{id, type, columns, kanban_backlog, fetched_at}` доски проекта; доски нет — `null` (остальные поля — + `[]`/`null`); доска есть, а её данных от агента нет — `{id, type, columns: null, kanban_backlog: null, fetched_at: + null}`. +- `sprints` — все спринты доски: `{id, name, state, start_date, end_date, complete_date, goal, count, count_done, + unestimated, points, points_done, issues}`; `issues` — свои задачи проекта в этом спринте (по `issue_sprint`); + `count`/`count_done` — их число и число выполненных, `points`/`points_done` — сумма `story_points` всех и выполненных, + `unestimated` — число без оценки. Порядок: `active` по `start_date`, `future` по id, `closed` по `complete_date`, новые + сверху. У kanban-доски — `[]`. +- `backlog` — scrum: свои задачи не в активном и не в будущем спринте и не выполненные; kanban: свои задачи в статусах + первой колонки, если `kanban_backlog = true`, иначе `null`. +- `board_issues` — только kanban: свои задачи в статусах остальных колонок (при `kanban_backlog` не `true` — и + первой) и без колонки; выполненные — только с + `jira_updated` не старше 14 дней; scrum — `null`. +Строка задачи — `{key, summary, issue_type, status, priority, assignee, epic_key, story_points, rank, done}`; списки — по +`rank` (строки сравниваются как есть; `null` — в конце), затем по `key`. + +#### Scenario: Спринт и бэклог +- **WHEN** у `AWPCA` активный спринт 102 с задачами A (3 SP, в последней колонке), B (без оценки), в бэклоге C и + выполненная D без спринта +- **THEN** спринт 102: `count = 2`, `count_done = 1`, `points = 3`, `points_done = 3`, `unestimated = 1`; `backlog` — + только C + +#### Scenario: Проект без доски +- **WHEN** `GET /agile?project=STOCKS_DRRK`, у проекта нет доски +- **THEN** 200, `board = null`, `sprints = []`, `backlog = null`, `board_issues = null` diff --git a/openspec/changes/sprints-backlog/specs/mcp/spec.md b/openspec/changes/sprints-backlog/specs/mcp/spec.md new file mode 100644 index 0000000..67cceac --- /dev/null +++ b/openspec/changes/sprints-backlog/specs/mcp/spec.md @@ -0,0 +1,22 @@ +## ADDED Requirements + +### Requirement: Спринты и бэклог в MCP +MCP `sdlc-jira` SHALL давать инструменты чтения по `GET /agile` проекта сессии (сессия без проекта — тот же отказ, что у +прочих инструментов с проектом): +- `sprints` — таблица спринтов доски: имя, состояние, даты, задач (выполнено/всего), SP (выполнено/всего), без оценки; + закрытые — 10 последних и строка «ещё N закрытых», если их больше; без доски — «у проекта нет доски»; +- `sprint` (`name`, по умолчанию — активный) — шапка спринта (имя, состояние, даты, цель, суммы) и задачи по колонкам + доски: ключ, тема (до 80 символов), исполнитель, SP; +- `backlog` (`limit`, по умолчанию 50) — бэклог по рангу: позиция, ключ, тема, статус, исполнитель, SP; в конце «ещё N», + если задач больше `limit`. +Таблица `issues` SHALL получить колонку «SP». Описание `batch_create` SHALL перечислять `ensure_sprint_meta`, +`ensure_sprint_state`, `ensure_rank` и `story_points` в `ensure_fields` с параметрами; хвостовой `refresh` хаб +дописывает сам (см. `commands`). + +#### Scenario: Спринты проекта +- **WHEN** сессия привязана к `AWPCA`, вызван `sprints` +- **THEN** строки: активный `AWPCA_Sprint_3_Corrections`, будущие, затем закрытые; у каждой — задачи и SP «выполнено/всего» + +#### Scenario: Бэклог с лимитом +- **WHEN** `backlog {limit: 2}`, в бэклоге 188 задач +- **THEN** две строки по рангу и «ещё 186» diff --git a/openspec/changes/sprints-backlog/specs/web-app/spec.md b/openspec/changes/sprints-backlog/specs/web-app/spec.md new file mode 100644 index 0000000..2a6edac --- /dev/null +++ b/openspec/changes/sprints-backlog/specs/web-app/spec.md @@ -0,0 +1,186 @@ +## MODIFIED Requirements + +### Requirement: Оболочка и адреса +Приложение SHALL открываться под префиксом `/app/`. Главная страница — «Обзор» ядра (`/app/?project=<метка>`, +требование «Обзор проекта»). Страницы модулей SHALL лежать под префиксом имени модуля: модуль jira — `/app/jira/links` +(«Связи» — доска связей), `/app/jira/backlog` («Бэклог»), `/app/jira/sprint` («Спринт»), `/app/jira/kanban` («Kanban»; +три последних — требование «Спринты и бэклог»), `/app/jira/issues` (список задач), `/app/jira/feed` (лента изменений, требование «Лента изменений») и +`/app/jira/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); модуль wiki — +`/app/wiki` (требование «Вики проекта»). Прежние адреса `/app/board` и `/app/jira/board` (на `/app/jira/links`), +`/app/issues`, `/app/issues/{key}`, `/app/feed` SHALL перенаправлять на новые с сохранением строки запроса, без новой записи истории. Неизвестный путь — страница «не +найдено» со ссылками на «Обзор» и разделы модулей. + +Боковая панель SHALL быть закреплена у левого края окна на всю его высоту и MUST NOT прокручиваться вместе со +страницей; если её содержимое не помещается по высоте, прокручивается сама панель. Сверху вниз панель SHALL содержать: +название «SDLC Hub» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий отмечен, в +конце «+ Новый проект…»; требование «Проект в приложении»); пункт «Обзор»; разделы модулей группами — заголовок группы +— название модуля («Jira», «Вики»), под ним ссылки разделов модуля со значками (текущий отмечен +`aria-current="page"`); внизу — блок состояния модулей (требование «Состояние и обновление модулей»), переключатель +темы (требование «Тема») и меню с действием «Сменить токен». Свёрнутая панель SHALL быть узкой полосой с теми же +действиями значками: «Развернуть панель», метка текущего проекта (полностью — в подсказке), «Обзор», разделы +(группы разделены линией), по значку состояния на модуль, тема, меню; названия — в подсказках и `aria-label`. Свёртка +SHALL храниться в `localStorage` под ключом `sdlc-hub-sidebar` (`collapsed`; без записи — развёрнута). + +На экранах уже 640 px панели в потоке нет: сверху SHALL быть закреплена строка с кнопкой «Меню», меткой текущего +проекта и сводной точкой состояния модулей (ошибка, если она есть у какого-нибудь модуля; иначе — пакет в работе; +иначе — все агенты на связи или нет), которая открывает панель. «Меню» открывает развёрнутую панель поверх страницы +модальным окном: фокус переходит в панель и остаётся в ней, страница под ней недоступна; панель закрывается выбором +раздела, клавишей Escape (если не открыто вложенное меню — тогда Escape закрывает только его) или щелчком мимо неё; фокус +возвращается на «Меню». Горизонтальной прокрутки страницы на любой ширине нет. + +Заголовок вкладки браузера SHALL быть «<название страницы> · <метка проекта> · SDLC Hub» (без проекта — без средней +части); название страницы даёт оболочка («Обзор») или модуль. Неожиданная ошибка отрисовки страницы SHALL показываться +сообщением с кнопкой «Перезагрузить», не пустым экраном. + +#### Scenario: Прежний адрес доски связей +- **WHEN** открыт `/app/jira/board?project=AWPCA` +- **THEN** адрес — `/app/jira/links?project=AWPCA`, показана доска связей, в меню отмечен пункт «Связи», заголовок + вкладки — «Связи · AWPCA · SDLC Hub» + +#### Scenario: Переход между разделами +- **WHEN** на доске в боковой панели выбран раздел «Задачи» группы «Jira» +- **THEN** адрес — `/app/jira/issues` с текущим проектом, показан список задач; «назад» возвращает на доску + +#### Scenario: Раздел «Лента» +- **WHEN** на доске в боковой панели выбран раздел «Лента» +- **THEN** адрес — `/app/jira/feed` с текущим проектом, показана лента изменений за сегодня; раздел отмечен в панели + +#### Scenario: Сменить токен +- **WHEN** в меню боковой панели выбрано «Сменить токен» +- **THEN** токен очищен, показан экран входа + +#### Scenario: Панель на месте при прокрутке +- **WHEN** доска прокручена до самого низа +- **THEN** боковая панель видна целиком на прежнем месте: разделы и блок состояния модулей доступны без прокрутки вверх + +#### Scenario: Свёрнутая панель +- **WHEN** нажата «Свернуть панель», страница перезагружена +- **THEN** панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; видны два значка + состояния — Jira и Вики; «Развернуть панель» возвращает полную панель + +#### Scenario: Узкий экран +- **WHEN** ширина окна 390 px, нажата «Меню», в панели выбран раздел «Задачи» +- **THEN** панель открылась поверх страницы, фокус в панели; после выбора панель закрыта, фокус на «Меню», показан список + задач; горизонтальной прокрутки нет + +#### Scenario: Прежний адрес +- **WHEN** открыт `/app/issues/AURORA-1536?project=AWPCA` +- **THEN** адрес заменён на `/app/jira/issues/AURORA-1536?project=AWPCA`, карточка открыта поверх списка задач + +#### Scenario: Заголовок вкладки +- **WHEN** открыта страница «Вики» проекта `AWPCA` +- **THEN** заголовок вкладки — «Вики · AWPCA · SDLC Hub» + +### Requirement: Проект в приложении +«Обзор», доска, список задач, лента и вики SHALL показывать один проект — текущий. Текущий проект SHALL задаваться +параметром `project` в адресе страниц; без параметра — последний выбранный в этом браузере (`localStorage`), если он +есть в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес без новой записи истории. +Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» и подсказку выбрать +проект в списке «Проект» боковой панели (на узком экране — в «Меню») вместо данных. Проектов нет — страницы SHALL +показывать приглашение создать проект («+ Новый проект…»). Переходы между страницами SHALL сохранять текущий проект; +смена проекта на странице, которой нет у нового проекта, открывает «Обзор». Карточка задачи +(`/app/jira/issues/{key}`) от проекта не зависит. + +Доска SHALL загружать `GET /board?project=<текущий>`. Раскладка, дорожки и фильтры доски в `localStorage` SHALL +храниться отдельно для каждого проекта; у проекта `AWPCA` — на прежних ключах (раскладка, сделанная до проектов, +сохраняется). Фильтр спринтов SHALL показываться, только если хотя бы у одной своей задачи снимка (`neighbor = false`) +есть спринт; иначе — «колонки «Спринт» нет — показаны все задачи». + +Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка (с +подсказкой «метка задач в Jira и имя проекта в хабе») и название, затем поля каждого модуля в своей группе с +заголовком — названием модуля: «Jira» — номер доски Jira (необязательно; подсказка «scrum или kanban определится +сам»), «Вики» — корень вики (необязательно; подсказка «DOC-… или ссылка на страницу Eva»). Подсказка — текстом под +полем, не в placeholder. Пустое необязательное поле в запрос не попадает; числовое поле с нецелым значением — ошибка у +поля, запрос не уходит. «Создать» — `POST /projects` с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с +кнопкой, окно остаётся открытым; успех — окно закрывается, текущим становится новый проект, каждый пакет из ответа +(`batches`) отслеживается своим модулем (требование «Состояние и обновление модулей»). + +#### Scenario: Первый вход после выкатки +- **WHEN** открыт `/app/jira/links` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK` +- **THEN** адрес — `/app/jira/links?project=AWPCA`, доска — `GET /board?project=AWPCA` с прежней раскладкой карточек + +#### Scenario: Переключение проекта +- **WHEN** на доске `AWPCA` в выпадающем списке выбран `STOCKS_DRRK`, затем открыт раздел «Задачи» +- **THEN** доска — `GET /board?project=STOCKS_DRRK` со своей раскладкой; адрес «Задач» — + `/app/jira/issues?project=STOCKS_DRRK` + +#### Scenario: Неизвестный проект в адресе +- **WHEN** открыт `/app/jira/issues?project=NOPE` +- **THEN** «проект NOPE не найден» и подсказка выбрать проект в боковой панели; запроса `GET /issues` нет + +#### Scenario: Создание проекта из формы +- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», доска и корень вики пусто; нажато «Создать» +- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", author: "viewer"}`; текущий проект — `STOCKS_DRRK`; + строка «Jira» блока состояния показывает «пакет #N выполняется» + +#### Scenario: Проект с корнем вики из формы +- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», корень `DOC-001234`; нажато «Создать» +- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234", author: "viewer"}`; строка + «Jira» показывает пакет обновления, строка «Вики» — пакет синхронизации + +#### Scenario: Метка занята +- **WHEN** в форме введена метка `awpca`, хаб ответил 409 +- **THEN** окно открыто, рядом с «Создать» — текст ошибки хаба + +#### Scenario: Канбан-проект +- **WHEN** текущий проект — `STOCKS_DRRK`, ни у одной своей задачи снимка нет спринта (у соседа из `AWPCA` спринт есть) +- **THEN** вместо фильтра спринтов — «колонки «Спринт» нет — показаны все задачи» + +#### Scenario: Нецелый номер доски +- **WHEN** в поле «Номер доски Jira» введено `51.5`, нажато «Создать» +- **THEN** у поля — «нужно целое число», запроса `POST /projects` нет + +## ADDED Requirements + +### Requirement: Спринты и бэклог +Модуль jira SHALL показывать для проекта с доской страницы по образцу Jira — только для просмотра: на них MUST NOT быть +действий, меняющих Jira (перетаскивания, кнопок спринтов, смены статуса или ранга). Пункты меню группы «Jira» — +«Связи», «Задачи», «Лента» и по типу доски текущего проекта (`board_type` из `GET /projects`): `scrum` — «Бэклог» и +«Спринт»; `kanban` — «Kanban»; доски нет или тип неизвестен — новых пунктов нет, а прямой переход на их адрес +показывает «У проекта нет scrum-доски» («…kanban-доски» для «Kanban») со ссылкой на «Связи». Данные — `GET +/agile?project=<текущий>`; если доска есть, но её данных нет (`board.columns = null` или ответ без доски) — «Нужен агент +v15 и обновление из Jira» с кнопкой «Обновить» блока состояния модуля; данные доски есть, а колонок нет — «Не удалось +прочитать настройки доски из Jira» и та же кнопка. Пока проекты не загружены, условные пункты меню скрыты; смена проекта +на странице, которой у нового проекта нет, оставляет адрес и показывает сообщение «У проекта нет …-доски». Даты +спринтов (ISO-8601 Jira) показываются «ДД.ММ» в часовом поясе браузера; «осталось N дн.» — от сегодня до конца спринта. + +Строка задачи и карточка задачи на этих страницах: ключ, значок типа, тема, исполнитель (или «—»), оценка (число или +«—»); выполненная — приглушена. Щелчок открывает карточку задачи (окно поверх страницы, как в списке задач). + +**«Бэклог»** (`/app/jira/backlog`), сверху вниз: секции активных спринтов, будущих спринтов, «Бэклог», затем +свёрнутый список «Прошлые спринты (N)». Заголовок секции спринта: имя, состояние, даты «ДД.ММ–ДД.ММ», цель (если есть), +«N задач», «S SP (выполнено D)» и «без оценки: U», если `U > 0`. Задачи секции — по рангу. Секция без задач показывает +«Нет задач». Прошлый спринт в списке — строка «имя · закрыт ДД.ММ · выполнено X из N · S SP»; раскрытие показывает его +задачи с пометкой «не выполнено» у невыполненных. + +**«Спринт»** (`/app/jira/sprint`): активный спринт доски — заголовок (имя, даты, «осталось N дн.» или «срок прошёл», +цель, суммы оценок) и колонки доски Jira с числом задач, карточки в колонке — по рангу; колонка «Без колонки» — для +статусов вне настроек доски, только если в ней есть задачи. Несколько активных спринтов — переключатель спринтов над +доской (выбор в адресе `?sprint=`). Активного нет — «Нет активного спринта» и ссылка на «Бэклог». + +**«Kanban»** (`/app/jira/kanban`): колонки доски из `board_issues`, карточки по рангу, у последней колонки подпись +«за 14 дней»; если `kanban_backlog = true` — над колонками свёрнутый список «Бэклог (N)» по рангу. + +Колонки, не помещающиеся по ширине, SHALL прокручиваться по горизонтали внутри доски, не страницы. Заголовок вкладки — +«Бэклог», «Спринт», «Kanban» по общему правилу оболочки. + +#### Scenario: Меню scrum-проекта +- **WHEN** текущий проект `AWPCA`, `board_type = scrum` +- **THEN** в группе «Jira» пункты «Связи», «Бэклог», «Спринт», «Задачи», «Лента»; пункта «Kanban» нет + +#### Scenario: Проект без доски +- **WHEN** текущий проект `STOCKS_DRRK` без доски, открыт `/app/jira/backlog?project=STOCKS_DRRK` +- **THEN** пунктов «Бэклог», «Спринт», «Kanban» в меню нет; страница — «У проекта нет scrum-доски» со ссылкой на «Связи» + +#### Scenario: Бэклог только смотрит +- **WHEN** открыт «Бэклог» `AWPCA` +- **THEN** на странице нет перетаскиваемых элементов и кнопок, кроме раскрытия секций и открытия карточки задачи + +#### Scenario: Прошлый спринт +- **WHEN** спринт `AWPCA_Sprint_2_VGO` закрыт, в нём 73 задачи, 61 в последней колонке, оценки у 10 задач на 25 SP +- **THEN** в «Прошлые спринты» строка «AWPCA_Sprint_2_VGO · закрыт ДД.ММ · выполнено 61 из 73 · 25 SP»; раскрытие — + 73 задачи, 12 с пометкой «не выполнено» + +#### Scenario: Агент без досок +- **WHEN** `AWPCA` scrum, агент v14 (данных доски нет), открыт «Спринт» +- **THEN** «Нужен агент v15 и обновление из Jira» и кнопка «Обновить»