feat(sprints): modèle de sprints — domaine, use-cases, persistance et surfaces (backend)

Ticket #10 — introduction du modèle de sprints côté backend.

- Domaine : nouvel agrégat Sprint (sprint.rs), IDs, événements et invariants ;
  rattachement des issues à un sprint (issue.rs) et ports associés.
- Application : use-cases sprints (application/src/sprints) + erreurs dédiées.
- Infrastructure : store de sprints (infrastructure/src/sprints.rs), adaptation
  du store d'issues et exposition MCP via orchestrator/mcp/tickets.rs.
- app-tauri : commandes, state et events pour piloter les sprints depuis l'UI.

Tests domaine/application/infra/app-tauri verts (sprint_usecases, sprint_store,
issue_store, mcp_server).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 10:57:33 +02:00
parent 9740149ebb
commit 48b652ff91
21 changed files with 2896 additions and 45 deletions

View File

@ -299,6 +299,52 @@ pub enum DomainEventDto {
/// New optimistic version.
version: u64,
},
/// A sprint was created.
#[serde(rename_all = "camelCase")]
SprintCreated {
/// Stable sprint id.
sprint_id: String,
/// Reorderable order.
order: u32,
},
/// A sprint was renamed.
#[serde(rename_all = "camelCase")]
SprintRenamed {
/// Stable sprint id.
sprint_id: String,
/// New name.
name: String,
/// New optimistic version.
version: u64,
},
/// A sprint was reordered.
#[serde(rename_all = "camelCase")]
SprintReordered {
/// Stable sprint id.
sprint_id: String,
/// New order.
order: u32,
/// New optimistic version.
version: u64,
},
/// A sprint was deleted.
#[serde(rename_all = "camelCase")]
SprintDeleted {
/// Stable sprint id.
sprint_id: String,
},
/// A ticket changed sprint membership.
#[serde(rename_all = "camelCase")]
IssueSprintChanged {
/// Public ticket reference (`#N`).
issue_ref: String,
/// Previous sprint id.
from: Option<String>,
/// New sprint id.
to: Option<String>,
/// New optimistic version.
version: u64,
},
/// An orchestrator request was processed on behalf of a requester agent.
#[serde(rename_all = "camelCase")]
OrchestratorRequestProcessed {
@ -773,6 +819,42 @@ impl From<&DomainEvent> for DomainEventDto {
agent_id: agent_id.to_string(),
version: version.get(),
},
DomainEvent::SprintCreated { sprint_id, order } => Self::SprintCreated {
sprint_id: sprint_id.to_string(),
order: order.get(),
},
DomainEvent::SprintRenamed {
sprint_id,
name,
version,
} => Self::SprintRenamed {
sprint_id: sprint_id.to_string(),
name: name.clone(),
version: version.get(),
},
DomainEvent::SprintReordered {
sprint_id,
order,
version,
} => Self::SprintReordered {
sprint_id: sprint_id.to_string(),
order: order.get(),
version: version.get(),
},
DomainEvent::SprintDeleted { sprint_id } => Self::SprintDeleted {
sprint_id: sprint_id.to_string(),
},
DomainEvent::IssueSprintChanged {
issue_ref,
from,
to,
version,
} => Self::IssueSprintChanged {
issue_ref: issue_ref.to_string(),
from: from.map(|id| id.to_string()),
to: to.map(|id| id.to_string()),
version: version.get(),
},
DomainEvent::OrchestratorRequestProcessed {
requester_id,
action,
@ -981,4 +1063,22 @@ mod tests {
assert_eq!(json["agentId"], agent(11).to_string());
assert_eq!(json["resetsAtMs"], 1_700_000_000_000_i64);
}
#[test]
fn issue_sprint_changed_relays_to_dto_and_wire() {
let from = domain::SprintId::from_uuid(uuid::Uuid::from_u128(41));
let to = domain::SprintId::from_uuid(uuid::Uuid::from_u128(42));
let dto = DomainEventDto::from(&DomainEvent::IssueSprintChanged {
issue_ref: domain::IssueRef::from(domain::IssueNumber::new(7).unwrap()),
from: Some(from),
to: Some(to),
version: domain::IssueVersion::new(3).unwrap(),
});
let json = serde_json::to_value(&dto).expect("serialisable");
assert_eq!(json["type"], "issueSprintChanged");
assert_eq!(json["issueRef"], "#7");
assert_eq!(json["from"], from.to_string());
assert_eq!(json["to"], to.to_string());
assert_eq!(json["version"], 3);
}
}