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:
@ -30,6 +30,9 @@ use thiserror::Error;
|
||||
use crate::agent::AgentManifest;
|
||||
use crate::events::DomainEvent;
|
||||
use crate::ids::{AgentId, NodeId, ScheduleId, SessionId};
|
||||
use crate::issue::{
|
||||
Issue, IssueCarnet, IssueIndexEntry, IssueListFilter, IssueNumber, IssueRef, IssueVersion,
|
||||
};
|
||||
use crate::markdown::MarkdownDoc;
|
||||
use crate::memory::{Memory, MemoryIndexEntry, MemoryLink, MemorySlug};
|
||||
use crate::permission::ProjectPermissions;
|
||||
@ -461,6 +464,28 @@ pub enum GitError {
|
||||
Operation(String),
|
||||
}
|
||||
|
||||
/// Errors from the issue store.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum IssueStoreError {
|
||||
/// The requested issue does not exist.
|
||||
#[error("issue not found")]
|
||||
NotFound,
|
||||
/// Optimistic concurrency conflict.
|
||||
#[error("issue version conflict: expected {expected}, actual {actual}")]
|
||||
VersionConflict {
|
||||
/// Expected version.
|
||||
expected: IssueVersion,
|
||||
/// Actual persisted version.
|
||||
actual: IssueVersion,
|
||||
},
|
||||
/// The issue payload is invalid.
|
||||
#[error("issue invalid: {0}")]
|
||||
Invalid(String),
|
||||
/// Store I/O or serialization failed.
|
||||
#[error("issue store failed: {0}")]
|
||||
Store(String),
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Git port support types
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -1191,6 +1216,84 @@ pub trait PermissionStore: Send + Sync {
|
||||
) -> Result<(), StoreError>;
|
||||
}
|
||||
|
||||
/// Allocates monotonic per-project issue numbers.
|
||||
#[async_trait]
|
||||
pub trait IssueNumberAllocator: Send + Sync {
|
||||
/// Allocates the next number for `root`.
|
||||
///
|
||||
/// Implementations must never reuse an allocated number, even if a later
|
||||
/// issue creation step fails.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`IssueStoreError`] on persistence/lock failure.
|
||||
async fn allocate_next(&self, root: &ProjectPath) -> Result<IssueNumber, IssueStoreError>;
|
||||
}
|
||||
|
||||
/// Persistence port for project-scoped issues.
|
||||
#[async_trait]
|
||||
pub trait IssueStore: Send + Sync {
|
||||
/// Creates a new issue.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`IssueStoreError`] when the issue already exists or cannot be stored.
|
||||
async fn create(&self, root: &ProjectPath, issue: &Issue) -> Result<(), IssueStoreError>;
|
||||
|
||||
/// Reads an issue by `#N` reference.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`IssueStoreError::NotFound`] when absent.
|
||||
async fn get_by_ref(
|
||||
&self,
|
||||
root: &ProjectPath,
|
||||
issue_ref: IssueRef,
|
||||
) -> Result<Issue, IssueStoreError>;
|
||||
|
||||
/// Lists issue index entries matching `filter`.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`IssueStoreError`] on persistence failure.
|
||||
async fn list(
|
||||
&self,
|
||||
root: &ProjectPath,
|
||||
filter: IssueListFilter,
|
||||
) -> Result<Vec<IssueIndexEntry>, IssueStoreError>;
|
||||
|
||||
/// Replaces an issue after checking its expected version.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`IssueStoreError::VersionConflict`] on optimistic-concurrency conflict.
|
||||
async fn update(
|
||||
&self,
|
||||
root: &ProjectPath,
|
||||
issue: &Issue,
|
||||
expected_version: IssueVersion,
|
||||
) -> Result<(), IssueStoreError>;
|
||||
|
||||
/// Reads only the issue carnet projection.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`IssueStoreError::NotFound`] when absent.
|
||||
async fn read_carnet(
|
||||
&self,
|
||||
root: &ProjectPath,
|
||||
issue_ref: IssueRef,
|
||||
) -> Result<IssueCarnet, IssueStoreError>;
|
||||
|
||||
/// Replaces an issue carnet and increments the issue version.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`IssueStoreError::VersionConflict`] on optimistic-concurrency conflict.
|
||||
async fn write_carnet(
|
||||
&self,
|
||||
root: &ProjectPath,
|
||||
issue_ref: IssueRef,
|
||||
carnet: MarkdownDoc,
|
||||
actor: crate::issue::IssueActor,
|
||||
now_ms: u64,
|
||||
expected_version: IssueVersion,
|
||||
) -> Result<IssueCarnet, IssueStoreError>;
|
||||
}
|
||||
|
||||
/// Git operations for a project. Named `GitPort` to avoid clashing with the
|
||||
/// [`crate::git::GitRepository`] *entity* (state image).
|
||||
#[async_trait]
|
||||
|
||||
Reference in New Issue
Block a user