From e653f8f08b7a89834e69a3854a0f11e514d77d69 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 26 Jul 2026 12:20:18 +0900 Subject: [PATCH] workspace: introduce authority abstraction --- crates/workspace-server/src/authority.rs | 341 +++++++++++++++++++++++ crates/workspace-server/src/lib.rs | 9 +- crates/workspace-server/src/records.rs | 290 +------------------ crates/workspace-server/src/server.rs | 20 +- 4 files changed, 359 insertions(+), 301 deletions(-) create mode 100644 crates/workspace-server/src/authority.rs diff --git a/crates/workspace-server/src/authority.rs b/crates/workspace-server/src/authority.rs new file mode 100644 index 00000000..bdd7a6dc --- /dev/null +++ b/crates/workspace-server/src/authority.rs @@ -0,0 +1,341 @@ +use std::path::PathBuf; + +use ticket::{ + SqliteTicketBackend, TicketBackend, TicketIdOrSlug, TicketListQuery, + TicketWorkspaceActionPriority, project_ticket_workspace_item, +}; + +use crate::records::{ + ObjectiveDetail, ObjectiveResourceSummary, ObjectiveSummary, ProjectRecordList, TicketDetail, + TicketSummary, summarize_body, truncate_body, validate_project_id, +}; +use crate::store::{ControlPlaneStore, SqliteWorkspaceStore}; +use crate::{Error, Result}; + +const DETAIL_BODY_LIMIT: usize = 64 * 1024; + +/// Workspace-scoped runtime authority for project resources. +/// +/// Normal Backend API handlers should depend on this authority abstraction, not +/// on legacy filesystem layouts. Filesystem readers belong in explicit import +/// / migration helpers such as `objective_import`. +pub trait WorkspaceAuthority: ObjectiveAuthority + TicketAuthority + MemoryAuthorityMarker {} + +impl WorkspaceAuthority for T where + T: ObjectiveAuthority + TicketAuthority + MemoryAuthorityMarker +{ +} + +pub trait TicketAuthority { + fn list_tickets(&self, limit: usize) -> Result>; + fn ticket(&self, id: &str) -> Result; +} + +pub trait ObjectiveAuthority { + fn list_objectives(&self, limit: usize) -> Result>; + fn objective(&self, id: &str) -> Result; +} + +/// Marker for the pending Memory authority split. +/// +/// Memory still routes through the legacy `memory` crate backend in this codebase. +/// This marker makes the missing SQL-backed Memory authority explicit so callers +/// do not mistake the current Backend API boundary for a completed storage +/// authority migration. +pub trait MemoryAuthorityMarker { + fn memory_authority_status(&self) -> MemoryAuthorityStatus; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemoryAuthorityStatus { + LegacyFilesystemBackendPendingSqliteAuthority, +} + +#[derive(Clone)] +pub struct SqliteWorkspaceAuthority { + workspace_id: String, + store: SqliteWorkspaceStore, + ticket_backend: SqliteTicketBackend, +} + +impl SqliteWorkspaceAuthority { + pub fn new(database_path: impl Into, workspace_id: impl Into) -> Result { + let database_path = database_path.into(); + let workspace_id = workspace_id.into(); + Ok(Self { + workspace_id: workspace_id.clone(), + store: SqliteWorkspaceStore::open(&database_path)?, + ticket_backend: SqliteTicketBackend::new(database_path, workspace_id), + }) + } +} + +impl TicketAuthority for SqliteWorkspaceAuthority { + fn list_tickets(&self, limit: usize) -> Result> { + let mut items = Vec::new(); + for item in self.ticket_backend.list(TicketListQuery::all())? { + let ticket = self + .ticket_backend + .show(TicketIdOrSlug::Id(item.id.clone()))?; + let projection = project_ticket_workspace_item(&item, &ticket.relations.blockers, None); + items.push(TicketSummary { + id: item.id, + title: item.title, + state: item.workflow_state.as_str().to_string(), + priority: item.priority, + updated_at: item.updated_at, + queued_by: item.queued_by, + queued_at: item.queued_at, + workspace_action_priority: workspace_action_priority_name(projection.priority) + .to_string(), + record_source: "sqlite_yoi_ticket".to_string(), + }); + } + items.sort_by(|a, b| { + b.updated_at + .cmp(&a.updated_at) + .then_with(|| a.id.cmp(&b.id)) + }); + items.truncate(limit); + Ok(ProjectRecordList { + items, + invalid_records: Vec::new(), + record_authority: "workspace-sqlite".to_string(), + }) + } + + fn ticket(&self, id: &str) -> Result { + validate_project_id(id)?; + let ticket = self + .ticket_backend + .show(TicketIdOrSlug::Id(id.to_string()))?; + let (body, body_truncated) = + truncate_body(ticket.document.body.as_str(), DETAIL_BODY_LIMIT); + Ok(TicketDetail { + id: ticket.meta.id, + title: ticket.meta.title, + state: ticket.meta.workflow_state.as_str().to_string(), + priority: ticket.meta.priority, + created_at: ticket.meta.created_at, + updated_at: ticket.meta.updated_at, + queued_by: ticket.meta.queued_by, + queued_at: ticket.meta.queued_at, + risk_flags: ticket.meta.risk_flags, + body, + body_truncated, + event_count: ticket.events.len(), + artifact_count: ticket.artifacts.len(), + record_source: "sqlite_yoi_ticket".to_string(), + }) + } +} + +impl ObjectiveAuthority for SqliteWorkspaceAuthority { + fn list_objectives(&self, limit: usize) -> Result> { + let mut items = Vec::new(); + for record in self.store.list_objectives(&self.workspace_id, limit)? { + let linked_tickets = self + .store + .list_objective_ticket_links(&self.workspace_id, &record.objective_id)? + .into_iter() + .map(|link| link.ticket_id) + .collect::>(); + items.push(ObjectiveSummary { + id: record.objective_id, + title: record.title, + state: record.state, + updated_at: Some(record.updated_at), + summary: summarize_body(&record.body_md), + linked_tickets, + record_source: "workspace-sqlite".to_string(), + }); + } + Ok(ProjectRecordList { + items, + invalid_records: Vec::new(), + record_authority: "workspace-sqlite".to_string(), + }) + } + + fn objective(&self, id: &str) -> Result { + validate_project_id(id)?; + let record = self + .store + .get_objective(&self.workspace_id, id)? + .ok_or_else(|| Error::Store(format!("unknown objective `{id}`")))?; + let linked_tickets = self + .store + .list_objective_ticket_links(&self.workspace_id, &record.objective_id)? + .into_iter() + .map(|link| link.ticket_id) + .collect::>(); + let resources = self + .store + .list_objective_resources(&self.workspace_id, &record.objective_id)? + .into_iter() + .map(|resource| ObjectiveResourceSummary { + path: resource.resource_path, + media_type: resource.media_type, + bytes: resource.body.len(), + updated_at: resource.updated_at, + }) + .collect(); + let (body, body_truncated) = truncate_body(&record.body_md, DETAIL_BODY_LIMIT); + Ok(ObjectiveDetail { + id: record.objective_id, + title: record.title, + state: record.state, + created_at: Some(record.created_at), + updated_at: Some(record.updated_at), + linked_tickets, + resources, + body, + body_truncated, + record_source: "workspace-sqlite".to_string(), + }) + } +} + +impl MemoryAuthorityMarker for SqliteWorkspaceAuthority { + fn memory_authority_status(&self) -> MemoryAuthorityStatus { + MemoryAuthorityStatus::LegacyFilesystemBackendPendingSqliteAuthority + } +} + +fn workspace_action_priority_name(priority: TicketWorkspaceActionPriority) -> &'static str { + match priority { + TicketWorkspaceActionPriority::ReadyForQueue => "ready_for_queue", + TicketWorkspaceActionPriority::ActiveWork => "active_work", + TicketWorkspaceActionPriority::Background => "background", + } +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::Path; + + use super::*; + use crate::store::WorkspaceRecord; + + #[tokio::test] + async fn sqlite_workspace_authority_reads_sqlite_records_without_filesystem_authority() { + let dir = tempfile::tempdir().unwrap(); + write_ticket(dir.path(), "00000000001J2", "Read bridge", "ready"); + let db_path = dir.path().join("workspace.db"); + SqliteTicketBackend::new(&db_path, "workspace-test") + .import_from_local_backend(&ticket::LocalTicketBackend::new( + dir.path().join(".yoi/tickets"), + )) + .unwrap(); + write_objective(dir.path(), "00000000001J3", "Control plane", "active"); + let store = SqliteWorkspaceStore::open(&db_path).unwrap(); + store + .upsert_workspace(&WorkspaceRecord { + workspace_id: "workspace-test".to_string(), + owner_account_id: None, + display_name: "Workspace Test".to_string(), + state: "active".to_string(), + created_at: "2026-01-01T00:00:00Z".to_string(), + updated_at: "2026-01-01T00:00:00Z".to_string(), + }) + .await + .unwrap(); + crate::objective_import::import_legacy_objectives_and_memory_staging( + dir.path(), + "workspace-test", + &store, + ) + .unwrap(); + write_objective( + dir.path(), + "00000000001J4", + "Filesystem-only objective", + "active", + ); + + let authority = SqliteWorkspaceAuthority::new(&db_path, "workspace-test").unwrap(); + let tickets = authority.list_tickets(20).unwrap(); + assert_eq!(tickets.record_authority, "workspace-sqlite"); + assert_eq!(tickets.items[0].record_source, "sqlite_yoi_ticket"); + assert_eq!(tickets.items[0].id, "00000000001J2"); + assert_eq!(tickets.items[0].state, "ready"); + assert_eq!( + tickets.items[0].workspace_action_priority, + "ready_for_queue" + ); + + let ticket = authority.ticket("00000000001J2").unwrap(); + assert!(ticket.body.contains("Ticket body")); + + let objectives = authority.list_objectives(20).unwrap(); + assert_eq!(objectives.record_authority, "workspace-sqlite"); + assert_eq!(objectives.items.len(), 1); + assert_eq!(objectives.items[0].id, "00000000001J3"); + assert_eq!(objectives.items[0].linked_tickets, vec!["00000000001J2"]); + + let objective = authority.objective("00000000001J3").unwrap(); + assert!(objective.body.contains("Objective body")); + assert_eq!( + authority.memory_authority_status(), + MemoryAuthorityStatus::LegacyFilesystemBackendPendingSqliteAuthority + ); + } + + #[test] + fn does_not_read_legacy_ticket_files_without_sqlite_import() { + let dir = tempfile::tempdir().unwrap(); + write_ticket(dir.path(), "00000000001J5", "Legacy file", "ready"); + let db_path = dir.path().join("workspace.db"); + + let authority = SqliteWorkspaceAuthority::new(&db_path, "workspace-test").unwrap(); + let tickets = authority.list_tickets(20).unwrap(); + assert!(tickets.items.is_empty()); + } + + fn write_ticket(root: &Path, id: &str, title: &str, state: &str) { + write_ticket_at(&root.join(".yoi/tickets"), id, title, state); + } + + fn write_ticket_at(ticket_root: &Path, id: &str, title: &str, state: &str) { + let ticket_dir = ticket_root.join(id); + fs::create_dir_all(&ticket_dir).unwrap(); + fs::write( + ticket_dir.join("item.md"), + format!( + r#"--- +title: "{title}" +state: "{state}" +created_at: "2026-01-01T00:00:00Z" +updated_at: "2026-01-02T00:00:00Z" +--- + +Ticket body. +"#, + ), + ) + .unwrap(); + fs::write(ticket_dir.join("thread.md"), "").unwrap(); + } + + fn write_objective(root: &Path, id: &str, title: &str, state: &str) { + let objective_dir = root.join(".yoi/objectives").join(id); + fs::create_dir_all(&objective_dir).unwrap(); + fs::write( + objective_dir.join("item.md"), + format!( + r#"--- +title: "{title}" +state: "{state}" +created_at: "2026-01-01T00:00:00Z" +updated_at: "2026-01-02T00:00:00Z" +linked_tickets: ["00000000001J2"] +--- + +Objective body. +"#, + ), + ) + .unwrap(); + } +} diff --git a/crates/workspace-server/src/lib.rs b/crates/workspace-server/src/lib.rs index 460502e0..87d58f10 100644 --- a/crates/workspace-server/src/lib.rs +++ b/crates/workspace-server/src/lib.rs @@ -5,6 +5,7 @@ //! Backend authority surfaces rather than Worker-local filesystem access. pub mod auth; +pub mod authority; pub mod companion; pub mod config; pub mod hosts; @@ -20,15 +21,17 @@ pub mod server; pub mod skills; pub mod store; +pub use authority::{ + MemoryAuthorityMarker, MemoryAuthorityStatus, ObjectiveAuthority, SqliteWorkspaceAuthority, + TicketAuthority, WorkspaceAuthority, +}; pub use config::{ BackendRuntimesConfigFile, ConfigDiff, ResolvedWorkspaceBackendConfig, WORKSPACE_BACKEND_CONFIG_RELATIVE_PATH, WORKSPACE_BACKEND_CONFIG_TEMPLATE, WorkspaceBackendConfigFile, }; pub use identity::{WORKSPACE_IDENTITY_RELATIVE_PATH, WorkspaceIdentity}; -pub use records::{ - LocalProjectRecordReader, ObjectiveDetail, ObjectiveSummary, TicketDetail, TicketSummary, -}; +pub use records::{ObjectiveDetail, ObjectiveSummary, TicketDetail, TicketSummary}; pub use repositories::{ ConfiguredRepository, GitCommitSummary, GitRemoteSummary, GitRepositorySummary, RepositoryLogRead, RepositoryRegistryReader, RepositorySummary, diff --git a/crates/workspace-server/src/records.rs b/crates/workspace-server/src/records.rs index 228d0e39..121741e7 100644 --- a/crates/workspace-server/src/records.rs +++ b/crates/workspace-server/src/records.rs @@ -1,178 +1,10 @@ -use std::path::{Path, PathBuf}; - use project_record::validate_record_id; use serde::{Deserialize, Serialize}; -use ticket::{ - SqliteTicketBackend, TicketBackend, TicketIdOrSlug, TicketListQuery, - TicketWorkspaceActionPriority, project_ticket_workspace_item, -}; -use crate::store::{ControlPlaneStore, SqliteWorkspaceStore}; use crate::{Error, Result}; -const DETAIL_BODY_LIMIT: usize = 64 * 1024; const SUMMARY_BODY_LIMIT: usize = 240; -#[derive(Clone)] -pub struct LocalProjectRecordReader { - workspace_id: String, - workspace_root: PathBuf, - store: SqliteWorkspaceStore, - ticket_backend: SqliteTicketBackend, -} - -impl LocalProjectRecordReader { - pub fn new( - workspace_root: impl Into, - database_path: impl Into, - workspace_id: impl Into, - ) -> Result { - let workspace_root = workspace_root.into(); - let database_path = database_path.into(); - let workspace_id = workspace_id.into(); - Ok(Self { - workspace_id: workspace_id.clone(), - workspace_root, - store: SqliteWorkspaceStore::open(&database_path)?, - ticket_backend: SqliteTicketBackend::new(database_path, workspace_id), - }) - } - - pub fn workspace_root(&self) -> &Path { - self.workspace_root.as_path() - } - - pub fn list_tickets(&self, limit: usize) -> Result> { - let mut items = Vec::new(); - for item in self.ticket_backend.list(TicketListQuery::all())? { - let ticket = self - .ticket_backend - .show(TicketIdOrSlug::Id(item.id.clone()))?; - let projection = project_ticket_workspace_item(&item, &ticket.relations.blockers, None); - items.push(TicketSummary { - id: item.id, - title: item.title, - state: item.workflow_state.as_str().to_string(), - priority: item.priority, - updated_at: item.updated_at, - queued_by: item.queued_by, - queued_at: item.queued_at, - workspace_action_priority: workspace_action_priority_name(projection.priority) - .to_string(), - record_source: "sqlite_yoi_ticket".to_string(), - }); - } - items.sort_by(|a, b| { - b.updated_at - .cmp(&a.updated_at) - .then_with(|| a.id.cmp(&b.id)) - }); - items.truncate(limit); - Ok(ProjectRecordList { - items, - invalid_records: Vec::new(), - record_authority: "local_yoi_project_records".to_string(), - }) - } - - pub fn ticket(&self, id: &str) -> Result { - validate_project_id(id)?; - let ticket = self - .ticket_backend - .show(TicketIdOrSlug::Id(id.to_string()))?; - let (body, body_truncated) = - truncate_body(ticket.document.body.as_str(), DETAIL_BODY_LIMIT); - Ok(TicketDetail { - id: ticket.meta.id, - title: ticket.meta.title, - state: ticket.meta.workflow_state.as_str().to_string(), - priority: ticket.meta.priority, - created_at: ticket.meta.created_at, - updated_at: ticket.meta.updated_at, - queued_by: ticket.meta.queued_by, - queued_at: ticket.meta.queued_at, - risk_flags: ticket.meta.risk_flags, - body, - body_truncated, - event_count: ticket.events.len(), - artifact_count: ticket.artifacts.len(), - record_source: "sqlite_yoi_ticket".to_string(), - }) - } - - pub fn list_objectives(&self, limit: usize) -> Result> { - let mut items = Vec::new(); - for record in self.store.list_objectives(&self.workspace_id, limit)? { - let linked_tickets = self - .store - .list_objective_ticket_links(&self.workspace_id, &record.objective_id)? - .into_iter() - .map(|link| link.ticket_id) - .collect::>(); - items.push(ObjectiveSummary { - id: record.objective_id, - title: record.title, - state: record.state, - updated_at: Some(record.updated_at), - summary: summarize_body(&record.body_md), - linked_tickets, - record_source: "workspace-sqlite".to_string(), - }); - } - Ok(ProjectRecordList { - items, - invalid_records: Vec::new(), - record_authority: "workspace-sqlite".to_string(), - }) - } - - pub fn objective(&self, id: &str) -> Result { - validate_project_id(id)?; - let record = self - .store - .get_objective(&self.workspace_id, id)? - .ok_or_else(|| Error::Store(format!("unknown objective `{id}`")))?; - let linked_tickets = self - .store - .list_objective_ticket_links(&self.workspace_id, &record.objective_id)? - .into_iter() - .map(|link| link.ticket_id) - .collect::>(); - let resources = self - .store - .list_objective_resources(&self.workspace_id, &record.objective_id)? - .into_iter() - .map(|resource| ObjectiveResourceSummary { - path: resource.resource_path, - media_type: resource.media_type, - bytes: resource.body.len(), - updated_at: resource.updated_at, - }) - .collect(); - let (body, body_truncated) = truncate_body(&record.body_md, DETAIL_BODY_LIMIT); - Ok(ObjectiveDetail { - id: record.objective_id, - title: record.title, - state: record.state, - created_at: Some(record.created_at), - updated_at: Some(record.updated_at), - linked_tickets, - resources, - body, - body_truncated, - record_source: "workspace-sqlite".to_string(), - }) - } -} - -fn workspace_action_priority_name(priority: TicketWorkspaceActionPriority) -> &'static str { - match priority { - TicketWorkspaceActionPriority::ReadyForQueue => "ready_for_queue", - TicketWorkspaceActionPriority::ActiveWork => "active_work", - TicketWorkspaceActionPriority::Background => "background", - } -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ProjectRecordList { pub items: Vec, @@ -250,11 +82,11 @@ pub struct ObjectiveResourceSummary { pub updated_at: String, } -fn validate_project_id(id: &str) -> Result<()> { +pub(crate) fn validate_project_id(id: &str) -> Result<()> { validate_record_id(id).map_err(|_| Error::InvalidRecordId(id.to_string())) } -fn summarize_body(body: &str) -> String { +pub(crate) fn summarize_body(body: &str) -> String { let summary = body .lines() .map(str::trim) @@ -268,7 +100,7 @@ fn summarize_body(body: &str) -> String { } } -fn truncate_body(body: &str, limit: usize) -> (String, bool) { +pub(crate) fn truncate_body(body: &str, limit: usize) -> (String, bool) { if body.len() <= limit { return (body.to_string(), false); } @@ -278,119 +110,3 @@ fn truncate_body(body: &str, limit: usize) -> (String, bool) { } (body[..end].to_string(), true) } - -#[cfg(test)] -mod tests { - use std::fs; - - use super::*; - use crate::store::WorkspaceRecord; - - #[tokio::test] - async fn reads_sqlite_yoi_ticket_and_objective_records_without_filesystem_authority() { - let dir = tempfile::tempdir().unwrap(); - write_ticket(dir.path(), "00000000001J2", "Read bridge", "ready"); - let db_path = dir.path().join("workspace.db"); - SqliteTicketBackend::new(&db_path, "workspace-test") - .import_from_local_backend(&ticket::LocalTicketBackend::new( - dir.path().join(".yoi/tickets"), - )) - .unwrap(); - write_objective(dir.path(), "00000000001J3", "Control plane", "active"); - let store = SqliteWorkspaceStore::open(&db_path).unwrap(); - store - .upsert_workspace(&WorkspaceRecord { - workspace_id: "workspace-test".to_string(), - owner_account_id: None, - display_name: "Workspace Test".to_string(), - state: "active".to_string(), - created_at: "2026-01-01T00:00:00Z".to_string(), - updated_at: "2026-01-01T00:00:00Z".to_string(), - }) - .await - .unwrap(); - crate::objective_import::import_legacy_objectives_and_memory_staging( - dir.path(), - "workspace-test", - &store, - ) - .unwrap(); - - let reader = LocalProjectRecordReader::new(dir.path(), &db_path, "workspace-test").unwrap(); - let tickets = reader.list_tickets(20).unwrap(); - assert_eq!(tickets.record_authority, "local_yoi_project_records"); - assert_eq!(tickets.items[0].record_source, "sqlite_yoi_ticket"); - assert_eq!(tickets.items[0].id, "00000000001J2"); - assert_eq!(tickets.items[0].state, "ready"); - assert_eq!( - tickets.items[0].workspace_action_priority, - "ready_for_queue" - ); - - let ticket = reader.ticket("00000000001J2").unwrap(); - assert!(ticket.body.contains("Ticket body")); - - let objectives = reader.list_objectives(20).unwrap(); - assert_eq!(objectives.items[0].id, "00000000001J3"); - assert_eq!(objectives.items[0].linked_tickets, vec!["00000000001J2"]); - - let objective = reader.objective("00000000001J3").unwrap(); - assert!(objective.body.contains("Objective body")); - } - #[test] - fn does_not_read_legacy_ticket_files_without_sqlite_import() { - let dir = tempfile::tempdir().unwrap(); - write_ticket(dir.path(), "00000000001J5", "Legacy file", "ready"); - let db_path = dir.path().join("workspace.db"); - - let reader = LocalProjectRecordReader::new(dir.path(), &db_path, "workspace-test").unwrap(); - let tickets = reader.list_tickets(20).unwrap(); - assert!(tickets.items.is_empty()); - } - - fn write_ticket(root: &Path, id: &str, title: &str, state: &str) { - write_ticket_at(&root.join(".yoi/tickets"), id, title, state); - } - - fn write_ticket_at(ticket_root: &Path, id: &str, title: &str, state: &str) { - let ticket_dir = ticket_root.join(id); - fs::create_dir_all(&ticket_dir).unwrap(); - fs::write( - ticket_dir.join("item.md"), - format!( - r#"--- -title: "{title}" -state: "{state}" -created_at: "2026-01-01T00:00:00Z" -updated_at: "2026-01-02T00:00:00Z" ---- - -Ticket body. -"#, - ), - ) - .unwrap(); - fs::write(ticket_dir.join("thread.md"), "").unwrap(); - } - - fn write_objective(root: &Path, id: &str, title: &str, state: &str) { - let objective_dir = root.join(".yoi/objectives").join(id); - fs::create_dir_all(&objective_dir).unwrap(); - fs::write( - objective_dir.join("item.md"), - format!( - r#"--- -title: "{title}" -state: "{state}" -created_at: "2026-01-01T00:00:00Z" -updated_at: "2026-01-02T00:00:00Z" -linked_tickets: ["00000000001J2"] ---- - -Objective body. -"#, - ), - ) - .unwrap(); - } -} diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 782b201a..bd31ec83 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -42,6 +42,7 @@ use crate::auth::{ new_user_code, normalize_handle, parse_cookie, resolve_request_actor, rfc3339_after, session_set_cookie, token_hash, }; +use crate::authority::{ObjectiveAuthority, SqliteWorkspaceAuthority, TicketAuthority}; use crate::companion::{ CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse, CompanionStatusResponse, CompanionTranscriptProjection, @@ -68,9 +69,7 @@ use crate::profile_settings::{ UpdateWorkspaceMetadataRequest, UpdateWorkspaceProfileRegistryRequest, UpdateWorkspaceProfileSourceRequest, WriteWorkspaceProfileTreeFileRequest, }; -use crate::records::{ - LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary, -}; +use crate::records::{ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary}; use crate::repositories::{ ConfiguredRepository, RepositoryListProjection, RepositoryLogRead, RepositoryLookupError, RepositoryRegistryReader, RepositorySummary, @@ -229,7 +228,7 @@ impl ServerConfig { pub struct WorkspaceApi { config: ServerConfig, store: Arc, - records: LocalProjectRecordReader, + authority: SqliteWorkspaceAuthority, runtime: Arc, memory_consolidater_worker: Arc>>, companion: Arc, @@ -328,8 +327,7 @@ impl WorkspaceApi { let companion = Arc::new(CompanionConsole::disabled()); let observation_proxy = BackendObservationProxy::new(config.runtime_event_sources.clone()); Ok(Self { - records: LocalProjectRecordReader::new( - config.workspace_root.clone(), + authority: SqliteWorkspaceAuthority::new( config.database_path.clone(), config.workspace_id.clone(), )?, @@ -3648,7 +3646,7 @@ async fn list_tickets( items, invalid_records, record_authority, - } = api.records.list_tickets(limit)?; + } = api.authority.list_tickets(limit)?; Ok(Json(ListResponse { workspace_id: api.config.workspace_id, limit, @@ -3662,7 +3660,7 @@ async fn get_ticket( State(api): State, AxumPath(id): AxumPath, ) -> ApiResult> { - Ok(Json(api.records.ticket(&id)?)) + Ok(Json(api.authority.ticket(&id)?)) } async fn list_objectives( @@ -3674,7 +3672,7 @@ async fn list_objectives( items, invalid_records, record_authority, - } = api.records.list_objectives(limit)?; + } = api.authority.list_objectives(limit)?; Ok(Json(ListResponse { workspace_id: api.config.workspace_id, limit, @@ -3688,7 +3686,7 @@ async fn get_objective( State(api): State, AxumPath(id): AxumPath, ) -> ApiResult> { - Ok(Json(api.records.objective(&id)?)) + Ok(Json(api.authority.objective(&id)?)) } async fn list_repositories( @@ -3752,7 +3750,7 @@ async fn repository_tickets( items, invalid_records, record_authority, - } = api.records.list_tickets(limit)?; + } = api.authority.list_tickets(limit)?; Ok(Json(RepositoryTicketsResponse { workspace_id: api.config.workspace_id, repository_id: canonical_repository_id,