Перейти до вмісту

Життєвий цикл тікета

Статус Рядок у сховищі Опис
backlog backlog Зафіксовано, ще не розібрано в активну чергу
open open Розібрано й готове до роботи, ще не розпочате
inProgress inProgress Активно виконується
blocked blocked Розпочато, але заблоковано чимось зовнішнім
inReview inReview Роботу виконано, очікує перевірки
done done Завершено успішно (термінальний)
failed failed Завершено з помилкою (термінальний)
cancelled cancelled Покинуто (термінальний)

Значення статусів зберігаються в camelCase (inProgress, inReview), ніколи не in_progress. Null-значення у сховищі розбирається як open; невідоме значення кидає виняток, а не зводиться до чогось іншого.

tryParseLoose приймає псевдоніми, надані агентом, ігноруючи регістр і - / _ / пробіл:

Статус Прийняті псевдоніми
open todo, ready, pending, new
inProgress doing, started, active, wip
blocked stuck
inReview review, reviewing
done closed, complete, completed, finished
failed error, errored
cancelled canceled, abandoned, dropped
backlog ──→ open ──→ inProgress ──→ inReview ──→ done
│ │ │
│ └──→ failed ┘
└──→ blocked ──→ inProgress
Any non-terminal status ──→ cancelled; inReview ──→ failed

Цей граф застосовується лише до агентів і автоматизації. TicketWorkflowService.transitionStatus перевіряє його, коли force дорівнює false, — це інструменти MCP та узгоджувачі. Неприпустимий перехід там записується в журнал і ігнорується. Кожна зміна статусу з боку користувача в інтерфейсі передає force: true і повністю обминає граф, тож людина може перевести тікет у будь-який статус зі списку селектора (кожен статус, крім failed), зокрема відкрити термінальний заново. Агенти позначають failed через fail_ticket.

Статус завжди може перейти сам у себе. Термінальні стани не мають вихідних ребер.

Зі статусу Дозволені цілі
backlog open, cancelled
open inProgress, blocked, cancelled
inProgress blocked, inReview, done, failed, cancelled
blocked inProgress, cancelled
inReview inProgress, done, failed, cancelled
done — (термінальний)
failed — (термінальний)
cancelled — (термінальний)

Зберігається як ціле число у власній шкалі Linear 0–4. Невідоме значення розбирається як none.

Пріоритет Зберігається як
none 0
urgent 1
high 2
medium 3
low 4

Невідоме або null-значення у сховищі розбирається як local.

Постачальник Стан
local Зберігається цілком у базі даних робочого простору; типово
linear Належить віддаленій системі, дзеркалиться локально

Адаптери синхронізації живуть у packages/cc_infra/lib/src/tickets/sync/ для Linear, GitHub Issues, Jira Cloud і ClickUp. GitHub Issues — це лише постачальник адаптера синхронізації (vendorId: github), а не значення TicketProvider, тож витягнутий issue з GitHub зберігається з originKind: externalSync і типовим provider: local. Зіставлення постачальників живе в ticket_sync_links.

createTicket записує manual. Витяг від постачальника записує externalSync. Делеговані дочірні тікети залишаються manual.

Значення Опис
manual Типове для createTicket (рядки від людини, агента й делеговані)
externalSync Встановлює TicketSyncEngine під час витягу від постачальника

Агрегат поєднує дві площини в одному рядку. Дзеркало (постачальник, ключі, заголовок, опис, пріоритет, мітки, статуси, часові мітки) — це кеш для віддаленого постачальника, який оновлення перезаписує повністю. Оверлей (призначення, делегування, простір, батьківський тікет, проєкт, пов’язані PR) існує лише в Control Center, і оновлення ніколи його не торкається.

