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

13 KiB
Raw Blame History

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