ito_domain/backend.rs
1//! Backend coordination port definitions.
2//!
3//! Traits and DTOs for backend API operations: change leases (claim/release),
4//! allocation, and artifact synchronization. Implementations live in `ito-core`.
5
6use serde::{Deserialize, Serialize};
7
8use crate::changes::ChangeLifecycleFilter;
9use crate::errors::DomainResult;
10
11// ── Lease DTOs ──────────────────────────────────────────────────────
12
13/// Result of a successful change lease claim.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ClaimResult {
16 /// The change that was claimed.
17 pub change_id: String,
18 /// Identity of the lease holder.
19 pub holder: String,
20 /// Lease expiry as ISO-8601 timestamp, if available.
21 pub expires_at: Option<String>,
22}
23
24/// Result of a lease release operation.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ReleaseResult {
27 /// The change that was released.
28 pub change_id: String,
29}
30
31/// Result of an allocation operation.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AllocateResult {
34 /// The allocated change, if any work was available.
35 pub claim: Option<ClaimResult>,
36}
37
38/// Conflict detail when a lease claim fails because another holder owns it.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct LeaseConflict {
41 /// The change that is already claimed.
42 pub change_id: String,
43 /// Current holder identity.
44 pub holder: String,
45 /// Lease expiry as ISO-8601 timestamp, if available.
46 pub expires_at: Option<String>,
47}
48
49// ── Sync DTOs ───────────────────────────────────────────────────────
50
51/// An artifact bundle pulled from the backend for a single change.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ArtifactBundle {
54 /// The change this bundle belongs to.
55 pub change_id: String,
56 /// Proposal markdown content, if present.
57 pub proposal: Option<String>,
58 /// Design markdown content, if present.
59 pub design: Option<String>,
60 /// Tasks markdown content, if present.
61 pub tasks: Option<String>,
62 /// Spec delta files: `(capability_name, content)` pairs.
63 pub specs: Vec<(String, String)>,
64 /// Backend revision identifier for optimistic concurrency.
65 pub revision: String,
66}
67
68/// Result of a push operation.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct PushResult {
71 /// The change whose artifacts were pushed.
72 pub change_id: String,
73 /// New revision after the push.
74 pub new_revision: String,
75}
76
77/// Conflict detail when a push fails due to a stale revision.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct RevisionConflict {
80 /// The change with the conflict.
81 pub change_id: String,
82 /// The local revision that was sent.
83 pub local_revision: String,
84 /// The current server revision.
85 pub server_revision: String,
86}
87
88// ── Backend error ───────────────────────────────────────────────────
89
90/// Backend operation error category.
91///
92/// Adapters convert this into the appropriate layer error type.
93#[derive(Debug, Clone)]
94pub enum BackendError {
95 /// The requested lease is held by another client.
96 LeaseConflict(LeaseConflict),
97 /// The push revision is stale.
98 RevisionConflict(RevisionConflict),
99 /// The backend is not reachable or returned a server error.
100 Unavailable(String),
101 /// Authentication failed (invalid or missing token).
102 Unauthorized(String),
103 /// The requested resource was not found.
104 NotFound(String),
105 /// A catch-all for unexpected errors.
106 Other(String),
107}
108
109impl std::fmt::Display for BackendError {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 BackendError::LeaseConflict(c) => {
113 write!(
114 f,
115 "change '{}' is already claimed by '{}'",
116 c.change_id, c.holder
117 )
118 }
119 BackendError::RevisionConflict(c) => {
120 write!(
121 f,
122 "revision conflict for '{}': local={}, server={}",
123 c.change_id, c.local_revision, c.server_revision
124 )
125 }
126 BackendError::Unavailable(msg) => write!(f, "backend unavailable: {msg}"),
127 BackendError::Unauthorized(msg) => write!(f, "backend auth failed: {msg}"),
128 BackendError::NotFound(msg) => write!(f, "not found: {msg}"),
129 BackendError::Other(msg) => write!(f, "backend error: {msg}"),
130 }
131 }
132}
133
134impl std::error::Error for BackendError {}
135
136// ── Project store port ──────────────────────────────────────────────
137
138/// Port for resolving `{org}/{repo}` to project-level repositories.
139///
140/// The backend server uses this trait to obtain domain repository instances
141/// for a given project namespace. Implementations live in `ito-core` and
142/// may be backed by the filesystem or a database.
143///
144/// This trait is `Send + Sync` so it can be shared across async request
145/// handlers via `Arc`.
146pub trait BackendProjectStore: Send + Sync {
147 /// Obtain a change repository for the given project.
148 fn change_repository(
149 &self,
150 org: &str,
151 repo: &str,
152 ) -> DomainResult<Box<dyn crate::changes::ChangeRepository + Send>>;
153
154 /// Obtain a module repository for the given project.
155 fn module_repository(
156 &self,
157 org: &str,
158 repo: &str,
159 ) -> DomainResult<Box<dyn crate::modules::ModuleRepository + Send>>;
160
161 /// Obtain a task repository for the given project.
162 fn task_repository(
163 &self,
164 org: &str,
165 repo: &str,
166 ) -> DomainResult<Box<dyn crate::tasks::TaskRepository + Send>>;
167
168 /// Obtain a task mutation service for the given project.
169 fn task_mutation_service(
170 &self,
171 org: &str,
172 repo: &str,
173 ) -> DomainResult<Box<dyn crate::tasks::TaskMutationService + Send>>;
174
175 /// Obtain a promoted spec repository for the given project.
176 fn spec_repository(
177 &self,
178 org: &str,
179 repo: &str,
180 ) -> DomainResult<Box<dyn crate::specs::SpecRepository + Send>>;
181
182 /// Pull the latest artifact bundle for a change from backend-managed storage.
183 fn pull_artifact_bundle(
184 &self,
185 org: &str,
186 repo: &str,
187 change_id: &str,
188 ) -> Result<ArtifactBundle, BackendError>;
189
190 /// Push an updated artifact bundle into backend-managed storage.
191 fn push_artifact_bundle(
192 &self,
193 org: &str,
194 repo: &str,
195 change_id: &str,
196 bundle: &ArtifactBundle,
197 ) -> Result<PushResult, BackendError>;
198
199 /// Archive a change in backend-managed storage and mirror promoted specs.
200 fn archive_change(
201 &self,
202 org: &str,
203 repo: &str,
204 change_id: &str,
205 ) -> Result<ArchiveResult, BackendError>;
206
207 /// Ensure the project directory/storage structure exists.
208 ///
209 /// Called before first write to a project. Implementations should
210 /// create whatever backing store structure is needed.
211 fn ensure_project(&self, org: &str, repo: &str) -> DomainResult<()>;
212
213 /// Check whether the project exists in the store.
214 fn project_exists(&self, org: &str, repo: &str) -> bool;
215}
216
217// ── Port traits ─────────────────────────────────────────────────────
218
219/// Port for backend lease operations (claim, release, allocate).
220///
221/// Implementations handle HTTP communication and token management.
222/// The domain layer uses this trait to remain decoupled from transport.
223pub trait BackendLeaseClient {
224 /// Claim a lease on a change.
225 fn claim(&self, change_id: &str) -> Result<ClaimResult, BackendError>;
226
227 /// Release a held lease.
228 fn release(&self, change_id: &str) -> Result<ReleaseResult, BackendError>;
229
230 /// Request the backend to allocate the next available change.
231 fn allocate(&self) -> Result<AllocateResult, BackendError>;
232}
233
234/// Port for backend artifact synchronization operations.
235///
236/// Pull retrieves the latest artifact bundle for a change. Push sends
237/// local updates using optimistic concurrency (revision checks).
238pub trait BackendSyncClient {
239 /// Pull the latest artifact bundle for a change from the backend.
240 fn pull(&self, change_id: &str) -> Result<ArtifactBundle, BackendError>;
241
242 /// Push local artifact updates to the backend with a revision check.
243 fn push(&self, change_id: &str, bundle: &ArtifactBundle) -> Result<PushResult, BackendError>;
244}
245
246/// Port for backend-backed change listing (read path).
247///
248/// Used by repository adapters to resolve change data from the backend
249/// instead of the filesystem when backend mode is enabled.
250pub trait BackendChangeReader {
251 /// List all change summaries from the backend.
252 fn list_changes(
253 &self,
254 filter: ChangeLifecycleFilter,
255 ) -> DomainResult<Vec<crate::changes::ChangeSummary>>;
256
257 /// Get a full change from the backend.
258 fn get_change(
259 &self,
260 change_id: &str,
261 filter: ChangeLifecycleFilter,
262 ) -> DomainResult<crate::changes::Change>;
263}
264
265/// Port for backend-backed module listing.
266///
267/// Used by repository adapters to resolve module data from the backend when
268/// backend mode is enabled.
269pub trait BackendModuleReader {
270 /// List all module summaries from the backend.
271 fn list_modules(&self) -> DomainResult<Vec<crate::modules::ModuleSummary>>;
272
273 /// Get a full module from the backend.
274 fn get_module(&self, module_id: &str) -> DomainResult<crate::modules::Module>;
275}
276
277/// Port for backend-backed task reading.
278///
279/// Used by repository adapters to resolve task data from the backend
280/// when backend mode is enabled.
281pub trait BackendTaskReader {
282 /// Load tasks content (raw markdown) from the backend for a change.
283 fn load_tasks_content(&self, change_id: &str) -> DomainResult<Option<String>>;
284}
285
286/// Port for backend-backed promoted spec reading.
287pub trait BackendSpecReader {
288 /// List all promoted specs from the backend.
289 fn list_specs(&self) -> DomainResult<Vec<crate::specs::SpecSummary>>;
290
291 /// Get a promoted spec from the backend.
292 fn get_spec(&self, spec_id: &str) -> DomainResult<crate::specs::SpecDocument>;
293}
294
295// ── Event ingest DTOs ──────────────────────────────────────────────
296
297/// A batch of audit events to send to the backend.
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct EventBatch {
300 /// Events in this batch, serialized as JSON objects.
301 pub events: Vec<crate::audit::event::AuditEvent>,
302 /// Client-generated idempotency key for safe retries.
303 pub idempotency_key: String,
304}
305
306/// Result of a successful event ingest operation.
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct EventIngestResult {
309 /// Number of events accepted by the backend.
310 pub accepted: usize,
311 /// Number of events that were duplicates (already ingested).
312 pub duplicates: usize,
313}
314
315/// Port for backend event ingestion.
316///
317/// Implementations handle HTTP communication to submit local audit events
318/// to the backend for centralized observability.
319pub trait BackendEventIngestClient {
320 /// Submit a batch of audit events to the backend.
321 ///
322 /// The batch includes an idempotency key so retries do not produce
323 /// duplicate events on the server.
324 fn ingest(&self, batch: &EventBatch) -> Result<EventIngestResult, BackendError>;
325}
326
327// ── Archive DTOs ───────────────────────────────────────────────────
328
329/// Result of marking a change as archived on the backend.
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct ArchiveResult {
332 /// The change that was archived.
333 pub change_id: String,
334 /// Timestamp when the backend recorded the archive (ISO-8601).
335 pub archived_at: String,
336}
337
338/// Port for backend archive lifecycle operations.
339///
340/// Marks a change as archived on the backend, making it immutable
341/// for subsequent backend operations (no further writes or leases).
342pub trait BackendArchiveClient {
343 /// Mark a change as archived on the backend.
344 ///
345 /// After this call succeeds, the backend SHALL reject further
346 /// write or lease operations for the change.
347 fn mark_archived(&self, change_id: &str) -> Result<ArchiveResult, BackendError>;
348}
349
350#[cfg(test)]
351#[path = "backend_tests.rs"]
352mod backend_tests;