Andrey Epifancev 30833a466a Архив OpenSpec: sprints-backlog (#108)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:24:17 +04:00

38 KiB
Raw Blame History

data-model Specification

Purpose

Модель данных хаба: один файл SQLite с зеркалом задач Jira, служебными заметками, пакетами и командами, журналом синхронизаций, статусом агента и настройками. Здесь таблицы, их смысл, правила записи (зеркало пишет только загрузка состояния от агента) и миграции схемы.

Requirements

Requirement: Схема базы хаба

База хаба SHALL быть одним файлом SQLite (WAL) с таблицами issue, issue_link, issue_comment, issue_change, issue_attachment, attachment_file, issue_note, issue_sprint, jira_board, jira_sprint, batch, command, sync_run, agent_status, setting, project. Схема SHALL создаваться и мигрироваться самим хабом при старте по номеру версии в setting.schema_version (текущая — 9). Миграция существующей базы 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.

Миграция v7 → v8 SHALL: удалить колонку issue_note.fact_status (если она есть; значения удаляются безвозвратно, копия — в backups/ по общему правилу) и ключи status_labels и transitions_map из setting, если они есть, и поднять settings_version, если хотя бы один из ключей был удалён. Прочие поля заметок, задачи и настройки миграция MUST NOT менять.

Миграция v8 → v9 SHALL: добавить колонки issue.rank и issue.story_points (у имеющихся задач NULL); таблицы jira_board, jira_sprint, issue_sprint создаются схемой пустыми. Данные появляются с первым refresh агента v15.

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 = текущая

Scenario: Миграция базы v2

  • WHEN хаб стартует с базой schema_version = 2, в которой есть задачи, связи и заметки
  • THEN в backups/ копия базы v2; добавлена пустая issue_comment; данные на месте; schema_version = текущая

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 = текущая

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 = текущая

Scenario: Миграция базы v5

  • WHEN хаб стартует с базой schema_version = 5
  • THEN в backups/ копия базы v5; есть пустые issue_attachment и attachment_file; прочие таблицы не тронуты; schema_version = текущая

Scenario: Миграция базы v7

  • WHEN хаб стартует с базой schema_version = 7, у задачи X заметка fact_status = "В работе", work_kind = "Аналитика", в setting есть status_labels и transitions_map, settings_version = 12
  • THEN в backups/ копия базы v7; в issue_note нет колонки fact_status, у X work_kind = "Аналитика", updated_at прежний; ключей status_labels и transitions_map в setting нет; settings_version = 13; schema_version = текущая

Scenario: Миграция базы v8

  • WHEN хаб стартует с базой schema_version = 8, в зеркале задача X со спринтом «Спринт 2»
  • THEN в backups/ копия базы v8; у X rank и story_points — NULL, sprint прежний; таблицы jira_board, jira_sprint, issue_sprint пусты; schema_version = 9

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, rank (ранг Jira строкой как есть или NULL), story_points (оценка, число или NULL), 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 обновлены, два вложения на месте

Requirement: Служебные заметки

Таблица issue_note SHALL хранить поля, которых нет в Jira: key (PK; ключ Jira или временный new:<n>), 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.work_kind = "Аналитика" и приходит полное обновление зеркала
  • THEN work_kind не изменился

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, project (метка проекта, если пакет поставлен в разрезе проекта, иначе NULL). Таблица 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 хранить по строке на агента (ключ agent — имя модуля, см. modules, «Агенты модулей»): last_seen_at, agent_version, settings_version, current_command_id, last_call_at (время последнего обращения агента к внешней системе; от jira-agent приходит полем last_jira_call_at). Через API и MCP статус агента SHALL отдаваться с признаком online (истина, если last_seen_at моложе 5 минут) и списком redelivered — команд его модуля в claimed, выданных больше одного раза (id, type, claimed_at, deliveries).

Scenario: Агент давно не выходил на связь

  • WHEN last_seen_at агента jira старше 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

Scenario: Статусы агентов раздельно

  • WHEN агент jira выходил на связь минуту назад, агент wiki — никогда
  • THEN GET /agents отдаёт jira с online = true и wiki с online = false, last_seen_at = null

Requirement: Настройки

Таблица setting SHALL хранить парами ключ–JSON: project_key, field_ids, issue_types, priority_map, link_types, pacing, tokens (агентский и клиентский), settings_version (увеличивается при любой правке), backup_keep, auto_refresh_minutes, relay_dir (абсолютный путь папки реле или null, по умолчанию null; относительный путь и не-строка при записи — 422; агенту не передаётся), closed_statuses (список имён статусов Jira, считающихся закрытыми, по умолчанию ["Готово", "Снято"]; не-список при записи — 422; агенту не передаётся; на существующей базе появляется со значением по умолчанию при открытии хаба). Базовая JQL вычисляется из проектов, доска — свойство проекта (требование «Проекты»); на новой базе ключей base_jql и board_id нет. Оставшиеся от v3 ключи хаб SHALL NOT использовать, кроме передачи board_id агенту как запасной доски для команд без доски в параметрах (поставленных до миграции); Ключи, которые модуль объявил устаревшими (у jira — status_labels, transitions_map), хаб SHALL удалять при каждом заполнении умолчаний, не меняя settings_version. GET /settings SHALL отдавать вычисленную base_jql только для чтения, а запись base_jql или board_id через PUT /settings — 422 с пояснением. Спринты агент читает с досок проектов, отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из admin/jira-sync/config.example.json командой sdlc-hub init; пароль и логин Jira в настройках MUST NOT присутствовать. issue_types — объект {task, epic, bug}, у каждого {name, id} (id может быть null); init берёт их из ключей конфига issue_type_<тип> и issue_type_<тип>_id; bug попадает в issue_types, только если в конфиге есть хотя бы один из ключей issue_type_bug, issue_type_bug_id (task и epic — как раньше, всегда).

Scenario: Правка настроек поднимает версию

  • WHEN через API изменён pacing
  • THEN settings_version увеличен, следующий claim отдаёт агенту новую версию

Scenario: Конфиг без ключей ошибки

  • WHEN sdlc-hub init --config с issue_type_task, issue_type_epic, без issue_type_bug
  • THEN issue_types содержит task и epic, bug нет; постановка ensure_issue с issue_type = bug — 422

Scenario: Неверный relay_dir

  • WHEN PUT /settings с relay_dir = "rshb/jira-bridge"
  • THEN 422, значение не записано, settings_version не изменился

Scenario: Закрытые статусы по умолчанию

  • WHEN хаб открыл базу, в которой нет ключа closed_statuses
  • THEN GET /settings отдаёт closed_statuses = ["Готово", "Снято"]; claim агенту этого ключа не передаёт

Scenario: Запись базовой JQL

  • WHEN PUT /settings {base_jql: "project = AURORA"}
  • THEN 422 «базовая JQL собирается из проектов»; settings_version не изменился

Scenario: Создание проекта не трогает версию настроек

  • WHEN создан проект STOCKS_DRRK
  • THEN settings_version прежний; GET /settings отдаёт base_jql с обеими метками

Scenario: Ключи вернул процесс прежней версии

  • WHEN база v8, процесс со старым кодом (MCP-сессия до reconnect) вставил status_labels = {} и transitions_map = {}, затем вызван ensure_defaults текущего кода (старт хаба, новая MCP-сессия)
  • THEN ключей в setting нет, settings_version прежний

Scenario: Ключей статуса по факту нет

  • WHEN хаб открыл базу v8
  • THEN GET /settings не содержит status_labels и transitions_map; GET /agent/settings агенту jira их не отдаёт

Requirement: Проекты

Таблица project SHALL хранить проекты моста — подмножества задач Jira-проекта project_key, выделенные меткой: label (PK, метка Jira), name, created_at. Метка SHALL быть непустой строкой до 100 символов без пробельных и управляющих символов (категория Unicode Cc), запятых, кавычек, скобок и обратной косой черты; две метки, совпадающие без учёта регистра, MUST NOT существовать одновременно. Название SHALL быть непустым после обрезки пробелов, до 100 символов. Метку и название после создания хаб не меняет (переименования и удаления нет).

Доска проекта SHALL храниться в таблице модуля jira jira_project: label (PK, ссылка на проект), board_id (номер доски Jira или NULL), board_type (scrum | kanban | NULL — тип ещё не известен). board_id — целое больше нуля или NULL, после создания не меняется; board_type SHALL записываться только по результату refresh от агента (см. agent). Миграция v7 SHALL перенести board_id и board_type всех проектов из project в jira_project. Базовая JQL SHALL вычисляться из проектов: project = <project_key> AND labels in ("<метка>", …) — каждая метка в двойных кавычках, метки по created_at, затем label, через , ; без проектов базовой JQL нет (null).

Для проекта P задачи зеркала делятся на свои — с меткой P в labels (точное совпадение) — и соседей — задачи без метки P, связанные со своей связью issue_link в любую сторону, и эпики своих (epic_key своей задачи), если они есть в зеркале. Признак neighbor в выдаче в разрезе проекта SHALL считаться относительно P: false у своих, true у соседей.

Scenario: Базовая JQL из двух проектов

  • WHEN заведены проекты AWPCA (раньше) и STOCKS_DRRK, project_key = AURORA
  • THEN базовая JQL — project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")

Scenario: Свои и соседи

  • WHEN у AURORA-1 метка STOCKS_DRRK и эпик AURORA-9 (метка AWPCA), связь AURORA-1 → AURORA-2 (метка AWPCA), у AURORA-3 метка AWPCA без связей с проектом STOCKS_DRRK
  • THEN в разрезе STOCKS_DRRK свои — AURORA-1, соседи — AURORA-2 и AURORA-9; AURORA-3 в разрез не входит

Scenario: Метка с пробелом

  • WHEN создаётся проект с меткой STOCKS DRRK
  • THEN отказ с указанием поля label, проекта нет

Scenario: Метка с обратной косой чертой или управляющим символом

  • WHEN создаётся проект с меткой A\B или A<перевод строки>B
  • THEN отказ с указанием поля label, проекта нет

Scenario: Метка — зарезервированное слово JQL

  • WHEN заведён проект с меткой EMPTY
  • THEN базовая JQL — project = AURORA AND labels in ("EMPTY"), метка не расширяет выборку

Scenario: Та же метка другим регистром

  • WHEN есть проект AWPCA, создаётся проект awpca
  • THEN отказ «проект с такой меткой уже есть», проект один

Scenario: Доски после миграции v7

  • WHEN база v6 с проектами AWPCA (board_id = 4216, scrum) и sfm_kedr (без доски) мигрирует в v7
  • THEN в project нет колонок board_id, board_type; в jira_project — AWPCA 4216 scrum и sfm_kedr NULL NULL; перед миграцией сделана копия базы

Requirement: Журнал изменений задач

Таблица issue_change SHALL хранить журнал изменений задач Jira по строке на поле записи журнала: history_id (id записи журнала Jira), seq (номер поля в записи, с 0), key, at (время записи в ISO-8601 UTC, приведённое хабом), created (время Jira как есть), author, field (логическое имя), field_id, field_name, from_value, from_string, to_value, to_string, fetched_at; PK (history_id, seq), индексы по at и по (key, at). Записывать в issue_change SHALL только приём журнала от агента (POST /agent/history); повторно присланная строка с тем же (history_id, seq) MUST NOT менять сохранённую. HTTP API интерфейсов и MCP MUST NOT писать в issue_change.

Логическое имя field SHALL вычисляться хабом при приёме: status, description, summary, assignee, priority, duedate, labels, resolution — если так называется field_id, а при его отсутствии — field_name; planned_start, planned_end — если field_id равен id этого поля в настройке field_ids; epic_link — если field_id равен epic_link.field_id; иначе — field_name как есть.

Время Jira (created журнала, created комментария) SHALL приводиться к UTC разбором ISO-8601 со смещением вида +0300, +03:00 или Z, с дробной частью секунд или без; время без смещения считается UTC; неразбираемое — NULL в приведённой колонке (строка журнала с at = NULL в ленту не попадает).

Таблица issue_comment SHALL хранить также created_at — created, приведённое к UTC, заполняемое при каждой записи комментариев.

Scenario: Повтор записи журнала

  • WHEN агент дважды прислал элемент {history_id: "5001", seq: 0, key: X, field_name: "status", …}
  • THEN в issue_change одна строка (5001, 0); второй приём ничего не изменил

Scenario: Плановая дата по id поля

  • WHEN в настройках field_ids.planned_end = "customfield_10201", агент прислал элемент с field_id = "customfield_10201", field_name = "Плановое окончание"
  • THEN у строки field = "planned_end"

Scenario: Неизвестное поле

  • WHEN агент прислал элемент с field_id = null, field_name = "Fix Version"
  • THEN у строки field = "Fix Version"

Scenario: Время со смещением

  • WHEN элемент журнала с created = "2026-10-22T09:00:00.000+0300"
  • THEN у строки at = "2026-10-22T06:00:00+00:00", created — как прислано

Requirement: Отметка журнала в журнале синхронизаций

Таблица sync_run SHALL хранить history_at — время, когда хаб принял итог журнала по этому прогону (NULL — журнал по прогону не принят). Хаб SHALL проставлять history_at при отчёте полного прогона (refresh по базовой JQL) со статусом done и result.history без error; у прочих refresh, при failed, cancelled, без result.history или с history.error — оставлять NULL. history.backfill_error и непустой history.incomplete отметку не держат.

Scenario: Журнал принят

  • WHEN refresh по базовой JQL завершился done с result.history = {issues: 3, changes: 7, incomplete: []}
  • THEN у его sync_run проставлен history_at

Scenario: Ошибка стартовой загрузки не держит отметку

  • WHEN refresh по базовой JQL завершился done с result.history = {issues: 1, changes: 2, incomplete: [], backfill_error: "HTTP 400 …"}
  • THEN у его sync_run проставлен history_at

Scenario: Ошибка журнала

  • WHEN refresh завершился done с result.history = {error: "HTTP 500 …"}
  • THEN у его sync_run history_at = NULL

Scenario: Старый агент

  • WHEN агент v12 завершил refresh done без result.history
  • THEN у его sync_run history_at = NULL

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

Requirement: Доски, спринты и членство задач

Модуль jira SHALL хранить данные досок проектов, присланные агентом в refresh (см. hub-api, «Приём досок и спринтов»):

  • jira_board: board_id (PK), type (scrum | kanban | NULL), columns (JSON-список [{name, statuses: [имя статуса]}] в порядке доски или NULL), kanban_backlog (1 / 0 / NULL — неизвестно), fetched_at (UTC);
  • jira_sprint: sprint_id (PK, id спринта Jira), board_id, name, state (active | future | closed), start_date, end_date, complete_date (строки Jira как есть или NULL), goal;
  • issue_sprint: key, sprint_id (PK — пара) — все спринты задачи любого состояния. Писать их SHALL только загрузка состояния от агента. Выполненной на доске SHALL считаться задача, чей status входит в statuses последней колонки доски; колонок нет (NULL) — выполненных нет.

Scenario: Задача прошла два спринта

  • WHEN агент прислал задачу X с sprint_ids: [101, 102], спринт 101 закрыт, 102 активен
  • THEN у X две строки issue_sprint; X в составе закрытого спринта 101 и активного 102

Scenario: Колонки доски

  • WHEN у доски 4216 колонки «К выполнению» [Backlog, Ожидает анализа], «В работе» […], «Готово» [Готово, Протестировано, Снято]
  • THEN задача в статусе Протестировано выполнена, в статусе Backlog — нет