Aller au contenu

Cycle de vie des tickets

Statut Chaîne stockée Description
backlog backlog Saisi, pas encore trié dans la file active
open open Trié et prêt à être travaillé, pas encore commencé
inProgress inProgress En cours de traitement actif
blocked blocked Commencé, mais bloqué par un élément externe
inReview inReview Travail terminé, en attente de révision
done done Terminé avec succès (état terminal)
failed failed Terminé avec une erreur (état terminal)
cancelled cancelled Abandonné (état terminal)

Les valeurs de statut sont stockées en camelCase (inProgress, inReview), jamais in_progress. Une valeur stockée nulle est interprétée comme open ; une valeur inconnue lève une exception plutôt que d’être convertie.

tryParseLoose accepte les alias fournis par les agents, en ignorant la casse et les caractères - / _ / espace :

Statut Alias acceptés
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

Ce graphe ne lie que les agents et l’automatisation. TicketWorkflowService.transitionStatus le vérifie quand force est faux — les outils MCP et les réconciliateurs. Une transition illégale y est journalisée et ignorée. Tout changement de statut déclenché par l’utilisateur dans l’interface passe force: true et contourne entièrement le graphe ; une personne peut donc déplacer un ticket vers n’importe quel statut figurant dans le sélecteur (tous les statuts sauf failed), y compris rouvrir un état terminal. Les agents marquent failed avec fail_ticket.

Un statut peut toujours effectuer une transition vers lui-même. Les états terminaux n’ont aucune arête sortante.

De Cibles autorisées
backlog open, cancelled
open inProgress, blocked, cancelled
inProgress blocked, inReview, done, failed, cancelled
blocked inProgress, cancelled
inReview inProgress, done, failed, cancelled
done — (état terminal)
failed — (état terminal)
cancelled — (état terminal)

Stockée sous forme d’entier sur l’échelle native 0–4 de Linear. Une valeur inconnue est interprétée comme none.

Priorité Stockée sous
none 0
urgent 1
high 2
medium 3
low 4

Une valeur stockée inconnue ou nulle est interprétée comme local.

Fournisseur État
local Stocké entièrement dans la base de données de l’espace de travail ; valeur par défaut
linear Détenu à distance, mis en miroir localement

Les adaptateurs de synchronisation vivent sous packages/cc_infra/lib/src/tickets/sync/ pour Linear, GitHub Issues, Jira Cloud et ClickUp. GitHub Issues n’est qu’un fournisseur d’adaptateur de synchronisation (vendorId: github) — ce n’est pas une valeur de TicketProvider, donc un ticket GitHub récupéré est stocké avec originKind: externalSync et provider: local par défaut. La correspondance des fournisseurs vit dans ticket_sync_links.

createTicket écrit manual. Une récupération chez un fournisseur écrit externalSync. Les enfants délégués restent manual.

Valeur Signification
manual Valeur par défaut de createTicket (lignes humaines, d’agent et déléguées)
externalSync Défini par TicketSyncEngine lors d’une récupération chez un fournisseur

L’agrégat couvre deux préoccupations sur une seule ligne. Le miroir (fournisseur, clés, titre, description, priorité, étiquettes, statuts, horodatages) est un cache pour un fournisseur distant et est réécrit au complet par une actualisation. La surcouche (attribution, délégation, espace, parent, projet, PR liées) n’existe que dans Control Center, et une actualisation n’y touche jamais.

