OpenSpec: change sprints-backlog (#108)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-24 18:06:15 +04:00
parent a2bf3fce0b
commit 15db39d57d
9 changed files with 803 additions and 0 deletions

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-24

View File

@ -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: <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, переименование «Доска связей». Цена ошибки — правка веба.

View File

@ -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`, если агент поле не нашёл.

View File

@ -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»

View File

@ -0,0 +1,129 @@
## MODIFIED Requirements
### Requirement: Команда — желаемый результат на одной задаче
Каждая команда SHALL описывать желаемое состояние одной задачи Jira и выполняться агентом как «прочитать → сравнить →
записать только разницу». Повторное выполнение любой команды с тем же результатом в Jira SHALL завершаться `done`
без записи. `fetch_attachment` в Jira не пишет: она читает файл вложения и передаёт его хабу; повтор безопасен. Типы команд и их контракт:
| Тип | Параметры | Что читает | Что пишет |
|---|---|---|---|
| `ensure_issue` | `temp_key` (`new:<n>`), `issue_type` (`task`/`epic`/`bug`), `summary`, `description`, `priority`, `duedate`, `planned_start`, `planned_end`, `labels`, `epic_key`, `assignee` | поиск `project = <proj> AND reporter = currentUser() AND created >= -7d`, точное сравнение `summary` | при отсутствии — `POST /issue` с типом из `issue_types.<issue_type>` (для эпика — с `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: {"<id>": "scrum" | "kanban" | null}` для досок, чей тип агент запрашивал |
| `fetch_attachment` | `key`, `attachment_id` (id вложения Jira, строка из цифр) | `GET /issue/{key}?fields=attachment` (имя и размер вложения) | ничего в Jira; содержимое — `GET <base_url>/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: <JQL проекта пакета>}` (`project =
<project_key> AND labels in ("<метка>")`); иначе — как раньше, по ключам команд. Подтверждения по состоянию зеркала у
`ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank` нет.
`ensure_sprint_meta` SHALL искать спринт по имени среди всех спринтов доски: закрытый — `failed` «спринт закрыт»;
несколько с этим именем — `failed` «несколько спринтов с именем …: <id…>»; `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: <sha>, 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

View File

@ -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 = <M>` или `… labels in (<M1>, …)` и завести проекты с этими метками (название проекта
`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` — нет

View File

@ -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`

View File

@ -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»

View File

@ -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=<id>`). Активного нет — «Нет активного спринта» и ссылка на «Бэклог».
**«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» и кнопка «Обновить»