From 30833a466a810c532f62d0dfcc32cfab3e42ee4a Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 25 Oct 2026 15:24:17 +0400 Subject: [PATCH] =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D0=B2=20OpenSpec:=20sp?= =?UTF-8?q?rints-backlog=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../.openspec.yaml | 0 .../2026-10-25-sprints-backlog}/design.md | 0 .../2026-10-25-sprints-backlog}/proposal.md | 0 .../specs/agent/spec.md | 0 .../specs/commands/spec.md | 0 .../specs/data-model/spec.md | 0 .../specs/hub-api/spec.md | 0 .../specs/mcp/spec.md | 0 .../specs/web-app/spec.md | 0 openspec/specs/agent/spec.md | 27 +++++++ openspec/specs/commands/spec.md | 37 +++++++++- openspec/specs/data-model/spec.md | 37 +++++++++- openspec/specs/hub-api/spec.md | 53 +++++++++++++ openspec/specs/mcp/spec.md | 21 ++++++ openspec/specs/web-app/spec.md | 74 +++++++++++++++++-- 15 files changed, 236 insertions(+), 13 deletions(-) rename openspec/changes/{sprints-backlog => archive/2026-10-25-sprints-backlog}/.openspec.yaml (100%) rename openspec/changes/{sprints-backlog => archive/2026-10-25-sprints-backlog}/design.md (100%) rename openspec/changes/{sprints-backlog => archive/2026-10-25-sprints-backlog}/proposal.md (100%) rename openspec/changes/{sprints-backlog => archive/2026-10-25-sprints-backlog}/specs/agent/spec.md (100%) rename openspec/changes/{sprints-backlog => archive/2026-10-25-sprints-backlog}/specs/commands/spec.md (100%) rename openspec/changes/{sprints-backlog => archive/2026-10-25-sprints-backlog}/specs/data-model/spec.md (100%) rename openspec/changes/{sprints-backlog => archive/2026-10-25-sprints-backlog}/specs/hub-api/spec.md (100%) rename openspec/changes/{sprints-backlog => archive/2026-10-25-sprints-backlog}/specs/mcp/spec.md (100%) rename openspec/changes/{sprints-backlog => archive/2026-10-25-sprints-backlog}/specs/web-app/spec.md (100%) diff --git a/openspec/changes/sprints-backlog/.openspec.yaml b/openspec/changes/archive/2026-10-25-sprints-backlog/.openspec.yaml similarity index 100% rename from openspec/changes/sprints-backlog/.openspec.yaml rename to openspec/changes/archive/2026-10-25-sprints-backlog/.openspec.yaml diff --git a/openspec/changes/sprints-backlog/design.md b/openspec/changes/archive/2026-10-25-sprints-backlog/design.md similarity index 100% rename from openspec/changes/sprints-backlog/design.md rename to openspec/changes/archive/2026-10-25-sprints-backlog/design.md diff --git a/openspec/changes/sprints-backlog/proposal.md b/openspec/changes/archive/2026-10-25-sprints-backlog/proposal.md similarity index 100% rename from openspec/changes/sprints-backlog/proposal.md rename to openspec/changes/archive/2026-10-25-sprints-backlog/proposal.md diff --git a/openspec/changes/sprints-backlog/specs/agent/spec.md b/openspec/changes/archive/2026-10-25-sprints-backlog/specs/agent/spec.md similarity index 100% rename from openspec/changes/sprints-backlog/specs/agent/spec.md rename to openspec/changes/archive/2026-10-25-sprints-backlog/specs/agent/spec.md diff --git a/openspec/changes/sprints-backlog/specs/commands/spec.md b/openspec/changes/archive/2026-10-25-sprints-backlog/specs/commands/spec.md similarity index 100% rename from openspec/changes/sprints-backlog/specs/commands/spec.md rename to openspec/changes/archive/2026-10-25-sprints-backlog/specs/commands/spec.md diff --git a/openspec/changes/sprints-backlog/specs/data-model/spec.md b/openspec/changes/archive/2026-10-25-sprints-backlog/specs/data-model/spec.md similarity index 100% rename from openspec/changes/sprints-backlog/specs/data-model/spec.md rename to openspec/changes/archive/2026-10-25-sprints-backlog/specs/data-model/spec.md diff --git a/openspec/changes/sprints-backlog/specs/hub-api/spec.md b/openspec/changes/archive/2026-10-25-sprints-backlog/specs/hub-api/spec.md similarity index 100% rename from openspec/changes/sprints-backlog/specs/hub-api/spec.md rename to openspec/changes/archive/2026-10-25-sprints-backlog/specs/hub-api/spec.md diff --git a/openspec/changes/sprints-backlog/specs/mcp/spec.md b/openspec/changes/archive/2026-10-25-sprints-backlog/specs/mcp/spec.md similarity index 100% rename from openspec/changes/sprints-backlog/specs/mcp/spec.md rename to openspec/changes/archive/2026-10-25-sprints-backlog/specs/mcp/spec.md diff --git a/openspec/changes/sprints-backlog/specs/web-app/spec.md b/openspec/changes/archive/2026-10-25-sprints-backlog/specs/web-app/spec.md similarity index 100% rename from openspec/changes/sprints-backlog/specs/web-app/spec.md rename to openspec/changes/archive/2026-10-25-sprints-backlog/specs/web-app/spec.md diff --git a/openspec/specs/agent/spec.md b/openspec/specs/agent/spec.md index e23cd78..819a11a 100644 --- a/openspec/specs/agent/spec.md +++ b/openspec/specs/agent/spec.md @@ -516,3 +516,30 @@ from_string, to, to_string}`: `history_id` — id записи строкой; ` - **WHEN** агент с `hub_url = folder:…` выполняет `fetch_attachment` вложения 3 МБ - **THEN** содержимое уходит хабу одним запросом `POST /agent/files` через папку (тело ≈ 4 МБ base64); отчёт `done` с `file` и `size`; в `journal.jsonl` нет содержимого файла + +### 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`, +`kanban_backlog: null`; ошибка чтения списка спринтов scrum-доски — доска в `boards` не входит (хаб сохраняет прежние +данные), имена спринтов задач — по правилам v12; причина в лог; `refresh` продолжается. + +Каждая задача в состоянии (с v15) SHALL нести `rank` — значение поля ранга Jira (поле типа LexoRank), `story_points` — +значение поля оценки (`field_ids.story_points`, если задано, иначе поле Jira с именем «Story Points»), и `sprint_ids` — +id всех спринтов задачи из поля спринта Jira (любого состояния). Поле не найдено — ключ у задач не передаётся (хаб +сохраняет прежнее значение) и запись в лог один раз за `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`; `ensure_fields {key: X, story_points: 3}` — `failed` «поле оценки не найдено + в Jira: задайте field_ids.story_points» diff --git a/openspec/specs/commands/spec.md b/openspec/specs/commands/spec.md index 26fecef..53a399d 100644 --- a/openspec/specs/commands/spec.md +++ b/openspec/specs/commands/spec.md @@ -13,11 +13,14 @@ | Тип | Параметры | Что читает | Что пишет | |---|---|---|---| | `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` | `GET /issue/{key}` | `PUT /issue/{key}` только с отличающимися полями; эпик — по `epic_link.mode` | +| `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}` | @@ -30,6 +33,20 @@ не хранится): все доски проектов без повторов. По результату `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). @@ -132,7 +149,7 @@ ### Requirement: Политика темпа и пауза на команде Хаб SHALL хранить `pacing` как минимальные зазоры в секундах по типу команды (по умолчанию: `ensure_issue` и -`ensure_fields` 120, `ensure_comment`, `ensure_status`, `ensure_link`, `ensure_sprint`, `ensure_attachment` 30, `refresh` и `fetch_attachment` 2; тип, которого нет в +`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`, путь переходов) агент @@ -142,6 +159,22 @@ SHALL выдерживать зазор типа команды между за - **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 + ### Requirement: Ошибки и подтверждение по состоянию Ошибка команды SHALL записываться в `error` (HTTP-код, текст Jira); хаб SHALL хранить не больше 1500 символов `error`, обрезая присланное. Пакет после ошибки продолжается. diff --git a/openspec/specs/data-model/spec.md b/openspec/specs/data-model/spec.md index 06a8798..37a78d7 100644 --- a/openspec/specs/data-model/spec.md +++ b/openspec/specs/data-model/spec.md @@ -7,8 +7,8 @@ ### Requirement: Схема базы хаба База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_comment`, `issue_change`, `issue_attachment`, -`attachment_file`, `issue_note`, `batch`, `command`, `sync_run`, `agent_status`, `setting`, `project`. Схема SHALL создаваться и мигрироваться самим хабом при -старте по номеру версии в `setting.schema_version` (текущая — 8). Миграция существующей базы SHALL выполняться в одной +`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` (в базе токены): хаб выставляет их @@ -37,6 +37,9 @@ Jira как есть (`issue_comment.created`/`updated`, `issue_change.created`, `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` @@ -73,7 +76,12 @@ Jira как есть (`issue_comment.created`/`updated`, `issue_change.created`, "Аналитика"`, в `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 = 8` + `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"}` @@ -91,7 +99,8 @@ Jira как есть (`issue_comment.created`/`updated`, `issue_change.created`, ### 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`, `jira_updated`, `fetched_at`, `neighbor` (0/1 — вне базовой JQL, дочитана по связи), `created`. Таблица `issue_link` +`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 @@ -376,3 +385,23 @@ SHALL вычисляться из проектов: `project = AND #### Scenario: Список вложений обновился - **WHEN** вложение #31337 скачано (`state = ready`), затем пришло состояние задачи с тем же вложением - **THEN** `state` по-прежнему `ready` + +### 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/specs/hub-api/spec.md b/openspec/specs/hub-api/spec.md index b1997f9..9e25466 100644 --- a/openspec/specs/hub-api/spec.md +++ b/openspec/specs/hub-api/spec.md @@ -737,3 +737,56 @@ from_string, to, to_string}`. До записи хаб SHALL проверить #### Scenario: Неизвестный модуль - **WHEN** `GET /batches?module=nope` - **THEN** 404 + +### 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/specs/mcp/spec.md b/openspec/specs/mcp/spec.md index d9d1e1b..c145586 100644 --- a/openspec/specs/mcp/spec.md +++ b/openspec/specs/mcp/spec.md @@ -291,3 +291,24 @@ HTTP API хаба клиентским токеном из настроек; с #### Scenario: Сервер jira не изменился - **WHEN** запущен `sdlc-hub mcp jira --project AWPCA` - **THEN** имя сервера `sdlc-jira`, список инструментов и описания совпадают с эталоном до change + +### 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/specs/web-app/spec.md b/openspec/specs/web-app/spec.md index 6a617d6..3c32ca9 100644 --- a/openspec/specs/web-app/spec.md +++ b/openspec/specs/web-app/spec.md @@ -41,11 +41,12 @@ SHALL очищать токен и показывать экран входа. ### Requirement: Оболочка и адреса Приложение SHALL открываться под префиксом `/app/`. Главная страница — «Обзор» ядра (`/app/?project=<метка>`, -требование «Обзор проекта»). Страницы модулей SHALL лежать под префиксом имени модуля: модуль jira — `/app/jira/board` -(доска связей), `/app/jira/issues` (список задач), `/app/jira/feed` (лента изменений, требование «Лента изменений») и +требование «Обзор проекта»). Страницы модулей 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/issues`, `/app/issues/{key}`, `/app/feed` -SHALL перенаправлять на новые с сохранением строки запроса, без новой записи истории. Неизвестный путь — страница «не +`/app/wiki` (требование «Вики проекта»). Прежние адреса `/app/board` и `/app/jira/board` (на `/app/jira/links`), +`/app/issues`, `/app/issues/{key}`, `/app/feed` SHALL перенаправлять на новые с сохранением строки запроса, без новой записи истории. Неизвестный путь — страница «не найдено» со ссылками на «Обзор» и разделы модулей. Боковая панель SHALL быть закреплена у левого края окна на всю его высоту и MUST NOT прокручиваться вместе со @@ -70,6 +71,11 @@ SHALL храниться в `localStorage` под ключом `sdlc-hub-sidebar части); название страницы даёт оболочка («Обзор») или модуль. Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не пустым экраном. +#### Scenario: Прежний адрес доски связей +- **WHEN** открыт `/app/jira/board?project=AWPCA` +- **THEN** адрес — `/app/jira/links?project=AWPCA`, показана доска связей, в меню отмечен пункт «Связи», заголовок + вкладки — «Связи · AWPCA · SDLC Hub» + #### Scenario: Переход между разделами - **WHEN** на доске в боковой панели выбран раздел «Задачи» группы «Jira» - **THEN** адрес — `/app/jira/issues` с текущим проектом, показан список задач; «назад» возвращает на доску @@ -489,7 +495,8 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» и подсказку выбрать проект в списке «Проект» боковой панели (на узком экране — в «Меню») вместо данных. Проектов нет — страницы SHALL показывать приглашение создать проект («+ Новый проект…»). Переходы между страницами SHALL сохранять текущий проект; -смена проекта на странице, которой нет у нового проекта, открывает «Обзор». Карточка задачи +смена проекта на странице, которой нет у нового проекта, открывает «Обзор»; исключение — «Бэклог», «Спринт», «Kanban»: +адрес остаётся, страница показывает «У проекта нет …-доски» (требование «Спринты и бэклог»). Карточка задачи (`/app/jira/issues/{key}`) от проекта не зависит. Доска SHALL загружать `GET /board?project=<текущий>`. Раскладка, дорожки и фильтры доски в `localStorage` SHALL @@ -507,8 +514,8 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха (`batches`) отслеживается своим модулем (требование «Состояние и обновление модулей»). #### Scenario: Первый вход после выкатки -- **WHEN** открыт `/app/jira/board` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK` -- **THEN** адрес — `/app/jira/board?project=AWPCA`, доска — `GET /board?project=AWPCA` с прежней раскладкой карточек +- **WHEN** открыт `/app/jira/links` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK` +- **THEN** адрес — `/app/jira/links?project=AWPCA`, доска — `GET /board?project=AWPCA` с прежней раскладкой карточек #### Scenario: Переключение проекта - **WHEN** на доске `AWPCA` в выпадающем списке выбран `STOCKS_DRRK`, затем открыт раздел «Задачи» @@ -819,3 +826,56 @@ Jira» — `POST /refresh {full: true, author: "viewer"}`; после пакет #### Scenario: Дубль адреса раздела - **WHEN** два модуля объявляют раздел с одинаковым `to` - **THEN** тесты фронта падают с адресом и модулями + +### Requirement: Спринты и бэклог +Модуль jira SHALL показывать для проекта с доской страницы по образцу Jira — только для просмотра: на них MUST NOT быть +действий, меняющих Jira (перетаскивания, кнопок спринтов, смены статуса или ранга). Пункты меню группы «Jira» — +«Связи», «Задачи», «Лента» и по типу доски текущего проекта (`board_type` из `GET /projects`): `scrum` — «Бэклог» и +«Спринт»; `kanban` — «Kanban»; доски нет или тип неизвестен — новых пунктов нет, а прямой переход на их адрес +показывает «У проекта нет scrum-доски» («…kanban-доски» для «Kanban») со ссылкой на «Связи». Данные — `GET +/agile?project=<текущий>`; если доска есть, но её данных от агента нет (`board = null` или `board.fetched_at = null`) — +«Нужен агент v15 и обновление из Jira» с кнопкой «Обновить» блока состояния модуля; данные доски есть (`fetched_at` задан), а колонок нет (`columns = null`) — «Не удалось +прочитать настройки доски из 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» и кнопка «Обновить» -- 2.47.2