13 KiB
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 стоит
queuedrefresh {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