Skip to main content

zenith_session/
layout.rs

1//! Pure path-builder helpers for the zenith store layout.
2//!
3//! [`StorePaths`] computes filesystem paths for every well-known location
4//! under the zenith data directory.  It performs NO I/O — callers must pass
5//! the resulting paths to an [`crate::adapter::Fs`] implementation.
6//!
7//! # Store layout
8//!
9//! ```text
10//! <data_dir>/
11//!   docs/
12//!     <doc_id>/
13//!       objects/         ← immutable object blobs (future unit)
14//!       versions.jsonl   ← append-only version manifest (future unit)
15//!       session/         ← mutable local session state
16//!       runs.jsonl       ← append-only agent-runs log
17//!       previews.jsonl   ← append-only preview-artifacts log
18//!       scratch/
19//!         index.jsonl    ← scratch/candidate index
20//! ```
21
22use std::path::PathBuf;
23
24/// Path-builder for the zenith local store rooted at a data directory.
25///
26/// All methods are pure: they compute a [`PathBuf`] via `Path::join` and
27/// return it without touching the filesystem.
28pub struct StorePaths {
29    root: PathBuf,
30}
31
32impl StorePaths {
33    /// Create a new `StorePaths` rooted at `data_dir`.
34    pub fn new(data_dir: impl Into<PathBuf>) -> Self {
35        Self {
36            root: data_dir.into(),
37        }
38    }
39
40    /// The root directory holding all per-document history: `<root>/docs`.
41    pub fn docs_root(&self) -> PathBuf {
42        self.root.join("docs")
43    }
44
45    /// Directory that contains all data for a given document.
46    ///
47    /// `<root>/docs/<doc_id>`
48    pub fn doc_dir(&self, doc_id: &str) -> PathBuf {
49        self.docs_root().join(doc_id)
50    }
51
52    /// Directory that holds immutable object blobs for a document.
53    ///
54    /// `<root>/docs/<doc_id>/objects`
55    pub fn objects_dir(&self, doc_id: &str) -> PathBuf {
56        self.doc_dir(doc_id).join("objects")
57    }
58
59    /// Append-only version manifest file for a document.
60    ///
61    /// `<root>/docs/<doc_id>/versions.jsonl`
62    pub fn versions_file(&self, doc_id: &str) -> PathBuf {
63        self.doc_dir(doc_id).join("versions.jsonl")
64    }
65
66    /// Mutable local session state directory for a document.
67    ///
68    /// `<root>/docs/<doc_id>/session`
69    pub fn session_dir(&self, doc_id: &str) -> PathBuf {
70        self.doc_dir(doc_id).join("session")
71    }
72
73    /// Persisted per-doc metadata file.
74    ///
75    /// `<root>/docs/<doc_id>/meta.json`
76    pub fn meta_file(&self, doc_id: &str) -> PathBuf {
77        self.doc_dir(doc_id).join("meta.json")
78    }
79
80    /// Append-only agent-runs log: `<root>/docs/<doc_id>/runs.jsonl`.
81    pub fn runs_file(&self, doc_id: &str) -> PathBuf {
82        self.doc_dir(doc_id).join("runs.jsonl")
83    }
84
85    /// Append-only preview-artifacts log: `<root>/docs/<doc_id>/previews.jsonl`.
86    pub fn previews_file(&self, doc_id: &str) -> PathBuf {
87        self.doc_dir(doc_id).join("previews.jsonl")
88    }
89
90    /// Scratch/candidate directory: `<root>/docs/<doc_id>/scratch`.
91    pub fn scratch_dir(&self, doc_id: &str) -> PathBuf {
92        self.doc_dir(doc_id).join("scratch")
93    }
94
95    /// Scratch/candidate index: `<root>/docs/<doc_id>/scratch/index.jsonl`.
96    pub fn scratch_index(&self, doc_id: &str) -> PathBuf {
97        self.scratch_dir(doc_id).join("index.jsonl")
98    }
99
100    /// Per-document workspace directory for ephemeral working artifacts that are
101    /// NOT part of the deliverable `.zen`: `<root>/docs/<doc_id>/workspace`.
102    pub fn workspace_dir(&self, doc_id: &str) -> PathBuf {
103        self.doc_dir(doc_id).join("workspace")
104    }
105
106    /// Predictable scratch area for rendered previews produced via the agent
107    /// (MCP) surface: `<root>/docs/<doc_id>/workspace/renders`.
108    ///
109    /// Keeping previews here means the `.zen` holds only final content while the
110    /// agent still has one stable, per-document place to find its render output.
111    pub fn workspace_renders_dir(&self, doc_id: &str) -> PathBuf {
112        self.workspace_dir(doc_id).join("renders")
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    fn paths() -> StorePaths {
121        StorePaths::new("/data")
122    }
123
124    #[test]
125    fn docs_root() {
126        assert_eq!(paths().docs_root(), PathBuf::from("/data/docs"));
127    }
128
129    #[test]
130    fn doc_dir() {
131        assert_eq!(paths().doc_dir("doc1"), PathBuf::from("/data/docs/doc1"));
132    }
133
134    #[test]
135    fn objects_dir() {
136        assert_eq!(
137            paths().objects_dir("doc1"),
138            PathBuf::from("/data/docs/doc1/objects")
139        );
140    }
141
142    #[test]
143    fn versions_file() {
144        assert_eq!(
145            paths().versions_file("doc1"),
146            PathBuf::from("/data/docs/doc1/versions.jsonl")
147        );
148    }
149
150    #[test]
151    fn session_dir() {
152        assert_eq!(
153            paths().session_dir("doc1"),
154            PathBuf::from("/data/docs/doc1/session")
155        );
156    }
157
158    #[test]
159    fn different_doc_ids_produce_different_paths() {
160        let p = paths();
161        assert_ne!(p.doc_dir("alpha"), p.doc_dir("beta"));
162    }
163
164    #[test]
165    fn meta_file() {
166        assert_eq!(
167            paths().meta_file("doc1"),
168            PathBuf::from("/data/docs/doc1/meta.json")
169        );
170    }
171
172    #[test]
173    fn runs_file() {
174        assert_eq!(
175            paths().runs_file("doc1"),
176            PathBuf::from("/data/docs/doc1/runs.jsonl")
177        );
178    }
179
180    #[test]
181    fn previews_file() {
182        assert_eq!(
183            paths().previews_file("doc1"),
184            PathBuf::from("/data/docs/doc1/previews.jsonl")
185        );
186    }
187
188    #[test]
189    fn scratch_dir() {
190        assert_eq!(
191            paths().scratch_dir("doc1"),
192            PathBuf::from("/data/docs/doc1/scratch")
193        );
194    }
195
196    #[test]
197    fn scratch_index() {
198        assert_eq!(
199            paths().scratch_index("doc1"),
200            PathBuf::from("/data/docs/doc1/scratch/index.jsonl")
201        );
202    }
203
204    #[test]
205    fn workspace_dir() {
206        assert_eq!(
207            paths().workspace_dir("doc1"),
208            PathBuf::from("/data/docs/doc1/workspace")
209        );
210    }
211
212    #[test]
213    fn workspace_renders_dir() {
214        assert_eq!(
215            paths().workspace_renders_dir("doc1"),
216            PathBuf::from("/data/docs/doc1/workspace/renders")
217        );
218    }
219}