Andrey Epifancev 54d87225bd OpenSpec: change attachment-fetch — вложения из Jira по запросу (#83)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:59:37 +04:00

142 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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