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:
238
crates/infrastructure/src/orchestrator/mcp/tickets.rs
Normal file
238
crates/infrastructure/src/orchestrator/mcp/tickets.rs
Normal file
@ -0,0 +1,238 @@
|
||||
//! Public MCP ticket tools.
|
||||
//!
|
||||
//! The public surface says `ticket`, while the provider behind this trait maps
|
||||
//! to the application/domain `Issue` use cases.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::Project;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::tools::ToolDef;
|
||||
|
||||
/// Error returned by an MCP ticket tool provider.
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
#[error("{code}: {message}")]
|
||||
pub struct TicketToolError {
|
||||
/// Stable machine-readable code.
|
||||
pub code: &'static str,
|
||||
/// Human-readable message.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl TicketToolError {
|
||||
/// Builds a typed ticket-tool error.
|
||||
#[must_use]
|
||||
pub fn new(code: &'static str, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialises the error as a camelCase JSON object.
|
||||
#[must_use]
|
||||
pub fn to_value(&self) -> Value {
|
||||
json!({ "code": self.code, "message": self.message })
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider injected by the composition root to execute public ticket tools.
|
||||
#[async_trait]
|
||||
pub trait TicketToolProvider: Send + Sync {
|
||||
/// Executes one public `idea_ticket_*` tool for `project`.
|
||||
async fn handle_ticket_tool(
|
||||
&self,
|
||||
project: &Project,
|
||||
requester: &str,
|
||||
name: &str,
|
||||
arguments: Value,
|
||||
) -> Result<Value, TicketToolError>;
|
||||
}
|
||||
|
||||
/// Returns true when `name` is one of the public ticket tools.
|
||||
#[must_use]
|
||||
pub fn is_ticket_tool(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"idea_ticket_create"
|
||||
| "idea_ticket_read"
|
||||
| "idea_ticket_list"
|
||||
| "idea_ticket_update"
|
||||
| "idea_ticket_update_status"
|
||||
| "idea_ticket_update_priority"
|
||||
| "idea_ticket_read_carnet"
|
||||
| "idea_ticket_update_carnet"
|
||||
| "idea_ticket_link"
|
||||
| "idea_ticket_unlink"
|
||||
)
|
||||
}
|
||||
|
||||
/// Public ticket tool definitions advertised by `tools/list`.
|
||||
#[must_use]
|
||||
pub fn catalogue() -> Vec<ToolDef> {
|
||||
let ticket_ref = json!({ "type": "string", "pattern": "^#[1-9][0-9]*$" });
|
||||
let status = json!({ "type": "string", "enum": ["open", "inProgress", "QA", "closed"] });
|
||||
let priority = json!({ "type": "string", "enum": ["low", "medium", "high", "critical"] });
|
||||
let link_kind = json!({
|
||||
"type": "string",
|
||||
"enum": ["relatesTo", "blocks", "blockedBy", "duplicates", "dependsOn"]
|
||||
});
|
||||
let link = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"targetRef": ticket_ref.clone(),
|
||||
"kind": link_kind.clone()
|
||||
},
|
||||
"required": ["targetRef", "kind"],
|
||||
"additionalProperties": false
|
||||
});
|
||||
|
||||
vec![
|
||||
ToolDef {
|
||||
name: "idea_ticket_create",
|
||||
description: "Create an IdeA ticket in the current project and return it as JSON.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"priority": priority.clone(),
|
||||
"status": status.clone(),
|
||||
"assignedAgentIds": { "type": "array", "items": { "type": "string", "format": "uuid" } },
|
||||
"links": { "type": "array", "items": link.clone() }
|
||||
},
|
||||
"required": ["title"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_read",
|
||||
description: "Read one IdeA ticket by public reference (#N).",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ref": ticket_ref.clone(),
|
||||
"includeCarnet": { "type": "boolean" }
|
||||
},
|
||||
"required": ["ref"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_list",
|
||||
description: "List IdeA tickets in the current project.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": status.clone(),
|
||||
"priority": priority.clone(),
|
||||
"assignedAgentId": { "type": "string", "format": "uuid" },
|
||||
"text": { "type": "string" },
|
||||
"limit": { "type": "integer", "minimum": 1 },
|
||||
"cursor": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_update",
|
||||
description: "Update ticket fields using optimistic concurrency.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ref": ticket_ref.clone(),
|
||||
"title": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"status": status.clone(),
|
||||
"priority": priority.clone(),
|
||||
"assignedAgentIds": { "type": "array", "items": { "type": "string", "format": "uuid" } },
|
||||
"expectedVersion": { "type": "integer", "minimum": 1 }
|
||||
},
|
||||
"required": ["ref", "expectedVersion"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_update_status",
|
||||
description: "Update a ticket status using optimistic concurrency.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ref": ticket_ref.clone(),
|
||||
"status": status.clone(),
|
||||
"expectedVersion": { "type": "integer", "minimum": 1 }
|
||||
},
|
||||
"required": ["ref", "status", "expectedVersion"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_update_priority",
|
||||
description: "Update a ticket priority using optimistic concurrency.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ref": ticket_ref.clone(),
|
||||
"priority": priority.clone(),
|
||||
"expectedVersion": { "type": "integer", "minimum": 1 }
|
||||
},
|
||||
"required": ["ref", "priority", "expectedVersion"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_read_carnet",
|
||||
description: "Read the editable carnet attached to a ticket.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": { "ref": ticket_ref.clone() },
|
||||
"required": ["ref"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_update_carnet",
|
||||
description: "Update a ticket carnet using optimistic concurrency.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ref": ticket_ref.clone(),
|
||||
"carnet": { "type": "string" },
|
||||
"expectedVersion": { "type": "integer", "minimum": 1 }
|
||||
},
|
||||
"required": ["ref", "carnet", "expectedVersion"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_link",
|
||||
description: "Add a link from one ticket to another using optimistic concurrency.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ref": ticket_ref.clone(),
|
||||
"targetRef": ticket_ref.clone(),
|
||||
"kind": link_kind.clone(),
|
||||
"expectedVersion": { "type": "integer", "minimum": 1 }
|
||||
},
|
||||
"required": ["ref", "targetRef", "kind", "expectedVersion"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_ticket_unlink",
|
||||
description: "Remove a link from one ticket to another using optimistic concurrency.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ref": ticket_ref.clone(),
|
||||
"targetRef": ticket_ref,
|
||||
"kind": link_kind,
|
||||
"expectedVersion": { "type": "integer", "minimum": 1 }
|
||||
},
|
||||
"required": ["ref", "targetRef", "expectedVersion"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user