Skip to main content

sqry_daemon/workspace/
builder.rs

1//! Workspace graph builder abstraction.
2//!
3//! [`WorkspaceBuilder`] is the dependency-injection seam between the
4//! daemon's workspace manager and sqry-core's full graph-build
5//! pipeline. Production code wraps
6//! [`sqry_core::graph::unified::build::build_unified_graph`] via
7//! [`RealWorkspaceBuilder`]; unit tests supply [`EmptyGraphBuilder`],
8//! [`FailingGraphBuilder`], or a custom impl.
9//!
10//! The trait is `Send + Sync` because the builder is held across a
11//! rebuild-dispatcher task boundary. Every concrete implementation
12//! must be cheap to clone — callers typically `Arc`-wrap the builder
13//! and share it across the reaper + dispatcher + lifecycle tasks.
14
15use std::{path::Path, sync::Arc};
16
17use sqry_core::graph::CodeGraph;
18
19use crate::error::DaemonError;
20
21/// Build a [`CodeGraph`] for the given workspace root.
22///
23/// Trait object–friendly; [`get_or_load`] and friends accept a
24/// `&dyn WorkspaceBuilder` so the caller can choose between the
25/// production [`UnifiedGraphBuilder`] and a test-local in-memory
26/// variant without the manager caring.
27///
28/// [`get_or_load`]: super::WorkspaceManager::get_or_load
29pub trait WorkspaceBuilder: Send + Sync + std::fmt::Debug {
30    /// Build the graph rooted at `workspace_root`. The implementation
31    /// is responsible for any required lock-free / rayon parallelism
32    /// and for honouring cancellation signals it consumes (e.g.
33    /// pass-boundary checks in the rebuild pipeline).
34    ///
35    /// # Errors
36    ///
37    /// The daemon converts any returned [`DaemonError`] into a Failed
38    /// workspace state + JSON-RPC `-32001 workspace_build_failed`.
39    fn build(&self, workspace_root: &Path) -> Result<CodeGraph, DaemonError>;
40}
41
42// Allow calling builders through an `Arc` so they can be shared
43// between tasks without explicit `.as_ref()` spam at every call site.
44impl<T: WorkspaceBuilder + ?Sized> WorkspaceBuilder for Arc<T> {
45    fn build(&self, workspace_root: &Path) -> Result<CodeGraph, DaemonError> {
46        (**self).build(workspace_root)
47    }
48}
49
50/// Test-only builder that always returns a freshly-built empty
51/// [`CodeGraph`]. Useful for admission-accounting tests where the
52/// actual graph content does not matter.
53#[doc(hidden)]
54#[derive(Debug, Default, Clone, Copy)]
55pub struct EmptyGraphBuilder;
56
57impl WorkspaceBuilder for EmptyGraphBuilder {
58    fn build(&self, _workspace_root: &Path) -> Result<CodeGraph, DaemonError> {
59        Ok(CodeGraph::new())
60    }
61}
62
63/// Test builder that always returns a configured error. Used by the
64/// Failed-state unit tests in Phase 6c; shipping the type here so
65/// every phase after 6b uses the same builder abstraction.
66#[doc(hidden)]
67#[derive(Debug, Clone)]
68pub struct FailingGraphBuilder {
69    /// Reason string surfaced via [`DaemonError::WorkspaceBuildFailed`].
70    pub reason: String,
71}
72
73impl FailingGraphBuilder {
74    /// Construct a failing builder with the given reason.
75    #[must_use]
76    pub fn new(reason: impl Into<String>) -> Self {
77        Self {
78            reason: reason.into(),
79        }
80    }
81}
82
83impl WorkspaceBuilder for FailingGraphBuilder {
84    fn build(&self, workspace_root: &Path) -> Result<CodeGraph, DaemonError> {
85        Err(DaemonError::WorkspaceBuildFailed {
86            root: workspace_root.to_path_buf(),
87            reason: self.reason.clone(),
88        })
89    }
90}
91
92/// Production [`WorkspaceBuilder`] that delegates to
93/// [`sqry_core::graph::unified::build::build_unified_graph`].
94///
95/// Task 8 Phase 8a. Task 9's daemon bootstrap constructs exactly one
96/// [`RealWorkspaceBuilder`] per daemon process, wrapping a shared
97/// [`sqry_core::plugin::PluginManager`]. Tests inject
98/// [`EmptyGraphBuilder`] or a custom builder.
99pub struct RealWorkspaceBuilder {
100    plugins: Arc<sqry_core::plugin::PluginManager>,
101    build_config: sqry_core::graph::unified::build::BuildConfig,
102}
103
104impl std::fmt::Debug for RealWorkspaceBuilder {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        // `PluginManager` is intentionally not `Debug` (the 37 plugin
107        // registrations are too noisy). Report pointer identity only.
108        f.debug_struct("RealWorkspaceBuilder")
109            .field(
110                "plugins",
111                &format_args!("<PluginManager@{:p}>", Arc::as_ptr(&self.plugins)),
112            )
113            .field("build_config", &self.build_config)
114            .finish()
115    }
116}
117
118impl RealWorkspaceBuilder {
119    /// Construct a real builder with default
120    /// [`sqry_core::graph::unified::build::BuildConfig`].
121    #[must_use]
122    pub fn new(plugins: Arc<sqry_core::plugin::PluginManager>) -> Self {
123        Self {
124            plugins,
125            build_config: sqry_core::graph::unified::build::BuildConfig::default(),
126        }
127    }
128
129    /// Construct a real builder with a caller-supplied
130    /// [`sqry_core::graph::unified::build::BuildConfig`].
131    #[must_use]
132    pub fn with_build_config(
133        plugins: Arc<sqry_core::plugin::PluginManager>,
134        build_config: sqry_core::graph::unified::build::BuildConfig,
135    ) -> Self {
136        Self {
137            plugins,
138            build_config,
139        }
140    }
141}
142
143impl WorkspaceBuilder for RealWorkspaceBuilder {
144    fn build(&self, workspace_root: &Path) -> Result<CodeGraph, DaemonError> {
145        sqry_core::graph::unified::build::build_unified_graph(
146            workspace_root,
147            &self.plugins,
148            &self.build_config,
149        )
150        .map_err(|e| DaemonError::WorkspaceBuildFailed {
151            root: workspace_root.to_path_buf(),
152            reason: e.to_string(),
153        })
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn empty_builder_returns_fresh_graph() {
163        let b = EmptyGraphBuilder;
164        let g = b.build(Path::new("/repos/example")).expect("always ok");
165        assert_eq!(g.node_count(), 0);
166    }
167
168    #[test]
169    fn failing_builder_surfaces_reason_and_root() {
170        let b = FailingGraphBuilder::new("plugin panic");
171        let err = b
172            .build(Path::new("/repos/example"))
173            .expect_err("always fails");
174        match err {
175            DaemonError::WorkspaceBuildFailed { root, reason } => {
176                assert_eq!(root, Path::new("/repos/example"));
177                assert_eq!(reason, "plugin panic");
178            }
179            other => panic!("wrong variant: {other:?}"),
180        }
181    }
182
183    #[test]
184    fn arc_builder_passes_through_to_inner() {
185        let inner: Arc<dyn WorkspaceBuilder> = Arc::new(EmptyGraphBuilder);
186        let g = inner
187            .build(Path::new("/repos/example"))
188            .expect("arc-wrapped builder delegates");
189        assert_eq!(g.node_count(), 0);
190    }
191}