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

@ -4,7 +4,7 @@ use std::str::FromStr;
use domain::{
AgentIssueRef, AgentIssueRole, Issue, IssueActor, IssueId, IssueListFilter,
IssueNumberAllocator, IssuePriority, IssueRef, IssueStatus, IssueStore, IssueStoreError,
MarkdownDoc, ProjectPath,
MarkdownDoc, ProjectPath, SprintId,
};
use infrastructure::{FsIssueNumberAllocator, FsIssueStore};
use uuid::Uuid;
@ -144,6 +144,44 @@ async fn issue_store_lists_by_index_filters() {
assert_eq!(rows[0].title, "Beta");
}
#[tokio::test]
async fn issue_store_persists_and_filters_sprint_membership() {
let tmp = TempDir::new();
let root = tmp.root();
let store = FsIssueStore::new();
let sprint_id = SprintId::from_uuid(Uuid::from_u128(42));
let assigned = issue(&root, 1, "Assigned")
.mutate(IssueActor::System, 2_000, |i| {
i.sprint = Some(sprint_id);
})
.unwrap();
store.create(&root, &assigned).await.unwrap();
store
.create(&root, &issue(&root, 2, "Backlog"))
.await
.unwrap();
let loaded = store
.get_by_ref(&root, IssueRef::from_str("#1").unwrap())
.await
.unwrap();
assert_eq!(loaded.sprint, Some(sprint_id));
let rows = store
.list(
&root,
IssueListFilter {
sprint: Some(sprint_id),
..IssueListFilter::default()
},
)
.await
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].title, "Assigned");
assert_eq!(rows[0].sprint, Some(sprint_id));
}
#[tokio::test]
async fn allocator_never_reuses_numbers() {
let tmp = TempDir::new();