12 KiB
data-model Specification
Purpose
Модель данных хаба: один файл SQLite с зеркалом задач Jira, служебными заметками, пакетами и командами, журналом синхронизаций, статусом агента и настройками. Здесь таблицы, их смысл, правила записи (зеркало пишет только загрузка состояния от агента) и миграции схемы.
Requirements
Requirement: Схема базы хаба
База хаба SHALL быть одним файлом SQLite (WAL) с таблицами issue, issue_link, issue_note, batch, command,
sync_run, agent_status, setting. Схема SHALL создаваться и мигрироваться самим хабом при старте по номеру версии
в setting.schema_version (текущая — 2). Миграция существующей базы SHALL выполняться в одной транзакции с блокировкой
записи; в той же транзакции, после повторной проверки версии, хаб SHALL скопировать файл базы в
backups/<дата-время>-schema-v<старая>.sqlite рядом с базой — одна копия на миграцию, сколько бы соединений ни
открывалось одновременно. Каталог данных хаба и backups/ SHALL иметь права 0700 (в базе токены): хаб выставляет их
при создании и при каждом старте; если выставить не удалось — предупреждение в лог, работа продолжается. Денежных и
дробных полей нет; даты — ISO-8601 строки в UTC, кроме duedate и плановых дат (даты без времени).
Scenario: Первый запуск
- WHEN хаб стартует с несуществующим файлом базы
- THEN файл создаётся, таблицы и индексы на месте,
setting.schema_version= текущая; каталог данных с правами0700
Scenario: Миграция базы v1
- WHEN хаб стартует с базой
schema_version = 1, в которой есть задачи, заметки и команда вclaimed - THEN в
backups/копия базы v1; добавленаcommand.deliveries(уclaimed— 1, у остальных — 0); данные на месте;schema_version = 2
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 и issue_link SHALL только загрузка состояния от
агента; HTTP API интерфейсов и MCP MUST NOT писать в них. Полный прогон (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задач зеркала не меняется
Requirement: Служебные заметки
Таблица issue_note SHALL хранить поля, которых нет в Jira: key (PK; ключ Jira или временный new:<n>),
fact_status, work_kind, done_text, rest_text, owner_note, updated_at, updated_by. Обновление зеркала
MUST NOT менять issue_note. При завершении ensure_issue с временным ключом хаб SHALL перенести заметку на настоящий
ключ — в том числе по позднему отчёту done с ключом о команде, отменённой или переведённой в failed до отчёта. Если
под настоящим ключом заметка уже есть, хаб SHALL слить их: каждое поле берётся из временной заметки, если там оно
непустое, иначе остаётся под настоящим ключом; updated_at/updated_by — от более поздней; временная строка удаляется.
Scenario: Заметка переживает refresh
- WHEN у задачи есть
issue_note.fact_status = "В работе"и приходит полное обновление зеркала - THEN
fact_statusне изменился
Scenario: Слияние с заметкой под ключом
- WHEN у
new:5заметкаwork_kind = "Аналитика",owner_noteпусто; уAURORA-1640заметкаowner_note = "спросить ИБ";ensure_issueдляnew:5завершилась сAURORA-1640 - THEN у
AURORA-1640work_kind = "Аналитика"иowner_note = "спросить ИБ"; строкиnew:5нет
Requirement: Пакеты и команды
Таблица batch SHALL хранить id, title, author, created_at, cancelled_at, estimate_seconds.
Таблица 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,
rate_limited (число 429/503), pause_seconds (выданная пауза), depends_on (id команды, чей результат подставляется).
Статус пакета SHALL вычисляться из команд и cancelled_at, не храниться: есть команды не в терминальном статусе —
«выполняется» (или «в очереди», если все queued); все терминальные и пакет отменён (cancelled_at) — «отменён»;
все терминальные без отмены — «завершён».
Scenario: Статус пакета
- WHEN в пакете 3 команды:
done,claimed,queued - THEN статус пакета «выполняется»; когда все в терминальных статусах — «завершён», с числом
failed/skipped
Scenario: Отменённый пакет с выполненной командой
- WHEN в пакете команда 1
done, команда 2claimed, команда 3queued; пакет отменён, затем агент отчитался по 2 - THEN статус пакета «отменён» (
cancelled), счётчики показывают двеdoneи однуcancelled
Requirement: Журнал синхронизаций и статус агента
Таблица sync_run SHALL хранить каждую загрузку состояния: started_at, finished_at, kind (keys | jql), query,
issues_count, links_count, jira_calls, rate_limited, command_id. При повторной выдаче команды refresh хаб
SHALL обнулить issues_count и links_count её sync_run (started_at не меняется); при переводе refresh в failed
по пределу выдач — проставить finished_at. Таблица agent_status (одна строка) SHALL хранить last_seen_at,
agent_version, settings_version, current_command_id, last_jira_call_at. Через API и MCP agent_status SHALL
отдаваться с признаком online (истина, если last_seen_at моложе 5 минут) и списком redelivered — команд в
claimed, выданных больше одного раза (id, type, claimed_at, deliveries).
Scenario: Агент давно не выходил на связь
- WHEN
last_seen_atстарше 5 минут - THEN
agent_statusчерез API и MCP отдаётся сonline = false
Scenario: Довыдача refresh не удваивает счётчики
- WHEN
refreshзагрузил 150 задач, отчёт потерян, команда выдана снова и загрузила те же 150 - THEN у её
sync_runissues_count = 150
Scenario: refresh сдался по пределу выдач
- WHEN
refreshпереведён вfailed«агент не отчитался после 3 выдач» - 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 (увеличивается при любой
правке), backup_keep, auto_refresh_minutes, relay_dir (абсолютный путь папки реле или null, по умолчанию null;
относительный путь и не-строка при записи — 422; агенту не передаётся). Спринты агент читает с доски board_id,
отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из admin/jira-sync/config.example.json
командой jira-hub init; пароль и логин Jira в настройках MUST NOT присутствовать.
Scenario: Правка настроек поднимает версию
- WHEN через API изменён
pacing - THEN
settings_versionувеличен, следующийclaimотдаёт агенту новую версию
Scenario: Неверный relay_dir
- WHEN
PUT /settingsсrelay_dir = "rshb/jira-bridge" - THEN 422, значение не записано,
settings_versionне изменился