Champ Type Description
id String UUID v4
workspaceId String Espace de travail propriétaire
provider TicketProvider Backend propriétaire des données canoniques (par défaut local)
externalKey String? Clé propre au fournisseur (p. ex. LIN-123). Le createTicket local y écrit un UUID distinct, et non le id de la ligne
url String? URL Web chez le fournisseur distant
title String Court résumé ; ne doit pas être vide
description String? Corps Markdown plus long
priority TicketPriority Valeur par défaut none
labels List<String> Étiquettes libres
status TicketStatus Statut normalisé canonique
rawStatus String? Nom d’état natif du fournisseur distant, conservé pour un affichage sans perte
externalSyncedAt DateTime? Dernière synchronisation multifournisseur ; nul avant la première. La correspondance par fournisseur vit dans ticket_sync_links
parentTicketId String? Parent dans l’arbre de délégation / de décomposition
projectId String? Projet propriétaire ; jamais transmis à un système distant
assignedAgentId String? Identifiant du principal attribué (métadonnées seulement — attribuer ne lance rien)
assigneeType PrincipalType Type de principal que désigne assignedAgentId — user ou agent (par défaut agent)
createdByType PrincipalType? Type de principal ayant créé le ticket (user ou agent) ; nul pour les lignes héritées ou système
createdById String? Identifiant du principal créateur
assignedTeamId String? Équipe attribuée (métadonnées seulement)
delegatedByAgentId String? Agent qui a délégué ce ticket
delegationDepth int Profondeur dans la chaîne de délégation ; la racine vaut 0, comparée au plafond de profondeur maximale
delegationRootTicketId String? Racine de la chaîne de délégation ; nul pour un ticket racine
spaceId String? Conversation dont le ticket a été tiré, ou espace de l’agent délégant
errorMessage String? Défini quand status vaut failed
linkedPrIds List<String> Identifiants de nœud PR auxquels ce ticket est lié
metadata Map<String, dynamic> Fourre-tout libre
createdAt DateTime Horodatage de création
startedAt DateTime? Moment où le travail a commencé
blockedAt DateTime? Moment où le ticket a été bloqué
cancelledAt DateTime? Moment où le ticket a été annulé
completedAt DateTime? Moment où le ticket s’est terminé avec succès
finishedAt DateTime? Moment où le ticket a atteint un état terminal
updatedAt DateTime Dernière mutation (actualisation du miroir ou modification de la surcouche)
version int Compteur de concurrence optimiste, incrémenté à chaque mutation
originKind TicketOriginKind Comment le ticket a été créé (par défaut manual)
collaborators List<TicketCollaborator> Hydraté à la demande par le dépôt

Dérivés : isTerminal, isRemote (provider != local), isAssignedToUser et displayKey (externalKey ?? id — un ticket local affiche donc l’UUID distinct généré à la création, et non le id de la ligne).

Les arêtes stockées sont des lignes directionnelles dans ticket_links :

Type Chaîne stockée Signification
blocks blocks La source bloque la cible
relatesTo relates_to Relation symétrique
duplicateOf duplicate_of La source est un doublon de la cible

Le vocabulaire utilisé par l’interface et par ticket_relation est TicketRelationKind, dérivé d’une arête stockée et de l’extrémité où se trouve le sujet :

Type Dérivé de
blockedBy Une ligne blocks où le sujet est la cible
blocking Une ligne blocks où le sujet est la source
relatedTo Une ligne relates_to (symétrique)
duplicateOf Une ligne duplicate_of où le sujet est la source
duplicatedBy Une ligne duplicate_of où le sujet est la cible
subIssueOf tickets.parent_ticket_id sur le sujet
parentOf tickets.parent_ticket_id sur l’autre ticket

Les liens de parent et de sous-ticket ne sont pas des lignes de ticket_links — ils vivent dans tickets.parent_ticket_id.

Ces huit classes vivent dans packages/cc_domain/lib/core/domain/events/ticketing_events.dart. Il n’existe aucun TicketStarted, TicketDelegated, TicketCollaboratorAdded ni ExternalTicketWebhookReceived.

Événement Déclenché quand
TicketCreated Le ticket est créé
TicketAssigned Le ticket est attribué à un principal ou à une équipe
TicketCompleted Le travail se termine avec succès
TicketFailed Le travail échoue
TicketCancelled Le ticket est annulé
TicketStatusChanged Tout changement de statut
TicketReassigned Le ticket est réattribué
TicketDetailsUpdated Le titre, la description, la priorité ou les étiquettes changent

TicketAssigned est un signal de vérification et de notification, ainsi qu’un déclencheur de pipeline. L’attribution à un agent ne démarre aucune exécution — aucun répartiteur de tickets ne surveille assignedAgentId. L’attribution à une équipe est l’exception : TeamRoutingService lance le responsable de l’équipe. L’ajout d’un collaborateur écrit une ligne et ne publie rien. Un webhook de fournisseur est appliqué par TicketSyncEngine sans événement de domaine.

TicketWorkflowService détient chaque mutation.

Mécanisme Détail
Point de passage unique Toutes les mutations passent par _mutate, qui charge la ligne et appelle _assertWorkspace avant d’appliquer
Isolation des espaces de travail _assertWorkspace lève WorkspaceMismatchException quand le workspaceId de la ligne chargée ne correspond pas à celui de l’appelant
Concurrence optimiste _mutate écrit avec expectedVersion fixé à la version qu’il vient de lire et réessaie sur ConcurrencyConflictException en relisant l’état à jour
Réservation exclusive tryCheckout / releaseCheckout ; un deuxième agent qui réclame un ticket déjà réservé reçoit une CheckoutConflictException
Garde de transition canTransitionTo, appliqué seulement quand force est faux