Архив OpenSpec: projects (#61)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-19 23:04:05 +04:00
parent 36266104a0
commit dc71d4e025
17 changed files with 387 additions and 62 deletions

View File

@ -11,9 +11,9 @@
или `bearer` с `token`), `verify_ssl`, `ca_bundle`, `timeout`, `poll_seconds`, необязательный `auto_update` (по
умолчанию `true`). `hub_url` вида `http://…` или `https://…` SHALL означать HTTP-транспорт, вида
`folder:<абсолютный путь>` — транспорт через общую папку. Всё остальное (id полей, типы, карта переходов, типы связей,
доска, темп) агент SHALL получать от хаба при старте и при изменении `settings_version`. Учётные данные Jira MUST NOT
отправляться в хаб и MUST NOT попадать в журнал, логи и общую папку. Команды `agent.py`: `run` (по умолчанию), `check`,
`selftest`, `prune`.
темп) агент SHALL получать от хаба при старте и при изменении `settings_version`; доски Jira — в параметрах команд
(требование «Доски проектов»). Учётные данные Jira MUST NOT отправляться в хаб и MUST NOT попадать в журнал, логи и общую
папку. Команды `agent.py`: `run` (по умолчанию), `check`, `selftest`, `prune`.
#### Scenario: Проверка без записи
- **WHEN** `python3 agent.py check`
@ -392,3 +392,31 @@ README агента SHALL описывать: первую установку (
#### Scenario: Старый агент
- **WHEN** агент v10 получил `ensure_attachment`
- **THEN** `failed` «неизвестный тип команды», агент работает дальше (поведение v10, без изменений)
### Requirement: Доски проектов
Агент (с v12) SHALL брать доски Jira из параметров команд, а не из настроек: `ensure_sprint` — из `board_id` команды,
`refresh` — из `boards` (`[{id, type}]`). В `refresh` для каждой доски с `type = null` агент SHALL запросить
`GET /rest/agile/1.0/board/{id}` и вернуть в результате команды `boards: {"<id>": "scrum" | "kanban" | null}` (`null` —
доска не найдена, нет доступа или тип не `scrum`/`kanban`; причина — в лог агента; `refresh` продолжается). Имена
спринтов задач агент SHALL собирать со всех досок типа `scrum` (известного или только что определённого), доски типа
`kanban` и `null` пропускать; задача в спринтах двух досок получает последнее найденное имя (как в v11). Если доска, которую хаб передал как `scrum`, не отдала спринты, агент SHALL перепроверить её тип: стала `kanban` — доска пропускается, в результате `boards: {"<id>": "kanban"}` (хаб записывает тип и при прежнем `scrum`); иначе `refresh` — `failed` с текстом «доска <id>: …» (агент v12, R-36). Команда без `board_id`/`boards`
(хаб до проектов) SHALL выполняться по `settings.board_id`, как в v11. `ensure_sprint` с `board_id` канбан-доски
агент не получает (хаб отказывает при постановке); если Jira всё же ответила, что у доски нет спринтов, — `failed` с
текстом Jira.
#### Scenario: Две доски, одна новая
- **WHEN** `refresh {jql: …, boards: [{id: 4216, type: "scrum"}, {id: 5120, type: null}]}`, доска 5120 в Jira — kanban
- **THEN** агент запрашивает `GET /board/5120` один раз, спринты читает только с доски 4216; результат содержит
`boards: {"5120": "kanban"}`
#### Scenario: Доска не найдена
- **WHEN** `refresh` с `boards: [{id: 9999, type: null}]`, Jira на `GET /board/9999` ответила 404
- **THEN** результат `refresh` — `done` с `boards: {"9999": null}`; в логе агента — доска 9999 и код ответа
#### Scenario: Старый хаб
- **WHEN** агент v12, хаб без проектов, `refresh {jql: …}` без `boards`, `settings.board_id = 4216`
- **THEN** спринты читаются с доски 4216, результат без `boards`
#### Scenario: Спринт по доске из команды
- **WHEN** `ensure_sprint {key: AURORA-1538, sprint: "Спринт 2", board_id: 4216}`
- **THEN** агент ищет «Спринт 2» среди активных и будущих спринтов доски 4216 и ставит задачу в него

View File

@ -17,9 +17,17 @@
| `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`) | `GET /issue/{key}` (поле спринта), список спринтов доски из `board_id` (активные и будущие) | `POST /rest/agile/1.0/sprint/{id}/issue` или `POST /rest/agile/1.0/backlog/issue` |
| `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_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` | `search` с полями зеркала, по 100 на страницу; плюс дочитывание соседей по `issuelinks` | ничего в Jira; состояние уходит в хаб |
| `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}` для досок, чей тип агент запрашивал |
Доску для `ensure_sprint` хаб SHALL определять при постановке: проект задачи — проекты, чьи метки есть у задачи в
зеркале (для временного ключа — в `labels` её `ensure_issue` в пакете, после дописывания метки проекта); `board_id` —
доска этого проекта. 422 при постановке: задача ни в одном проекте; в нескольких проектах с разными досками; у проекта
нет доски; тип доски `kanban` («у проекта P канбан-доска, спринтов нет»); тип доски ещё не известен («тип доски проекта P
ещё не известен — дождитесь обновления из Jira»). `boards` у `refresh` хаб SHALL дописывать при каждой выдаче (в базе
не хранится): все доски проектов без повторов. По результату `refresh` хаб SHALL записать присланные типы `scrum` и
`kanban` в проекты с этой доской; `null` тип не меняет.
Семантика `ensure_link`: `source` блокирует/ссылается на `target`; агент в `POST /issueLink` SHALL ставить `source` в
`inwardIssue`, `target` в `outwardIssue` (проверено на Jira банка 11.10.2026).
@ -67,6 +75,18 @@
- **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: Временные ключи и ссылки внутри пакета
Параметр-ключ любой команды MAY быть временным `new:<n>`. Хаб SHALL выдавать агенту такую команду только после `done`
команды `ensure_issue` с тем же `temp_key` в том же пакете, подставив настоящий ключ. Если `ensure_issue` завершилась
@ -198,3 +218,35 @@ SHALL перевести её в `confirmed_by_state`: `ensure_fields` — вс
- **WHEN** пакет `[ensure_issue {temp_key: new:1, issue_type: bug, …}, ensure_sprint {key: new:1, sprint: "Спринт 2"},
ensure_attachment {key: new:1, file: <sha>, filename: "ОСВ 60.xlsx"}]`, файл загружен
- **THEN** пакет создан; `ensure_attachment` выдаётся после завершения `ensure_issue` с подставленным ключом и `size` файла
### Requirement: Пакет проекта
Пакет, поставленный с `project = P` (см. `hub-api`), SHALL проходить при постановке, до записи, проверки проекта;
любая неудача — 422 с ключом задачи и причиной, пакет не создаётся:
- `P` нет среди проектов — 422;
- `ensure_issue`: если в `labels` нет P, хаб SHALL дописать P в конец `labels` (в базе параметры хранятся уже с ней;
без `labels` — `labels = [P]`); метка, совпадающая с P без учёта регистра, но не точно, SHALL заменяться на P; метка
другого проекта в `labels` — 422;
- `ensure_fields`: P в `labels_remove` или метка другого проекта в `labels_add` — 422;
- `ensure_fields`, `ensure_comment`, `ensure_status`, `ensure_sprint`, `ensure_attachment`: `key` SHALL быть временным
ключом `ensure_issue` этого пакета или задачей зеркала с меткой P, иначе 422 «<key> не в проекте P»;
- `ensure_link`: хотя бы один из `source`, `target` — временный ключ этого пакета или задача зеркала с меткой P;
- `refresh` — без проверок проекта.
«Метка другого проекта» и «P в `labels_remove`» SHALL сравниваться без учёта регистра; принадлежность задачи проекту
(метка P у задачи зеркала) — точным совпадением, как в разрезе проекта. Пакет без `project` эти проверки не проходит,
метки хаб в нём не дописывает.
#### Scenario: Метка проекта дописана
- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_issue {temp_key: new:1, summary: "Сверка остатков", labels: ["kind-dev"]}`
- **THEN** пакет создан; в параметрах команды `labels = ["kind-dev", "STOCKS_DRRK"]`
#### Scenario: Чужая метка проекта
- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_issue {…, labels: ["awpca"]}`, есть проект `AWPCA`
- **THEN** 422 «метка другого проекта: awpca», пакет не создан
#### Scenario: Задача другого проекта
- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_fields {key: AURORA-1538, duedate: …}`, у AURORA-1538 метка только `AWPCA`
- **THEN** 422 «AURORA-1538 не в проекте STOCKS_DRRK»
#### Scenario: Связь с соседом
- **WHEN** пакет с `project = STOCKS_DRRK`, `ensure_link {source: AURORA-1 (метка STOCKS_DRRK), target: AURORA-1538 (метка AWPCA), …}`
- **THEN** пакет создан

View File

@ -7,15 +7,24 @@
### Requirement: Схема базы хаба
База хаба SHALL быть одним файлом SQLite (WAL) с таблицами `issue`, `issue_link`, `issue_comment`, `issue_note`, `batch`,
`command`, `sync_run`, `agent_status`, `setting`. Схема SHALL создаваться и мигрироваться самим хабом при старте по номеру
версии в `setting.schema_version` (текущая — 3). Миграция существующей базы SHALL выполняться в одной транзакции с
блокировкой записи; в той же транзакции, после повторной проверки версии, хаб SHALL скопировать файл базы в
`command`, `sync_run`, `agent_status`, `setting`, `project`. Схема SHALL создаваться и мигрироваться самим хабом при
старте по номеру версии в `setting.schema_version` (текущая — 4). Миграция существующей базы SHALL выполняться в одной
транзакции с блокировкой записи; в той же транзакции, после повторной проверки версии, хаб SHALL скопировать файл базы в
`backups/<дата-время>-schema-v<старая>.sqlite` рядом с базой — одна копия на миграцию, сколько бы соединений ни
открывалось одновременно. Каталог данных хаба и `backups/` SHALL иметь права `0700` (в базе токены): хаб выставляет их
при создании и при каждом старте; если выставить не удалось — предупреждение в лог, работа продолжается. Денежных и
дробных полей нет; даты — ISO-8601 строки в UTC, кроме `duedate` и плановых дат (даты без времени) и времени
комментариев (строки Jira как есть).
Миграция 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` — проектов нет, предупреждение в лог с
прежним значением.
#### Scenario: Первый запуск
- **WHEN** хаб стартует с несуществующим файлом базы
- **THEN** файл создаётся, таблицы и индексы на месте, `setting.schema_version` = текущая; каталог данных с правами `0700`
@ -23,11 +32,23 @@
#### Scenario: Миграция базы v1
- **WHEN** хаб стартует с базой `schema_version = 1`, в которой есть задачи, заметки и команда в `claimed`
- **THEN** в `backups/` копия базы v1; добавлена `command.deliveries` (у `claimed` — 1, у остальных — 0); добавлена пустая
`issue_comment`; данные на месте; `schema_version = 3`
`issue_comment`; данные на месте; `schema_version = 4`
#### Scenario: Миграция базы v2
- **WHEN** хаб стартует с базой `schema_version = 2`, в которой есть задачи, связи и заметки
- **THEN** в `backups/` копия базы v2; добавлена пустая `issue_comment`; данные на месте; `schema_version = 3`
- **THEN** в `backups/` копия базы v2; добавлена пустая `issue_comment`; данные на месте; `schema_version = 4`
#### 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 = 4`
#### 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` одновременно открываются два соединения
@ -88,7 +109,8 @@ MUST NOT менять `issue_note`. При завершении `ensure_issue`
- **THEN** у `AURORA-1640` `work_kind = "Аналитика"` и `owner_note = "спросить ИБ"`; строки `new:5` нет
### Requirement: Пакеты и команды
Таблица `batch` SHALL хранить `id`, `title`, `author`, `created_at`, `cancelled_at`, `estimate_seconds`.
Таблица `batch` SHALL хранить `id`, `title`, `author`, `created_at`, `cancelled_at`, `estimate_seconds`, `project` (метка
проекта, если пакет поставлен в разрезе проекта, иначе `NULL`).
Таблица `command` SHALL хранить `id`, `batch_id`, `seq`, `type`, `params` (JSON), `status`
(`queued | claimed | done | failed | skipped | cancelled | confirmed_by_state`), `claimed_at` (время последней выдачи),
`deliveries` (сколько раз команда выдана агенту), `finished_at`, `result` (JSON), `error` (до 1500 символов), `jira_calls`,
@ -127,13 +149,16 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started
- **THEN** у его `sync_run` проставлен `finished_at`
### Requirement: Настройки
Таблица `setting` SHALL хранить парами ключ–JSON: `base_jql`, `field_ids`, `issue_types`, `priority_map`, `link_types`,
`transitions_map`, `pacing`, `board_id`, `tokens` (агентский и клиентский), `settings_version` (увеличивается при любой
Таблица `setting` SHALL хранить парами ключ–JSON: `project_key`, `field_ids`, `issue_types`, `priority_map`, `link_types`,
`transitions_map`, `pacing`, `tokens` (агентский и клиентский), `settings_version` (увеличивается при любой
правке), `backup_keep`, `auto_refresh_minutes`, `relay_dir` (абсолютный путь папки реле или `null`, по умолчанию `null`;
относительный путь и не-строка при записи — 422; агенту не передаётся), `closed_statuses` (список имён статусов Jira, считающихся закрытыми, по умолчанию
`["Готово", "Снято"]`; не-список при записи — 422; агенту не передаётся; на существующей базе появляется со значением
по умолчанию при открытии хаба). Спринты агент читает с доски `board_id`,
отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json`
по умолчанию при открытии хаба). Базовая JQL вычисляется из проектов, доска — свойство проекта (требование
«Проекты»); на новой базе ключей `base_jql` и `board_id` нет. Оставшиеся от v3 ключи хаб SHALL NOT использовать, кроме
передачи `board_id` агенту как запасной доски для команд без доски в параметрах (поставленных до миграции);
`GET /settings` SHALL отдавать вычисленную `base_jql` только для чтения, а запись `base_jql` или `board_id` через
`PUT /settings` — 422 с пояснением. Спринты агент читает с досок проектов, отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json`
командой `jira-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать. `issue_types` — объект
`{task, epic, bug}`, у каждого `{name, id}` (`id` может быть `null`); `init` берёт их из ключей конфига
`issue_type_<тип>` и `issue_type_<тип>_id`; `bug` попадает в `issue_types`, только если в конфиге есть хотя бы
@ -154,3 +179,52 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started
#### Scenario: Закрытые статусы по умолчанию
- **WHEN** хаб открыл базу, в которой нет ключа `closed_statuses`
- **THEN** `GET /settings` отдаёт `closed_statuses = ["Готово", "Снято"]`; `claim` агенту этого ключа не передаёт
#### Scenario: Запись базовой JQL
- **WHEN** `PUT /settings {base_jql: "project = AURORA"}`
- **THEN** 422 «базовая JQL собирается из проектов»; `settings_version` не изменился
#### Scenario: Создание проекта не трогает версию настроек
- **WHEN** создан проект `STOCKS_DRRK`
- **THEN** `settings_version` прежний; `GET /settings` отдаёт `base_jql` с обеими метками
### Requirement: Проекты
Таблица `project` SHALL хранить проекты моста — подмножества задач Jira-проекта `project_key`, выделенные меткой:
`label` (PK, метка Jira), `name`, `board_id` (номер доски Jira или `NULL`), `board_type` (`scrum` | `kanban` | `NULL` —
тип ещё не известен), `created_at`. Метка SHALL быть непустой строкой до 100 символов без пробельных и управляющих символов (категория
Unicode `Cc`), запятых, кавычек, скобок и обратной косой черты; две метки, совпадающие без учёта регистра, MUST NOT существовать одновременно. Название SHALL быть
непустым после обрезки пробелов, до 100 символов. `board_id` — целое больше нуля или `NULL`. `board_type` SHALL
записываться только по результату `refresh` от агента (см. `agent`). Метку, название и доску проекта после создания
хаб не меняет (переименования и удаления нет). Базовая JQL SHALL вычисляться из проектов:
`project = <project_key> AND labels in ("<метка>", …)` — каждая метка в двойных кавычках, метки по created_at, затем
label, через `, `; без проектов базовой JQL
нет (`null`).
Для проекта P задачи зеркала делятся на *свои* — с меткой P в `labels` (точное совпадение) — и *соседей* — задачи без
метки P, связанные со своей связью `issue_link` в любую сторону, и эпики своих (`epic_key` своей задачи), если они есть в
зеркале. Признак `neighbor` в выдаче в разрезе проекта SHALL считаться относительно P: `false` у своих, `true` у соседей.
#### Scenario: Базовая JQL из двух проектов
- **WHEN** заведены проекты `AWPCA` (раньше) и `STOCKS_DRRK`, `project_key = AURORA`
- **THEN** базовая JQL — `project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")`
#### Scenario: Свои и соседи
- **WHEN** у AURORA-1 метка `STOCKS_DRRK` и эпик AURORA-9 (метка `AWPCA`), связь AURORA-1 → AURORA-2 (метка `AWPCA`),
у AURORA-3 метка `AWPCA` без связей с проектом `STOCKS_DRRK`
- **THEN** в разрезе `STOCKS_DRRK` свои — AURORA-1, соседи — AURORA-2 и AURORA-9; AURORA-3 в разрез не входит
#### Scenario: Метка с пробелом
- **WHEN** создаётся проект с меткой `STOCKS DRRK`
- **THEN** отказ с указанием поля `label`, проекта нет
#### Scenario: Метка с обратной косой чертой или управляющим символом
- **WHEN** создаётся проект с меткой `A\B` или `A<перевод строки>B`
- **THEN** отказ с указанием поля `label`, проекта нет
#### Scenario: Метка — зарезервированное слово JQL
- **WHEN** заведён проект с меткой `EMPTY`
- **THEN** базовая JQL — `project = AURORA AND labels in ("EMPTY")`, метка не расширяет выборку
#### Scenario: Та же метка другим регистром
- **WHEN** есть проект `AWPCA`, создаётся проект `awpca`
- **THEN** отказ «проект с такой меткой уже есть», проект один

View File

@ -99,29 +99,36 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
- **THEN** 422 `bad_state` с ключом Y и полем `body`; ни X, ни Y в зеркале не изменились
### Requirement: Маршруты интерфейсов
Хаб SHALL предоставлять интерфейсам (MCP, позже просмотрщик) следующие маршруты под клиентским токеном:
- `GET /issues` с фильтрами `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`,
`q` (подстрока темы), `hide_closed` (`1` / `true` — без задач, чей `status` входит в настройку `closed_statuses`;
задача без статуса не закрыта), `limit`/`offset` — строки зеркала (все поля `issue`) с `fact_status` и `work_kind` из заметок;
сокращение до карточки — дело интерфейса.
Хаб SHALL предоставлять интерфейсам (MCP, веб-приложение) следующие маршруты под клиентским токеном:
- `GET /issues` с фильтрами `project` (разрез по проекту, см. `data-model`, «Проекты»: свои и соседи, `neighbor`
относительно проекта; неизвестный проект — 404), `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`,
`epic`, `neighbor`, `q` (подстрока темы), `hide_closed` (`1` / `true` — без задач, чей `status` входит в настройку
`closed_statuses`; задача без статуса не закрыта), `limit`/`offset` — строки зеркала (все поля `issue`) с `fact_status`
и `work_kind` из заметок; сокращение до карточки — дело интерфейса. Без `project` — всё зеркало, `neighbor` — признак
зеркала (вне базовой JQL).
- `GET /issues/{key}` — задача целиком: поля, `epic_summary` (тема эпика из зеркала или `null`), связи в обе стороны
(`links_out: [{target_key, link_type, summary, status}]`, `links_in: [{source_key, link_type, summary, status}]`, где
`summary` и `status` — связанной задачи из зеркала или `null`, если её там нет), заметки, последние 10 команд по ней,
`comments: [{comment_id, author, created, updated, body}]` — все комментарии, новые сверху (по `created`, затем
`comment_id`, по убыванию).
- `PUT /issues/{key}/note` — заметки (частичное обновление, `updated_by` из тела).
- `POST /batches` `{title, author, commands: [...]}` → `{id, estimate_seconds}`; 422 при неизвестном типе, неверных
параметрах (в том числе неверного типа значения), временном ключе без `ensure_issue`, двух и более `ensure_issue` с
одинаковой `summary` (сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся темы), а также
по проверкам постановки из `commands` (тип `bug` без настройки, файл вложения не загружен).
- `POST /batches` `{title, author, project?, commands: [...]}` → `{id, estimate_seconds}`; 422 при неизвестном типе,
неверных параметрах (в том числе неверного типа значения), временном ключе без `ensure_issue`, двух и более
`ensure_issue` с одинаковой `summary` (сравнение после обрезки пробелов по краям; в ответе перечислены повторяющиеся
темы), по проверкам постановки из `commands` (тип `bug` без настройки, файл вложения не загружен, доска для
`ensure_sprint`), а также по проверкам пакета проекта (см. `commands`), если передан `project`.
- `POST /files` `{content}` (`content` — base64 содержимого файла) → `{file, size}`, где `file` — sha256 содержимого
(64 hex-символа, нижний регистр); см. «Хранилище файлов вложений».
- `GET /batches?limit=`, `GET /batches/{id}` (с командами), `POST /batches/{id}/cancel` — отменяет `queued` команды
пакета; ответ — вид пакета с полем `still_claimed`: список `{id, type}` команд пакета в `claimed` (они будут выполнены
агентом; остановить — отмена команды с `force`).
- `POST /commands/{id}/retry` — новая команда с теми же параметрами в новом пакете «Повтор команды N».
- `GET /batches?limit=&project=` (с `project` — только пакеты этого проекта), `GET /batches/{id}` (с командами и
`project`), `POST /batches/{id}/cancel` — отменяет `queued` команды пакета; ответ — вид пакета с полем `still_claimed`:
список `{id, type}` команд пакета в `claimed` (они будут выполнены агентом; остановить — отмена команды с `force`).
- `POST /commands/{id}/retry` `{author?, project?}` — новая команда с теми же параметрами в новом пакете «Повтор
команды N»; хаб снимает с параметров то, что дописывает сам (`board_id` у `ensure_sprint`), и проверяет команду
заново. `project` передан и у исходного пакета другой проект (или его нет) — 422 «команда #N не из проекта P»; не
передан — новый пакет получает проект исходного и проходит его проверки.
- `POST /commands/{id}/cancel` — только для `queued`; для `claimed` — 409, если не передан `force: true`.
- `POST /refresh` `{keys | jql | full: true}` → пакет из одного `refresh`.
- `POST /refresh` `{keys | jql | full: true}` → пакет из одного `refresh`; `full` — по базовой JQL из проектов, без
проектов — 422 «нет проектов».
- `GET /agent`, `GET /sync-runs?limit=`, `GET /settings`, `PUT /settings` (частично; поднимает `settings_version`;
ответ — все настройки после записи, как у `GET /settings`, включая `settings_version`).
@ -129,6 +136,30 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
- **WHEN** `GET /issues?sprint=Спринт 2`
- **THEN** только задачи с таким `sprint`, отсортированы по сроку, затем по ключу
#### Scenario: Список проекта
- **WHEN** свои задачи `STOCKS_DRRK` — AURORA-1 и AURORA-4, сосед — AURORA-2; `GET /issues?project=STOCKS_DRRK`
- **THEN** три задачи, у AURORA-2 `neighbor = true`; с `neighbor=1` — только AURORA-2; задач только с меткой `AWPCA` нет
#### Scenario: Неизвестный проект
- **WHEN** `GET /issues?project=NOPE`
- **THEN** 404
#### Scenario: Пакеты проекта
- **WHEN** пакет 10 поставлен с `project = STOCKS_DRRK`, пакет 11 — без проекта; `GET /batches?project=STOCKS_DRRK`
- **THEN** в ответе только пакет 10
#### Scenario: Повтор чужой команды
- **WHEN** команда 40 из пакета проекта `AWPCA`; `POST /commands/40/retry {project: "STOCKS_DRRK"}`
- **THEN** 422 «команда #40 не из проекта STOCKS_DRRK», пакет не создан
#### Scenario: Повтор ensure_sprint
- **WHEN** `ensure_sprint` 41 failed, в её `params` `board_id = 4216`; `POST /commands/41/retry`
- **THEN** новый пакет создан, `board_id` в новой команде вычислен хабом заново
#### Scenario: Полный прогон без проектов
- **WHEN** проектов нет, `POST /refresh {full: true}`
- **THEN** 422 «нет проектов», пакет не поставлен
#### Scenario: Отмена взятой команды
- **WHEN** `POST /commands/15/cancel` без `force`, команда `claimed`
- **THEN** 409 с `claimed_at`; с `force: true` — `cancelled`, следующий отчёт агента по ней принимается, статус не меняется
@ -175,9 +206,9 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
- **THEN** самые старые удаляются до `backup_keep`
### Requirement: Автообновление зеркала
Если `auto_refresh_minutes > 0` и агент на связи, хаб SHALL поставить пакет «Автообновление» из `refresh {jql: base_jql}`,
когда в очереди нет команд в `queued` и `claimed` и последний `sync_run` начат раньше, чем `auto_refresh_minutes` назад
(или `sync_run` ещё нет).
Если `auto_refresh_minutes > 0`, агент на связи и есть хотя бы один проект, хаб SHALL поставить пакет «Автообновление»
из `refresh {jql: <базовая JQL из проектов>}`, когда в очереди нет команд в `queued` и `claimed` и последний `sync_run`
начат раньше, чем `auto_refresh_minutes` назад (или `sync_run` ещё нет).
#### Scenario: Очередь занята
- **WHEN** интервал прошёл, но в очереди есть `queued` команды
@ -191,6 +222,10 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
- **WHEN** очередь пуста, `auto_refresh_minutes = 60`, последний `sync_run` начат 20 минут назад
- **THEN** автообновление не ставится
#### Scenario: Два проекта
- **WHEN** интервал прошёл, очередь пуста, проекты `AWPCA` и `STOCKS_DRRK`
- **THEN** поставлен один `refresh` с JQL `project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")`
### Requirement: Реле через общую папку
`jira-hub serve` SHALL запускать поток реле вместе с HTTP-сервером. Пока настройка `relay_dir` пуста (`null`), реле
бездействует; настройку оно SHALL перечитывать не реже раза в 5 с, включение и выключение не требуют перезапуска. Если
@ -247,18 +282,21 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
- **THEN** реле создаёт `resp/` заново; файл в `work/` не удалён, в логе нет «удалён незавершённый запрос»
### Requirement: Снимок доски
Хаб SHALL предоставлять `GET /board` под клиентским токеном — согласованный снимок зеркала для доски связей, прочитанный
в одной транзакции:
Хаб SHALL предоставлять `GET /board?project=` под клиентским токеном — согласованный снимок зеркала для доски связей,
прочитанный в одной транзакции:
- `generated_at` — момент снимка (UTC);
- `last_sync` — последний завершённый полный прогон (`refresh` по базовой JQL): `{started_at, finished_at, issues_count}`
или `null`, если такого не было;
- `issues` — все задачи зеркала, включая эпики и соседей: `key`, `summary`, `issue_type`, `status`, `priority`,
`assignee`, `sprint`, `duedate`, `planned_start`, `planned_end`, `epic_key`, `labels` (список), `neighbor` (bool),
`fact_status`, `work_kind` (из заметок, `null` если заметки нет); порядок — по ключу;
- `links` — все связи зеркала: `{source, target, link_type}`, `link_type` — имя типа в Jira, направление как в
`issue_link` (`source` блокирует/ссылается на `target`).
Перевод в модель доски (вид работы по меткам, имена эпиков, логические типы связей) — дело интерфейса; хаб отдаёт данные
зеркала без преобразований.
- `last_sync` — последний завершённый полный прогон (`refresh` по текущей базовой JQL из проектов):
`{started_at, finished_at, issues_count}` или `null`, если такого не было; общий для всех проектов;
- `issues` — с `project` — свои задачи проекта и его соседи (см. `data-model`, «Проекты»), `neighbor` относительно
проекта; без `project` — все задачи зеркала, включая эпики и соседей, `neighbor` — признак зеркала. Поля: `key`,
`summary`, `issue_type`, `status`, `priority`, `assignee`, `sprint`, `duedate`, `planned_start`, `planned_end`,
`epic_key`, `labels` (список), `neighbor` (bool), `fact_status`, `work_kind` (из заметок, `null` если заметки нет);
порядок — по ключу;
- `links` — с `project` — связи, у которых хотя бы один конец — своя задача проекта; без `project` — все связи
зеркала: `{source, target, link_type}`, `link_type` — имя типа в Jira, направление как в `issue_link` (`source`
блокирует/ссылается на `target`).
Неизвестный проект — 404. Перевод в модель доски (вид работы по меткам, имена эпиков, логические типы связей) — дело
интерфейса; хаб отдаёт данные зеркала без преобразований.
#### Scenario: Снимок с задачами и связью
- **WHEN** в зеркале эпик AURORA-1351, задачи AURORA-1620 (метка `kind-dev`, эпик AURORA-1351) и AURORA-1621, связь
@ -266,6 +304,11 @@ HTTP API хаба `jira-hub serve`: маршруты агента (выдача
- **THEN** `GET /board` отдаёт три задачи по ключу, у AURORA-1620 `labels = ["kind-dev"]`, `epic_key = "AURORA-1351"`,
`work_kind = "разработка"`; в `links` — `{source: "AURORA-1620", target: "AURORA-1621", link_type: "ASUPR_Блокер_Начала"}`
#### Scenario: Снимок проекта
- **WHEN** AURORA-1 (метка `STOCKS_DRRK`) связана с AURORA-2 (метка `AWPCA`), AURORA-2 связана с AURORA-3 (метка `AWPCA`);
`GET /board?project=STOCKS_DRRK`
- **THEN** `issues` — AURORA-1 (`neighbor = false`) и AURORA-2 (`neighbor = true`); `links` — только AURORA-1 ↔ AURORA-2
#### Scenario: Последний полный прогон
- **WHEN** в журнале синхронизаций полный прогон, завершённый в 10:00, и после него обновление по ключам в 11:00
- **THEN** `last_sync.finished_at` — 10:00
@ -391,3 +434,29 @@ sha256 (без расширения и имени вложения: имя за
- **WHEN** в `files/` файл A старше 30 дней без ссылок, файл B старше 30 дней, на него ссылается `ensure_attachment` в
`queued`, файл C загружен вчера
- **THEN** после уборки удалён только A
### Requirement: Маршруты проектов
Хаб SHALL предоставлять под клиентским токеном:
- `GET /projects` → список `{label, name, board_id, board_type, created_at, issues_count}` по `created_at`, затем `label`;
`issues_count` — задачи зеркала с меткой проекта.
- `POST /projects` `{label, name, board_id?, author?}` → 201 `{project, batch_id}`: в одной транзакции проект
записывается и ставится пакет «Новый проект <метка>» из одного `refresh` по базовой JQL, уже включающей новую метку;
автор пакета — `author` (по умолчанию `api`). Неверные поля (см. `data-model`, «Проекты») — 422 с указанием поля;
метка, совпадающая с существующей без учёта регистра, — 409. При ошибке ни проекта, ни пакета нет.
#### Scenario: Новый проект
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120, author: "viewer"}`, есть проект `AWPCA`
- **THEN** 201; `GET /projects` отдаёт два проекта, у `STOCKS_DRRK` `board_type = null`; в очереди пакет автора `viewer`
с `refresh {jql: 'project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")'}`
#### Scenario: Проект без доски
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК"}`
- **THEN** 201, `board_id = null`
#### Scenario: Повтор метки
- **WHEN** `POST /projects {label: "AWPCA", name: "…"}`, проект `AWPCA` есть
- **THEN** 409, пакет не поставлен
#### Scenario: Без названия
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: " "}`
- **THEN** 422 с указанием поля `name`

View File

@ -6,9 +6,11 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
## Requirements
### Requirement: Сервер и регистрация
`jira-hub mcp` SHALL запускать MCP-сервер по stdio (пакет `mcp`), ходящий в HTTP API хаба клиентским токеном из
настроек. Регистрация — в `~/.claude.json` для проекта `awp-ca` как `jira-hub` (`uv run --project <путь> jira-hub mcp`).
Если хаб недоступен, каждый инструмент SHALL вернуть текст «хаб не запущен: jira-hub serve», а не исключение.
`jira-hub mcp [--project <метка>]` SHALL запускать MCP-сервер по stdio (пакет `mcp`), ходящий в HTTP API хаба клиентским
токеном из настроек. Общая регистрация — в user scope Claude Code как `jira-hub` (`uv run --project <путь> jira-hub mcp`),
без проекта. Репозиторий проекта SHALL регистрировать `jira-hub` в своём `.mcp.json` (project scope) с
`--project <метка>`; эта запись перекрывает общую. Если хаб недоступен, каждый инструмент SHALL вернуть текст «хаб не
запущен: jira-hub serve», а не исключение.
#### Scenario: Хаб выключен
- **WHEN** вызван `issues` при остановленном хабе
@ -17,7 +19,9 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
### Requirement: Инструменты
| Инструмент | Параметры | Результат |
|---|---|---|
| `issues` | фильтры как у `GET /issues`, `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту |
| `projects` | — | таблица: метка, название, доска, тип доски («неизвестен», пока агент не определил), число своих задач; в привязанной сессии текущий проект отмечен |
| `project_create` | `label`, `name`, `board_id` (необязательно) | `POST /projects` с автором `claude`: проект, id пакета обновления и фрагмент `.mcp.json` для репозитория проекта (см. ниже); 409/422 — текст ошибки |
| `issues` | фильтры как у `GET /issues` (кроме `project` — его задаёт привязка), `limit` (по умолчанию 50) | таблица: ключ, тема (до 80 символов), статус, срок, исполнитель, спринт, статус по факту |
| `issue` | `key` | поля, связи (обе стороны, с типом и направлением), заметки, последние команды, комментарии: «комментарии (N):» и 5 последних (автор, время, текст до 300 символов с «…»), «ещё M», если их больше 5; без комментариев — «комментариев нет» |
| `notes_set` | `key`, подмножество полей заметок | обновлённые заметки |
| `batch_create` | `title`, `commands` (список объектов по контракту `commands`; у `ensure_attachment` вместо `file` можно `path`) | `id`, `estimate_seconds`, число команд; 422 — текст ошибки валидации |
@ -31,6 +35,8 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
| `sync_log` | `limit` | последние `sync_run` |
| `settings_get`, `settings_set` | ключ, значение | текущее значение / новая `settings_version` |
Разрез по проекту и поведение без привязки — требование «Привязка сессии к проекту».
Описание инструмента `batch_create` SHALL перечислять параметры каждого типа команды (обязательные, необязательные в
`[ ]`, формат значения) и собираться из описания параметров в коде хаба (см. `commands`), а не вручную.
@ -39,8 +45,14 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
`file`; `filename` по умолчанию — имя файла из `path`. Нет файла, нет прав, не файл, больше 10 МБ — ответ инструмента
с причиной и путём, пакет не ставится. Передан и `path`, и `file` — ответ «у ensure_attachment либо path, либо file».
Описание инструмента `batch_create` SHALL предупреждать, что `ensure_issue` должна нести `labels` с базовой меткой
проекта (`AWPCA`): хаб и агент метки сами не добавляют, и задача без неё выпадает из базовой JQL и из зеркала.
Описание инструмента `batch_create` SHALL сообщать, что метку проекта сессии хаб дописывает в `ensure_issue` сам, метки
других проектов запрещены, а команды над задачами вне проекта отклоняются; `board_id` для `ensure_sprint` выставляет хаб.
Ответ `project_create` SHALL содержать фрагмент `.mcp.json`
`{"mcpServers": {"jira-hub": {"command": "uv", "args": ["run", "--project", "<корень хаба>", "jira-hub", "mcp",
"--project", "<метка>"]}}}`, где корень хаба — корень репозитория, из которого загружен пакет текущего процесса MCP
(вычисляется от расположения пакета, без зашитого пути), и указание: положить файл в корень репозитория проекта (слить
с имеющимся `.mcp.json`) и переподключить MCP (`/mcp` → reconnect, одобрить сервер проекта).
Автор команд из MCP SHALL быть `claude`; из API без автора — `api`.
@ -48,6 +60,10 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
- **WHEN** `batch_create {title: "1538 в спринт 2", commands: [ensure_fields{…}, ensure_link{…}, ensure_sprint{…}]}`
- **THEN** пакет создан с четырьмя командами (три + `refresh`), ответ содержит оценку в минутах
#### Scenario: Проект из чата
- **WHEN** сессия без привязки, `project_create {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120}`
- **THEN** проект создан, ответ содержит id пакета обновления и фрагмент `.mcp.json` с `"--project", "STOCKS_DRRK"`
#### Scenario: Ошибка с файлом из чата
- **WHEN** `batch_create {title: "Ошибка ОСВ 60", commands: [ensure_issue{temp_key: new:1, issue_type: bug, …},
ensure_attachment{key: new:1, path: "/Users/…/ОСВ сч. 60 за 2 кв. 2026.xlsx"}]}`
@ -83,3 +99,39 @@ MCP-сервер хаба (`jira-hub mcp`, stdio) для Claude: инструм
#### Scenario: Задача с семью комментариями
- **WHEN** `issue {key: "AURORA-9001"}`, у задачи 7 комментариев, у самого нового текст 500 символов
- **THEN** ответ содержит «комментарии (7):», 5 последних комментариев (самый новый — первым, обрезан до 300 символов с «…») и «ещё 2»
### Requirement: Привязка сессии к проекту
`jira-hub mcp --project <метка>` SHALL привязывать сервер MCP к проекту. Привязка SHALL проверяться при каждом вызове
инструмента по `GET /projects`: метки нет — ответ «проект <метка> не заведён в хабе: project_create или исправьте
--project в .mcp.json» (кроме `projects`, `project_create`).
В привязанной к проекту P сессии:
- `issues` — `GET /issues?project=P` с остальными фильтрами;
- `issue` — любая задача по ключу;
- `notes_set` — только своя задача P (метка P; соседи — нет) или временный ключ `new:<n>` (его задача создаётся пакетом
проекта), иначе ответ «<key> не в проекте P»;
- `batch_create`, `command_retry` — с `project = P` (проверки пакета проекта, см. `commands`; повтор команды другого
проекта или пакета без проекта — отказ хаба);
- `batches` — `GET /batches?project=P`; `batch_status`, `batch_wait`, `batch_cancel` — по id, без ограничения;
- `refresh` — `full` — общий полный прогон всех проектов, `keys` — как раньше.
Без привязки инструменты `issues`, `issue`, `notes_set`, `batch_create`, `command_retry`, `refresh` SHALL отвечать
«сессия не привязана к проекту: запустите MCP с --project <метка> (список — projects)» и в хаб не ходить; `projects`,
`project_create`, `agent_status`, `sync_log`, `settings_get`, `settings_set`, `batches`, `batch_status`, `batch_wait`,
`batch_cancel` работают.
#### Scenario: Сессия без проекта
- **WHEN** MCP запущен без `--project`, вызван `issues`
- **THEN** ответ «сессия не привязана к проекту: …», запросов к `GET /issues` нет
#### Scenario: Задачи своего проекта
- **WHEN** MCP запущен с `--project STOCKS_DRRK`, вызван `issues`
- **THEN** в таблице только свои задачи `STOCKS_DRRK` и их соседи
#### Scenario: Создание задачи без метки
- **WHEN** сессия `STOCKS_DRRK`, `batch_create {commands: [ensure_issue {temp_key: new:1, summary: "…"}]}` без `labels`
- **THEN** пакет поставлен с `project = STOCKS_DRRK`, у `ensure_issue` `labels = ["STOCKS_DRRK"]`
#### Scenario: Опечатка в проекте
- **WHEN** MCP запущен с `--project STOKS`, такого проекта нет; вызван `issues`
- **THEN** ответ «проект STOKS не заведён в хабе: …»

View File

@ -8,13 +8,17 @@
### Requirement: Инициализация настроек
`jira-hub init --config <admin/jira-sync/config.example.json>` SHALL перенести в `setting`: `project_key`, типы задач,
`epic_name_field_id`, `fields`, `epic_link`, `priority_map`, `labels`, `status_labels`, `kind_labels`, `comment_marker`,
`pacing` (в новую форму по типам команд), `transitions.map`, `link_types`, `sprints.board_id`; сгенерировать два токена;
установить `base_jql = "project = AURORA AND labels = AWPCA"`. Поля `auth`, `base_url`, `verify_ssl`, `ca_bundle`
MUST NOT переноситься (они у агента).
`pacing` (в новую форму по типам команд), `transitions.map`, `link_types`; сгенерировать два токена; завести проект
`AWPCA` «АРМ КА» с `board_id` из `sprints.board_id` конфига (если проекта `AWPCA` ещё нет). Поля `auth`, `base_url`,
`verify_ssl`, `ca_bundle` MUST NOT переноситься (они у агента).
#### Scenario: Повторный init
- **WHEN** `init` вызван на уже инициализированной базе
- **THEN** настройки не перезаписываются без `--force`, токены сохраняются
- **THEN** настройки не перезаписываются без `--force`, токены сохраняются; проект `AWPCA` не дублируется
#### Scenario: Первый init
- **WHEN** `init --config` на пустой базе, в конфиге `sprints.board_id = 4216`
- **THEN** есть проект `AWPCA` «АРМ КА» с `board_id = 4216`; в `setting` нет `base_jql` и `board_id`
### Requirement: Импорт CSV-пакета
`jira-hub import <папка admin/jira-sync>` SHALL прочитать `tasks.csv`, `new_tasks.csv`, `epics.csv` и заполнить

View File

@ -38,13 +38,14 @@ SHALL очищать токен и показывать экран входа.
### Requirement: Оболочка и адреса
Приложение SHALL открываться под префиксом `/app/` и иметь страницы `/app/board` (доска связей), `/app/issues` (список
задач) и `/app/issues/{key}` (карточка задачи — окно поверх страницы, требование «Карточка задачи»); `/app` и `/app/` SHALL вести на `/app/board`, неизвестный путь —
на страницу «не найдено» со ссылками на разделы. Верхняя панель SHALL содержать переходы «Доска», «Задачи» и действие
«Сменить токен». Неожиданная ошибка отрисовки страницы SHALL показываться сообщением с кнопкой «Перезагрузить», не
пустым экраном.
на страницу «не найдено» со ссылками на разделы. Верхняя панель SHALL содержать выпадающий список проектов рядом с
названием («<метка> — <название>», текущий отмечен, в конце «+ Новый проект…»; требование «Проект в приложении»),
переходы «Доска», «Задачи» и действие «Сменить токен». Неожиданная ошибка отрисовки страницы SHALL показываться
сообщением с кнопкой «Перезагрузить», не пустым экраном.
#### Scenario: Переход между разделами
- **WHEN** на доске выбран раздел «Задачи»
- **THEN** адрес — `/app/issues`, показан список задач; «назад» возвращает на доску
- **THEN** адрес — `/app/issues` с текущим проектом, показан список задач; «назад» возвращает на доску
### Requirement: Ошибки хаба
Приложение SHALL различать ошибки запросов к хабу: 401 — экран входа; хаб не отвечает (сеть, 5xx) — сообщение
@ -160,11 +161,11 @@ SHALL проверять агента (`GET /agent`; офлайн — преду
- **THEN** доска его не опрашивает, кнопка «Обновить из Jira» доступна
### Requirement: Список задач
Страница `/app/issues` SHALL показывать задачи зеркала из `GET /issues` таблицей: ключ (открывает карточку задачи окном поверх списка), тема,
Страница `/app/issues` SHALL показывать задачи текущего проекта из `GET /issues?project=<текущий>` таблицей: ключ (открывает карточку задачи окном поверх списка), тема,
тип, статус Jira, исполнитель, спринт, срок (`planned_end`, иначе `duedate`), эпик (имя эпика, иначе ключ), вид работы,
признак «сосед»; статуса по факту в списке нет. Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид
признак «сосед» (относительно проекта); статуса по факту в списке нет. Фильтры SHALL соответствовать параметрам `GET /issues` (спринт, исполнитель, вид
работы, статус Jira, метка, эпик, только соседи, поиск по ключу или теме, скрыть закрытые), храниться в query-строке
адреса и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board`; значение фильтра из адреса, которого нет среди
адреса вместе с `project` и применяться сервером; варианты выпадающих списков SHALL браться из снимка `GET /board?project=<текущий>`; значение фильтра из адреса, которого нет среди
вариантов, SHALL показываться в списке с пометкой «(нет на доске)». Флажок «скрыть закрытые» SHALL
быть включён по умолчанию (в адресе ничего), снятый — `closed=1` в адресе; включённый — запрос с `hide_closed=1`,
кроме случая, когда в фильтре «Статус Jira» выбран статус из `closed_statuses`. Параметр `fact_status` в адресе
@ -173,7 +174,7 @@ SHALL проверять агента (`GET /agent`; офлайн — преду
#### Scenario: Фильтр в адресе
- **WHEN** выбран спринт «AWPCA_Спринт 2» и исполнитель
- **THEN** адрес содержит `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого
- **THEN** адрес содержит `project=`, `sprint=` и `assignee=`, таблица — результат `GET /issues` с этими параметрами; открытие этого
адреса в новой вкладке показывает ту же выборку
#### Scenario: Конец списка
@ -181,7 +182,7 @@ SHALL проверять агента (`GET /agent`; офлайн — преду
- **THEN** в таблице 67 задач, «Показать ещё» не показана
#### Scenario: Фильтр по закрытому спринту
- **WHEN** открыт адрес `/app/issues?sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет
- **WHEN** открыт адрес `/app/issues?project=AWPCA&sprint=AWPCA_Спринт 0`, а в снимке доски такого спринта нет
- **THEN** выпадающий список «Спринт» показывает «AWPCA_Спринт 0 (нет на доске)» выбранным, таблица — по этому фильтру
#### Scenario: Карточка поверх списка
@ -371,3 +372,48 @@ SHALL сообщать об этом с кнопкой «Закрыть»; ха
- **WHEN** описание — `Поле data_type = null, дата 2026-10-15, оборот 18 803 903,65 - 9 351 102,89, a*b*c, UTF-8,
**важно**`
- **THEN** текст показан как есть, без курсива, зачёркивания, жирного и ссылок
### Requirement: Проект в приложении
Доска и список задач SHALL показывать один проект — текущий. Текущий проект SHALL задаваться параметром `project` в
адресе `/app/board` и `/app/issues`; без параметра — последний выбранный в этом браузере (`localStorage`), если он есть
в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес без новой записи истории.
Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден» и выпадающий список
проектов вместо данных. Проектов нет — доска и список SHALL показывать приглашение создать проект («+ Новый проект…»).
Переходы «Доска» / «Задачи» SHALL сохранять текущий проект. Карточка задачи (`/app/issues/{key}`) от проекта не зависит.
Доска SHALL загружать `GET /board?project=<текущий>`; «Обновить из Jira» — как прежде, `POST /refresh {full: true}`,
обновляет все проекты. Раскладка, дорожки и фильтры доски в `localStorage` SHALL храниться отдельно для каждого проекта;
у проекта `AWPCA` — на прежних ключах (раскладка, сделанная до проектов, сохраняется). Фильтр спринтов SHALL
показываться, только если хотя бы у одной своей задачи снимка (`neighbor = false`) есть спринт; иначе — «колонки
«Спринт» нет — показаны все задачи».
Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка,
название, номер доски Jira (необязательно; подсказка «scrum или kanban определится сам»). «Создать» — `POST /projects`
с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с кнопкой, окно остаётся открытым; успех — окно
закрывается, текущим становится новый проект, доска подхватывает пакет обновления (`refresh` автора `viewer`) и
показывает его ход.
#### Scenario: Первый вход после выкатки
- **WHEN** открыт `/app/board` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK`
- **THEN** адрес — `/app/board?project=AWPCA`, доска — `GET /board?project=AWPCA` с прежней раскладкой карточек
#### Scenario: Переключение проекта
- **WHEN** на доске `AWPCA` в выпадающем списке выбран `STOCKS_DRRK`, затем открыт раздел «Задачи»
- **THEN** доска — `GET /board?project=STOCKS_DRRK` со своей раскладкой; адрес «Задач» — `/app/issues?project=STOCKS_DRRK`
#### Scenario: Неизвестный проект в адресе
- **WHEN** открыт `/app/issues?project=NOPE`
- **THEN** «проект NOPE не найден» и список проектов; запроса `GET /issues` нет
#### Scenario: Создание проекта из формы
- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», доска пусто; нажато «Создать»
- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", author: "viewer"}`; текущий проект — `STOCKS_DRRK`;
шапка доски показывает «пакет #N выполняется»
#### Scenario: Метка занята
- **WHEN** в форме введена метка `awpca`, хаб ответил 409
- **THEN** окно открыто, рядом с «Создать» — текст ошибки хаба
#### Scenario: Канбан-проект
- **WHEN** текущий проект — `STOCKS_DRRK`, ни у одной своей задачи снимка нет спринта (у соседа из `AWPCA` спринт есть)
- **THEN** вместо фильтра спринтов — «колонки «Спринт» нет — показаны все задачи»