142 lines
13 KiB
Markdown
142 lines
13 KiB
Markdown
## MODIFIED Requirements
|
||
|
||
### 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` (текущая — 6). Миграция существующей базы 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.
|
||
|
||
#### 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 = 6`
|
||
|
||
#### Scenario: Миграция базы v2
|
||
- **WHEN** хаб стартует с базой `schema_version = 2`, в которой есть задачи, связи и заметки
|
||
- **THEN** в `backups/` копия базы v2; добавлена пустая `issue_comment`; данные на месте; `schema_version = 6`
|
||
|
||
#### 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 = 6`
|
||
|
||
#### 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 = 6`
|
||
|
||
#### Scenario: Миграция базы v5
|
||
- **WHEN** хаб стартует с базой `schema_version = 5`
|
||
- **THEN** в `backups/` копия базы v5; есть пустые `issue_attachment` и `attachment_file`; прочие таблицы не тронуты;
|
||
`schema_version = 6`
|
||
|
||
#### 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`, `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: Скачанные вложения
|
||
Таблица `attachment_file` SHALL хранить, какие вложения уже скачаны в хранилище файлов хаба: `attachment_id` (PK),
|
||
`file` (sha256 содержимого), `size`, `fetched_at` (UTC). Строку SHALL записывать (вставка или замена) только отчёт
|
||
`done` по `fetch_attachment`; загрузка состояния её MUST NOT трогать, поэтому скачанный файл переживает обновление
|
||
списка вложений. Строка без файла в хранилище (файл убран уборкой) SHALL считаться «не скачано».
|
||
|
||
Состояние вложения для интерфейсов (`state`) SHALL вычисляться при чтении:
|
||
- `ready` — есть строка `attachment_file` и файл `file` есть в хранилище;
|
||
- `fetching` — нет `ready`, и есть `fetch_attachment` с этим `attachment_id` в `queued` или `claimed`;
|
||
- `failed` — нет ни того, ни другого, а последняя (по `id`) `fetch_attachment` с этим `attachment_id` — `failed` или
|
||
`cancelled`; её `error` отдаётся вместе с состоянием;
|
||
- `none` — вложение ни разу не запрашивали (или запрос был до уборки файла).
|
||
|
||
#### Scenario: Файл убран уборкой
|
||
- **WHEN** у вложения #31337 строка `attachment_file`, файла `file` в `files/` нет; команд по нему в очереди нет, последняя
|
||
`fetch_attachment` по нему — `done`
|
||
- **THEN** `state = none`; новый запрос на скачивание ставит новую `fetch_attachment`
|
||
|
||
#### Scenario: Список вложений обновился
|
||
- **WHEN** вложение #31337 скачано (`state = ready`), затем пришло состояние задачи с тем же вложением
|
||
- **THEN** `state` по-прежнему `ready`
|