feat(tickets): checkpoint backend V1 du système de tickets (T1-T5)

Checkpoint de travail — QA formelle encore à venir (après reset Codex).
`cargo build` workspace OK, tests ticket/issue verts (domaine, store,
use cases, app-tauri 47/51). Le domaine s'appelle `Issue`, exposé
« ticket » côté MCP/UI.

- T1 domaine : entité Issue (statut, priorité, liens, carnet),
  events, ids, ports.
- T2 infra : store FS Markdown + allocator de références #N.
- T3 application : use cases Issue (create/read/list/update/status/
  priority/carnet/link/unlink/assign) + erreurs dédiées.
- T4 surface MCP : 10 outils publics idea_ticket_* (23 outils au
  total), mappés vers les use cases Issue.
- T5 app-tauri : commandes Tauri ticket_* + DTOs d'events Issue +
  câblage state.rs.

Dette de test PRÉ-EXISTANTE héritée de develop (4 tests app-tauri
mcp_e2e_loopback / mcp_serve_peer rouges) hors périmètre tickets,
non traitée ici.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 22:34:04 +02:00
parent a9653bc417
commit 8de7be01a8
22 changed files with 4395 additions and 50 deletions

View File

@ -14,6 +14,7 @@ use tauri::{AppHandle, Emitter};
use domain::events::{DomainEvent, OrchestrationSource};
use domain::input::AgentLiveness;
use domain::{IssueLinkKind, IssuePriority, IssueStatus};
use infrastructure::TokioBroadcastEventBus;
/// Name of the Tauri event carrying relayed [`DomainEvent`]s.
@ -158,6 +159,94 @@ pub enum DomainEventDto {
/// Project id.
project_id: String,
},
/// An issue-backed public ticket was created.
#[serde(rename_all = "camelCase")]
IssueCreated {
/// Issue id.
issue_id: String,
/// Public ticket reference (`#N`).
issue_ref: String,
},
/// An issue-backed public ticket was updated.
#[serde(rename_all = "camelCase")]
IssueUpdated {
/// Public ticket reference (`#N`).
issue_ref: String,
/// New optimistic version.
version: u64,
},
/// A public ticket status changed.
#[serde(rename_all = "camelCase")]
IssueStatusChanged {
/// Public ticket reference (`#N`).
issue_ref: String,
/// New status.
status: String,
/// New optimistic version.
version: u64,
},
/// A public ticket priority changed.
#[serde(rename_all = "camelCase")]
IssuePriorityChanged {
/// Public ticket reference (`#N`).
issue_ref: String,
/// New priority.
priority: String,
/// New optimistic version.
version: u64,
},
/// A public ticket carnet changed.
#[serde(rename_all = "camelCase")]
IssueCarnetUpdated {
/// Public ticket reference (`#N`).
issue_ref: String,
/// New optimistic version.
version: u64,
},
/// A public ticket link was added.
#[serde(rename_all = "camelCase")]
IssueLinked {
/// Public source ticket reference (`#N`).
issue_ref: String,
/// Public target ticket reference (`#N`).
target: String,
/// Link kind.
kind: String,
/// New optimistic version.
version: u64,
},
/// A public ticket link was removed.
#[serde(rename_all = "camelCase")]
IssueUnlinked {
/// Public source ticket reference (`#N`).
issue_ref: String,
/// Public target ticket reference (`#N`).
target: String,
/// Link kind.
kind: String,
/// New optimistic version.
version: u64,
},
/// An agent was assigned to a public ticket.
#[serde(rename_all = "camelCase")]
IssueAgentAssigned {
/// Public ticket reference (`#N`).
issue_ref: String,
/// Assigned agent id.
agent_id: String,
/// New optimistic version.
version: u64,
},
/// An agent was unassigned from a public ticket.
#[serde(rename_all = "camelCase")]
IssueAgentUnassigned {
/// Public ticket reference (`#N`).
issue_ref: String,
/// Unassigned agent id.
agent_id: String,
/// New optimistic version.
version: u64,
},
/// An orchestrator request was processed on behalf of a requester agent.
#[serde(rename_all = "camelCase")]
OrchestratorRequestProcessed {
@ -312,6 +401,34 @@ impl From<OrchestrationSource> for OrchestrationSourceDto {
}
}
fn issue_status_wire(status: IssueStatus) -> &'static str {
match status {
IssueStatus::Open => "open",
IssueStatus::InProgress => "inProgress",
IssueStatus::Qa => "QA",
IssueStatus::Closed => "closed",
}
}
fn issue_priority_wire(priority: IssuePriority) -> &'static str {
match priority {
IssuePriority::Low => "low",
IssuePriority::Medium => "medium",
IssuePriority::High => "high",
IssuePriority::Critical => "critical",
}
}
fn issue_link_kind_wire(kind: IssueLinkKind) -> &'static str {
match kind {
IssueLinkKind::RelatesTo => "relatesTo",
IssueLinkKind::Blocks => "blocks",
IssueLinkKind::BlockedBy => "blockedBy",
IssueLinkKind::Duplicates => "duplicates",
IssueLinkKind::DependsOn => "dependsOn",
}
}
impl From<&DomainEvent> for DomainEventDto {
fn from(e: &DomainEvent) -> Self {
match e {
@ -400,6 +517,79 @@ impl From<&DomainEvent> for DomainEventDto {
DomainEvent::GitStateChanged { project_id } => Self::GitStateChanged {
project_id: project_id.to_string(),
},
DomainEvent::IssueCreated {
issue_id,
issue_ref,
} => Self::IssueCreated {
issue_id: issue_id.to_string(),
issue_ref: issue_ref.to_string(),
},
DomainEvent::IssueUpdated { issue_ref, version } => Self::IssueUpdated {
issue_ref: issue_ref.to_string(),
version: version.get(),
},
DomainEvent::IssueStatusChanged {
issue_ref,
status,
version,
} => Self::IssueStatusChanged {
issue_ref: issue_ref.to_string(),
status: issue_status_wire(*status).to_owned(),
version: version.get(),
},
DomainEvent::IssuePriorityChanged {
issue_ref,
priority,
version,
} => Self::IssuePriorityChanged {
issue_ref: issue_ref.to_string(),
priority: issue_priority_wire(*priority).to_owned(),
version: version.get(),
},
DomainEvent::IssueCarnetUpdated { issue_ref, version } => Self::IssueCarnetUpdated {
issue_ref: issue_ref.to_string(),
version: version.get(),
},
DomainEvent::IssueLinked {
issue_ref,
target,
kind,
version,
} => Self::IssueLinked {
issue_ref: issue_ref.to_string(),
target: target.to_string(),
kind: issue_link_kind_wire(*kind).to_owned(),
version: version.get(),
},
DomainEvent::IssueUnlinked {
issue_ref,
target,
kind,
version,
} => Self::IssueUnlinked {
issue_ref: issue_ref.to_string(),
target: target.to_string(),
kind: issue_link_kind_wire(*kind).to_owned(),
version: version.get(),
},
DomainEvent::IssueAgentAssigned {
issue_ref,
agent_id,
version,
} => Self::IssueAgentAssigned {
issue_ref: issue_ref.to_string(),
agent_id: agent_id.to_string(),
version: version.get(),
},
DomainEvent::IssueAgentUnassigned {
issue_ref,
agent_id,
version,
} => Self::IssueAgentUnassigned {
issue_ref: issue_ref.to_string(),
agent_id: agent_id.to_string(),
version: version.get(),
},
DomainEvent::OrchestratorRequestProcessed {
requester_id,
action,