Andrey Epifancev 30d904e1a8 docs(openspec): архив bridge-hygiene (#13)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:22:53 +04:00

12 KiB
Raw Blame History

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-1640 work_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, команда 2 claimed, команда 3 queued; пакет отменён, затем агент отчитался по 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_run issues_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 не изменился