Поле Тип Опис
id String UUID v4
workspaceId String Робочий простір-власник
provider TicketProvider Бекенд, який володіє канонічними даними (типово local)
externalKey String? Ключ, рідний для постачальника (наприклад, LIN-123). Локальний createTicket записує сюди окремий UUID, а не id рядка
url String? Веб-URL у віддаленого постачальника
title String Короткий підсумок; не може бути порожнім
description String? Довше тіло у форматі Markdown
priority TicketPriority Типово none
labels List<String> Мітки довільної форми
status TicketStatus Канонічний нормалізований статус
rawStatus String? Рідна назва стану у віддаленого постачальника, збережена для показу без втрат
externalSyncedAt DateTime? Остання синхронізація між кількома постачальниками; null до першої. Зіставлення для кожного постачальника живе в ticket_sync_links
parentTicketId String? Батько в дереві делегування / розбиття
projectId String? Проєкт-власник; ніколи не надсилається на віддалену систему
assignedAgentId String? id призначеного суб’єкта (лише метадані — призначення нічого не запускає)
assigneeType PrincipalType Який різновид суб’єкта називає assignedAgentId — user чи agent (типово agent)
createdByType PrincipalType? Різновид суб’єкта, який створив тікет (user чи agent); null для застарілих/системних рядків
createdById String? id суб’єкта, який створив тікет
assignedTeamId String? Призначена команда (лише метадані)
delegatedByAgentId String? Агент, який делегував цей тікет
delegationDepth int Глибина в ланцюгу делегування; корінь — 0, перевіряється проти обмеження максимальної глибини
delegationRootTicketId String? Корінь ланцюга делегування; null для кореневого тікета
spaceId String? Розмова, з якої виокремився тікет, або простір агента, що делегує
errorMessage String? Встановлюється, коли status дорівнює failed
linkedPrIds List<String> id вузлів PR, з якими пов’язано цей тікет
metadata Map<String, dynamic> Контейнер довільної форми
createdAt DateTime Час створення
startedAt DateTime? Коли робота почалася
blockedAt DateTime? Коли тікет було заблоковано
cancelledAt DateTime? Коли тікет було скасовано
completedAt DateTime? Коли тікет успішно завершився
finishedAt DateTime? Коли тікет досяг будь-якого термінального стану
updatedAt DateTime Остання зміна (оновлення дзеркала або зміна оверлею)
version int Лічильник оптимістичної конкурентності, який зростає за кожної зміни
originKind TicketOriginKind Як тікет було створено (типово manual)
collaborators List<TicketCollaborator> Наповнюються репозиторієм на вимогу

Похідні: isTerminal, isRemote (provider != local), isAssignedToUser і displayKey (externalKey ?? id — тож локальні тікети показують окремий UUID, створений під час створення, а не id рядка).

Збережені ребра — це напрямлені рядки в ticket_links:

Тип Рядок у сховищі Значення
blocks blocks Джерело блокує ціль
relatesTo relates_to Симетричний зв’язок
duplicateOf duplicate_of Джерело є дублікатом цілі

Словник, яким послуговуються інтерфейс і ticket_relation, — це TicketRelationKind, виведений зі збереженого ребра плюс те, на якому кінці стоїть суб’єкт:

Вид Виведено з
blockedBy Рядок blocks, де суб’єкт — ціль
blocking Рядок blocks, де суб’єкт — джерело
relatedTo Рядок relates_to (симетричний)
duplicateOf Рядок duplicate_of, де суб’єкт — джерело
duplicatedBy Рядок duplicate_of, де суб’єкт — ціль
subIssueOf tickets.parent_ticket_id на суб’єкті
parentOf tickets.parent_ticket_id на іншому тікеті

Зв’язки батьківського тікета й підзадачі — не рядки ticket_links; вони живуть у tickets.parent_ticket_id.

Ці вісім класів живуть у packages/cc_domain/lib/core/domain/events/ticketing_events.dart. Немає TicketStarted, TicketDelegated, TicketCollaboratorAdded чи ExternalTicketWebhookReceived.

Подія Коли спрацьовує
TicketCreated Тікет створено
TicketAssigned Тікет призначено суб’єкту або команді
TicketCompleted Робота успішно завершується
TicketFailed Робота зазнає невдачі
TicketCancelled Тікет скасовано
TicketStatusChanged Будь-яка зміна статусу
TicketReassigned Тікет перепризначено
TicketDetailsUpdated Змінюються заголовок, опис, пріоритет або мітки

TicketAssigned — це сигнал аудиту й сповіщень та тригер конвеєра. Призначення агенту не запускає жодного запуску — немає диспетчера тікетів, який стежив би за assignedAgentId. Призначення команді — виняток: TeamRoutingService запускає лідера команди. Додавання учасника записує рядок і нічого не публікує. Вебхук від постачальника застосовується через TicketSyncEngine без доменної події.

TicketWorkflowService володіє кожною зміною.

Механізм Деталі
Єдина контрольна точка Усі зміни проходять через _mutate, який завантажує рядок і перед застосуванням викликає _assertWorkspace
Ізоляція робочого простору _assertWorkspace кидає WorkspaceMismatchException, коли workspaceId завантаженого рядка не збігається з workspaceId того, хто викликає
Оптимістична конкурентність _mutate записує з expectedVersion, встановленим у щойно прочитаний version, і повторює спробу на ConcurrencyConflictException, перечитуючи свіжий стан
Виключне резервування tryCheckout / releaseCheckout; другий агент, який претендує на вже зарезервований тікет, отримує CheckoutConflictException
Запобіжник переходів canTransitionTo, застосовується лише коли force дорівнює false