1use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use ito_config::ito_dir::{absolutize_and_normalize, lexical_normalize};
11use ito_config::types::{ItoConfig, RepositoryPersistenceMode};
12use ito_config::{ConfigContext, load_cascading_project_config};
13
14#[cfg(feature = "backend")]
15use crate::artifact_mutations::RemoteChangeArtifactBundleClient;
16use crate::artifact_mutations::{
17 BundleBackedChangeArtifactMutationService, FsChangeArtifactMutationService,
18 SqliteChangeArtifactBundleClient,
19};
20#[cfg(feature = "backend")]
21use crate::backend_change_repository::BackendChangeRepository;
22#[cfg(feature = "backend")]
23use crate::backend_client::{BackendRuntime, resolve_backend_runtime};
24#[cfg(feature = "backend")]
25use crate::backend_http::BackendHttpClient;
26#[cfg(feature = "backend")]
27use crate::backend_module_repository::BackendModuleRepository;
28#[cfg(feature = "backend")]
29use crate::backend_spec_repository::BackendSpecRepository;
30#[cfg(not(feature = "backend"))]
31use crate::capabilities::CompiledFeature;
32use crate::capabilities::{CapabilityPreflight, preflight_config};
33use crate::change_repository::FsChangeRepository;
34use crate::errors::{CoreError, CoreResult};
35use crate::module_repository::FsModuleRepository;
36#[cfg(feature = "backend")]
37use crate::remote_task_repository::RemoteTaskRepository;
38use crate::spec_repository::FsSpecRepository;
39use crate::sqlite_project_store::SqliteBackendProjectStore;
40use crate::task_mutations::FsTaskMutationService;
41#[cfg(feature = "backend")]
42use crate::task_mutations::boxed_fs_task_mutation_service;
43use crate::task_repository::FsTaskRepository;
44use ito_domain::changes::ChangeArtifactMutationService;
45use ito_domain::changes::ChangeRepository;
46use ito_domain::modules::ModuleRepository;
47use ito_domain::specs::SpecRepository;
48use ito_domain::tasks::{TaskMutationService, TaskRepository};
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum PersistenceMode {
53 Filesystem,
55 Sqlite,
57 Remote,
59}
60
61#[derive(Clone)]
63pub struct RepositorySet {
64 pub changes: Arc<dyn ChangeRepository + Send + Sync>,
66 pub modules: Arc<dyn ModuleRepository + Send + Sync>,
68 pub tasks: Arc<dyn TaskRepository + Send + Sync>,
70 pub task_mutations: Arc<dyn TaskMutationService + Send + Sync>,
72 pub specs: Arc<dyn SpecRepository + Send + Sync>,
74}
75
76#[derive(Debug, Clone)]
78pub struct SqliteRuntime {
79 pub db_path: PathBuf,
81 pub org: String,
83 pub repo: String,
85}
86
87pub struct RepositoryRuntime {
89 mode: PersistenceMode,
90 ito_path: PathBuf,
91 #[cfg(feature = "backend")]
92 backend_runtime: Option<BackendRuntime>,
93 sqlite_runtime: Option<SqliteRuntime>,
94 repositories: RepositorySet,
95 change_artifact_mutations: Arc<dyn ChangeArtifactMutationService + Send + Sync>,
96}
97
98impl RepositoryRuntime {
99 pub fn mode(&self) -> PersistenceMode {
101 self.mode
102 }
103
104 pub fn ito_path(&self) -> &Path {
106 self.ito_path.as_path()
107 }
108
109 #[cfg(feature = "backend")]
111 pub fn backend_runtime(&self) -> Option<&BackendRuntime> {
112 self.backend_runtime.as_ref()
113 }
114
115 pub fn sqlite_runtime(&self) -> Option<&SqliteRuntime> {
117 self.sqlite_runtime.as_ref()
118 }
119
120 pub fn repositories(&self) -> &RepositorySet {
122 &self.repositories
123 }
124
125 pub fn change_artifact_mutations(&self) -> &(dyn ChangeArtifactMutationService + Send + Sync) {
127 self.change_artifact_mutations.as_ref()
128 }
129}
130
131#[cfg(feature = "backend")]
133pub trait RemoteRepositoryFactory: Send + Sync {
134 fn build(&self, runtime: &BackendRuntime) -> CoreResult<RepositorySet>;
136}
137
138#[cfg(feature = "backend")]
140pub struct HttpRemoteRepositoryFactory;
141
142#[cfg(feature = "backend")]
143impl RemoteRepositoryFactory for HttpRemoteRepositoryFactory {
144 fn build(&self, runtime: &BackendRuntime) -> CoreResult<RepositorySet> {
145 let client = BackendHttpClient::new(runtime.clone());
146 Ok(RepositorySet {
147 changes: Arc::new(BackendChangeRepository::new(client.clone())),
148 modules: Arc::new(BackendModuleRepository::new(client.clone())),
149 tasks: Arc::new(RemoteTaskRepository::new(client.clone())),
150 task_mutations: Arc::new(client.clone()),
151 specs: Arc::new(BackendSpecRepository::new(client.clone())),
152 })
153 }
154}
155
156pub struct RepositoryRuntimeBuilder {
158 ito_path: PathBuf,
159 mode: PersistenceMode,
160 #[cfg(feature = "backend")]
161 backend_runtime: Option<BackendRuntime>,
162 sqlite_runtime: Option<SqliteRuntime>,
163 #[cfg(feature = "backend")]
164 remote_factory: Arc<dyn RemoteRepositoryFactory>,
165}
166
167impl RepositoryRuntimeBuilder {
168 pub fn new(ito_path: impl Into<PathBuf>) -> Self {
170 Self {
171 ito_path: ito_path.into(),
172 mode: PersistenceMode::Filesystem,
173 #[cfg(feature = "backend")]
174 backend_runtime: None,
175 sqlite_runtime: None,
176 #[cfg(feature = "backend")]
177 remote_factory: Arc::new(HttpRemoteRepositoryFactory),
178 }
179 }
180
181 pub fn mode(mut self, mode: PersistenceMode) -> Self {
183 self.mode = mode;
184 self
185 }
186
187 #[cfg(feature = "backend")]
189 pub fn backend_runtime(mut self, runtime: BackendRuntime) -> Self {
190 self.backend_runtime = Some(runtime);
191 self
192 }
193
194 pub fn sqlite_runtime(mut self, runtime: SqliteRuntime) -> Self {
196 self.sqlite_runtime = Some(runtime);
197 self
198 }
199
200 #[cfg(feature = "backend")]
202 pub fn remote_factory(mut self, factory: Arc<dyn RemoteRepositoryFactory>) -> Self {
203 self.remote_factory = factory;
204 self
205 }
206
207 pub fn build(self) -> CoreResult<RepositoryRuntime> {
209 match self.mode {
210 PersistenceMode::Filesystem => {
211 let ito_path = self.ito_path;
212 let repositories = filesystem_repository_set(&ito_path);
213 Ok(RepositoryRuntime {
214 mode: PersistenceMode::Filesystem,
215 ito_path: ito_path.clone(),
216 #[cfg(feature = "backend")]
217 backend_runtime: None,
218 sqlite_runtime: None,
219 repositories,
220 change_artifact_mutations: Arc::new(FsChangeArtifactMutationService::new(
221 ito_path,
222 )),
223 })
224 }
225 PersistenceMode::Sqlite => {
226 let ito_path = self.ito_path;
227 let runtime = self.sqlite_runtime.ok_or_else(|| {
228 CoreError::validation("sqlite mode requires sqlite runtime".to_string())
229 })?;
230 let repositories = sqlite_repository_set(&runtime)?;
231 let artifact_mutations = Arc::new(BundleBackedChangeArtifactMutationService::new(
232 SqliteChangeArtifactBundleClient::new(&runtime),
233 ));
234 Ok(RepositoryRuntime {
235 mode: PersistenceMode::Sqlite,
236 ito_path,
237 #[cfg(feature = "backend")]
238 backend_runtime: None,
239 sqlite_runtime: Some(runtime),
240 repositories,
241 change_artifact_mutations: artifact_mutations,
242 })
243 }
244 #[cfg(feature = "backend")]
245 PersistenceMode::Remote => {
246 let ito_path = self.ito_path;
247 let runtime = self.backend_runtime.ok_or_else(|| {
248 CoreError::validation("remote mode requires backend runtime".to_string())
249 })?;
250 let repositories = self.remote_factory.build(&runtime)?;
251 let artifact_mutations = Arc::new(BundleBackedChangeArtifactMutationService::new(
252 RemoteChangeArtifactBundleClient::new(BackendHttpClient::new(runtime.clone())),
253 ));
254 Ok(RepositoryRuntime {
255 mode: PersistenceMode::Remote,
256 ito_path,
257 backend_runtime: Some(runtime),
258 sqlite_runtime: None,
259 repositories,
260 change_artifact_mutations: artifact_mutations,
261 })
262 }
263 #[cfg(not(feature = "backend"))]
264 PersistenceMode::Remote => Err(CoreError::feature_unavailable(
265 CompiledFeature::Backend,
266 "repository.mode=remote",
267 "use filesystem or sqlite persistence, or install an experimental build with the backend feature",
268 )),
269 }
270 }
271}
272
273pub fn resolve_repository_runtime(
275 ito_path: &Path,
276 ctx: &ConfigContext,
277) -> CoreResult<RepositoryRuntime> {
278 let project_root = ctx
279 .project_dir
280 .as_deref()
281 .unwrap_or_else(|| ito_path.parent().unwrap_or(ito_path));
282 let merged = load_cascading_project_config(project_root, ito_path, ctx).merged;
283 let raw_mode = merged
284 .pointer("/repository/mode")
285 .and_then(|v| v.as_str())
286 .unwrap_or("filesystem");
287
288 if RepositoryPersistenceMode::parse_value(raw_mode).is_none() {
292 let valid = RepositoryPersistenceMode::ALL.join(", ");
293 return Err(CoreError::validation(format!(
294 "Invalid repository.mode '{raw_mode}': must be one of {valid}"
295 )));
296 }
297
298 let config = serde_json::from_value::<ItoConfig>(merged)
299 .map_err(|err| CoreError::serde("parse resolved Ito config", err.to_string()))?;
300
301 preflight_config(&config, CapabilityPreflight::Stateful)?;
302
303 if !config.backend.enabled {
304 return match config.repository.mode {
305 RepositoryPersistenceMode::Filesystem => {
306 RepositoryRuntimeBuilder::new(ito_path).build()
307 }
308 RepositoryPersistenceMode::Sqlite => {
309 let runtime = resolve_sqlite_runtime(&config, project_root)?;
310 RepositoryRuntimeBuilder::new(ito_path)
311 .mode(PersistenceMode::Sqlite)
312 .sqlite_runtime(runtime)
313 .build()
314 }
315 };
316 }
317
318 #[cfg(feature = "backend")]
319 {
320 let runtime = resolve_backend_runtime(&config.backend)?.ok_or_else(|| {
321 CoreError::validation(
322 "Backend mode is enabled but runtime was not resolved".to_string(),
323 )
324 })?;
325
326 RepositoryRuntimeBuilder::new(ito_path)
327 .mode(PersistenceMode::Remote)
328 .backend_runtime(runtime)
329 .build()
330 }
331
332 #[cfg(not(feature = "backend"))]
333 Err(CoreError::feature_unavailable(
334 CompiledFeature::Backend,
335 "backend.enabled",
336 "disable backend.enabled, or install an experimental build with the backend feature",
337 ))
338}
339
340fn resolve_sqlite_runtime(config: &ItoConfig, project_root: &Path) -> CoreResult<SqliteRuntime> {
341 let Some(db_path) = config.repository.sqlite.db_path.as_deref() else {
342 return Err(CoreError::validation(
343 "SQLite persistence mode requires 'repository.sqlite.dbPath' to be set",
344 ));
345 };
346 let db_path = db_path.trim();
347 if db_path.is_empty() {
348 return Err(CoreError::validation(
349 "SQLite persistence mode requires 'repository.sqlite.dbPath' to be set",
350 ));
351 }
352
353 let db_path = PathBuf::from(db_path);
354 let db_path = if db_path.is_absolute() {
355 db_path
356 } else {
357 project_root.join(db_path)
358 };
359 let db_path =
360 absolutize_and_normalize(&db_path).unwrap_or_else(|_| lexical_normalize(&db_path));
361
362 let repo = match project_root.file_name().and_then(|s| s.to_str()) {
363 Some(name) if !name.trim().is_empty() => name.to_string(),
364 _ => "project".to_string(),
365 };
366
367 Ok(SqliteRuntime {
368 db_path,
369 org: "local".to_string(),
370 repo,
371 })
372}
373
374fn filesystem_repository_set(ito_path: &Path) -> RepositorySet {
375 let ito_path = ito_path.to_path_buf();
376 RepositorySet {
377 changes: Arc::new(OwnedFsChangeRepository::new(ito_path.clone())),
378 modules: Arc::new(OwnedFsModuleRepository::new(ito_path.clone())),
379 tasks: Arc::new(OwnedFsTaskRepository::new(ito_path.clone())),
380 task_mutations: Arc::new(FsTaskMutationService::new(ito_path.clone())),
381 specs: Arc::new(OwnedFsSpecRepository::new(ito_path)),
382 }
383}
384
385fn sqlite_repository_set(runtime: &SqliteRuntime) -> CoreResult<RepositorySet> {
386 let store = SqliteBackendProjectStore::open(&runtime.db_path)?;
387 store.repository_set(&runtime.org, &runtime.repo)
388}
389
390#[cfg(feature = "backend")]
391pub(crate) fn boxed_fs_change_repository(ito_path: PathBuf) -> Box<dyn ChangeRepository + Send> {
392 Box::new(OwnedFsChangeRepository::new(ito_path))
393}
394
395#[cfg(feature = "backend")]
396pub(crate) fn boxed_fs_module_repository(ito_path: PathBuf) -> Box<dyn ModuleRepository + Send> {
397 Box::new(OwnedFsModuleRepository::new(ito_path))
398}
399
400#[cfg(feature = "backend")]
401pub(crate) fn boxed_fs_task_repository(ito_path: PathBuf) -> Box<dyn TaskRepository + Send> {
402 Box::new(OwnedFsTaskRepository::new(ito_path))
403}
404
405#[cfg(feature = "backend")]
406pub(crate) fn boxed_fs_task_mutation_port(
407 ito_path: PathBuf,
408) -> Box<dyn TaskMutationService + Send> {
409 boxed_fs_task_mutation_service(ito_path)
410}
411
412#[cfg(feature = "backend")]
413pub(crate) fn boxed_fs_spec_repository(ito_path: PathBuf) -> Box<dyn SpecRepository + Send> {
414 Box::new(OwnedFsSpecRepository::new(ito_path))
415}
416
417#[derive(Debug, Clone)]
420struct OwnedFsChangeRepository {
421 ito_path: PathBuf,
422}
423
424impl OwnedFsChangeRepository {
425 fn new(ito_path: PathBuf) -> Self {
426 Self { ito_path }
427 }
428
429 fn inner(&self) -> FsChangeRepository<'_> {
430 FsChangeRepository::new(&self.ito_path)
431 }
432}
433
434impl ChangeRepository for OwnedFsChangeRepository {
435 fn resolve_target_with_options(
436 &self,
437 input: &str,
438 options: ito_domain::changes::ResolveTargetOptions,
439 ) -> ito_domain::changes::ChangeTargetResolution {
440 self.inner().resolve_target_with_options(input, options)
441 }
442
443 fn suggest_targets(&self, input: &str, max: usize) -> Vec<String> {
444 self.inner().suggest_targets(input, max)
445 }
446
447 fn exists(&self, id: &str) -> bool {
448 self.inner().exists(id)
449 }
450
451 fn exists_with_filter(
452 &self,
453 id: &str,
454 filter: ito_domain::changes::ChangeLifecycleFilter,
455 ) -> bool {
456 self.inner().exists_with_filter(id, filter)
457 }
458
459 fn get_with_filter(
460 &self,
461 id: &str,
462 filter: ito_domain::changes::ChangeLifecycleFilter,
463 ) -> ito_domain::errors::DomainResult<ito_domain::changes::Change> {
464 self.inner().get_with_filter(id, filter)
465 }
466
467 fn list_with_filter(
468 &self,
469 filter: ito_domain::changes::ChangeLifecycleFilter,
470 ) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
471 self.inner().list_with_filter(filter)
472 }
473
474 fn list_by_module_with_filter(
475 &self,
476 module_id: &str,
477 filter: ito_domain::changes::ChangeLifecycleFilter,
478 ) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
479 self.inner().list_by_module_with_filter(module_id, filter)
480 }
481
482 fn list_incomplete_with_filter(
483 &self,
484 filter: ito_domain::changes::ChangeLifecycleFilter,
485 ) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
486 self.inner().list_incomplete_with_filter(filter)
487 }
488
489 fn list_complete_with_filter(
490 &self,
491 filter: ito_domain::changes::ChangeLifecycleFilter,
492 ) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
493 self.inner().list_complete_with_filter(filter)
494 }
495
496 fn get_summary_with_filter(
497 &self,
498 id: &str,
499 filter: ito_domain::changes::ChangeLifecycleFilter,
500 ) -> ito_domain::errors::DomainResult<ito_domain::changes::ChangeSummary> {
501 self.inner().get_summary_with_filter(id, filter)
502 }
503}
504
505#[derive(Debug, Clone)]
506struct OwnedFsModuleRepository {
507 ito_path: PathBuf,
508}
509
510impl OwnedFsModuleRepository {
511 fn new(ito_path: PathBuf) -> Self {
512 Self { ito_path }
513 }
514
515 fn inner(&self) -> FsModuleRepository<'_> {
516 FsModuleRepository::new(&self.ito_path)
517 }
518}
519
520impl ModuleRepository for OwnedFsModuleRepository {
521 fn exists(&self, id: &str) -> bool {
522 self.inner().exists(id)
523 }
524
525 fn get(
526 &self,
527 id_or_name: &str,
528 ) -> ito_domain::errors::DomainResult<ito_domain::modules::Module> {
529 self.inner().get(id_or_name)
530 }
531
532 fn list(&self) -> ito_domain::errors::DomainResult<Vec<ito_domain::modules::ModuleSummary>> {
533 self.inner().list()
534 }
535
536 fn list_sub_modules(
537 &self,
538 parent_id: &str,
539 ) -> ito_domain::errors::DomainResult<Vec<ito_domain::modules::SubModuleSummary>> {
540 self.inner().list_sub_modules(parent_id)
541 }
542
543 fn get_sub_module(
544 &self,
545 composite_id: &str,
546 ) -> ito_domain::errors::DomainResult<ito_domain::modules::SubModule> {
547 self.inner().get_sub_module(composite_id)
548 }
549}
550
551#[derive(Debug, Clone)]
552struct OwnedFsTaskRepository {
553 ito_path: PathBuf,
554}
555
556impl OwnedFsTaskRepository {
557 fn new(ito_path: PathBuf) -> Self {
558 Self { ito_path }
559 }
560
561 fn inner(&self) -> FsTaskRepository<'_> {
562 FsTaskRepository::new(&self.ito_path)
563 }
564}
565
566impl TaskRepository for OwnedFsTaskRepository {
567 fn load_tasks(
568 &self,
569 change_id: &str,
570 ) -> ito_domain::errors::DomainResult<ito_domain::tasks::TasksParseResult> {
571 self.inner().load_tasks(change_id)
572 }
573}
574
575#[derive(Debug, Clone)]
576struct OwnedFsSpecRepository {
577 ito_path: PathBuf,
578}
579
580impl OwnedFsSpecRepository {
581 fn new(ito_path: PathBuf) -> Self {
582 Self { ito_path }
583 }
584
585 fn inner(&self) -> FsSpecRepository<'_> {
586 FsSpecRepository::new(&self.ito_path)
587 }
588}
589
590impl SpecRepository for OwnedFsSpecRepository {
591 fn list(&self) -> ito_domain::errors::DomainResult<Vec<ito_domain::specs::SpecSummary>> {
592 self.inner().list()
593 }
594
595 fn get(&self, id: &str) -> ito_domain::errors::DomainResult<ito_domain::specs::SpecDocument> {
596 self.inner().get(id)
597 }
598}