patina-ai 0.23.0

Context orchestration for AI development - captures and evolves patterns over time
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//! Single source of truth for ALL Patina filesystem layout.
//!
//! This module defines WHERE data lives. It has no I/O, no validation,
//! no business logic. One file shows the entire filesystem layout.
//!
//! # Design Philosophy
//!
//! From `rationale-eskil-steenberg.md`:
//! > "It's faster to write 5 lines of code today than to write 1 line today and edit it later."
//!
//! This API is complete from day one - user-level AND project-level paths.
//! The API never needs to change. Migrations can happen incrementally.
//!
//! # User-Level Paths (~/.patina/)
//!
//! ```text
//! ~/.patina/
//! ├── config.toml              # Global config
//! ├── registry.yaml            # Project/repo registry
//! ├── adapters/                # LLM adapter templates
//! ├── personas/default/events/ # Source (valuable)
//! ├── run/                     # Runtime (socket, pid, token)
//! │   ├── serve.sock           # Unix domain socket
//! │   └── serve.token          # Bearer token file (TCP only)
//! └── cache/                   # Derived (rebuildable)
//!     ├── repos/               # Cloned reference repos
//!     └── personas/default/    # Materialized indices
//! ```
//!
//! # Project-Level Paths (project/.patina/)
//!
//! ```text
//! project/.patina/
//! ├── config.toml              # Project config (committed)
//! ├── uid                      # Project identity (committed)
//! ├── oxidize.yaml             # Embedding recipe (committed)
//! ├── versions.json            # Version manifest (committed)
//! └── local/                   # Local state (gitignored)
//!     ├── data/
//!     │   ├── patina.db        # SQLite database
//!     │   └── embeddings/      # Vector indices
//!     └── backups/             # Backup files
//! ```

use std::path::{Path, PathBuf};

// =============================================================================
// User Level (~/.patina/)
// =============================================================================

/// User's patina home directory: `~/.patina/`
pub fn patina_home() -> PathBuf {
    dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join(".patina")
}

/// Cache directory for all rebuildable data: `~/.patina/cache/`
pub fn patina_cache() -> PathBuf {
    patina_home().join("cache")
}

/// Global config file: `~/.patina/config.toml`
pub fn config_path() -> PathBuf {
    patina_home().join("config.toml")
}

/// Project/repo registry: `~/.patina/registry.yaml`
pub fn registry_path() -> PathBuf {
    patina_home().join("registry.yaml")
}

/// LLM adapter templates: `~/.patina/adapters/`
pub fn adapters_dir() -> PathBuf {
    patina_home().join("adapters")
}

/// Persona paths (cross-project user knowledge)
pub mod persona {
    use super::*;

    /// Source events (valuable): `~/.patina/personas/default/events/`
    pub fn events_dir() -> PathBuf {
        patina_home().join("personas/default/events")
    }

    /// Materialized cache (rebuildable): `~/.patina/cache/personas/default/`
    pub fn cache_dir() -> PathBuf {
        patina_cache().join("personas/default")
    }
}

/// Reference repository paths
pub mod repos {
    use super::*;

    /// Cloned repos (rebuildable): `~/.patina/cache/repos/`
    pub fn cache_dir() -> PathBuf {
        patina_cache().join("repos")
    }
}

/// Secrets management paths (v2 - local age-encrypted vault)
pub mod secrets {
    use super::*;
    use std::path::Path;

    // =========================================================================
    // Global (mother) paths - ~/.patina/
    // =========================================================================

    /// Global secrets registry: `~/.patina/secrets.toml`
    pub fn registry_path() -> PathBuf {
        patina_home().join("secrets.toml")
    }

    /// Global vault (encrypted): `~/.patina/vault.age`
    pub fn vault_path() -> PathBuf {
        patina_home().join("vault.age")
    }

    /// Global recipient (your public key): `~/.patina/recipient.txt`
    /// Note: singular - global vault has one recipient (you)
    pub fn recipient_path() -> PathBuf {
        patina_home().join("recipient.txt")
    }

    // =========================================================================
    // Project paths - {project}/.patina/
    // =========================================================================

    /// Project secrets registry: `{root}/.patina/secrets.toml`
    pub fn project_registry_path(root: &Path) -> PathBuf {
        root.join(".patina").join("secrets.toml")
    }

    /// Project vault (encrypted): `{root}/.patina/vault.age`
    pub fn project_vault_path(root: &Path) -> PathBuf {
        root.join(".patina").join("vault.age")
    }

    /// Project recipients (shared): `{root}/.patina/recipients.txt`
    /// Note: plural - project vault has multiple recipients
    pub fn project_recipients_path(root: &Path) -> PathBuf {
        root.join(".patina").join("recipients.txt")
    }
}

/// Serve daemon runtime paths (socket, pid, token)
pub mod serve {
    use super::*;

    /// Runtime directory: `~/.patina/run/`
    /// Permissions: 0o700 (owner only)
    pub fn run_dir() -> PathBuf {
        patina_home().join("run")
    }

    /// Unix domain socket: `~/.patina/run/serve.sock`
    /// Permissions: 0o600 (owner only)
    pub fn socket_path() -> PathBuf {
        run_dir().join("serve.sock")
    }

    /// Bearer token file (TCP only): `~/.patina/run/serve.token`
    /// Permissions: 0o600 (owner only)
    pub fn token_path() -> PathBuf {
        run_dir().join("serve.token")
    }

    /// PID file: `~/.patina/run/mother.pid`
    /// Permissions: 0o600 (owner only)
    pub fn pid_path() -> PathBuf {
        run_dir().join("mother.pid")
    }
}

/// Plugin paths (WASM children, command plugins, work dirs)
pub mod plugin {
    use super::*;

    /// WASM children directory: `~/.patina/children/`
    /// Contains .wasm files + plugin.toml manifests for Mother daemon children.
    pub fn children_dir() -> PathBuf {
        patina_home().join("children")
    }

    /// CLI command plugins directory: `~/.patina/plugins/`
    /// Contains .wasm files + plugin.toml manifests for CLI command plugins (Phase 2+).
    pub fn plugins_dir() -> PathBuf {
        patina_home().join("plugins")
    }

    /// Plugin work directory (WASI sandbox root): `~/.patina/plugins/{name}/work/`
    /// Mapped to `/work/` in the plugin's virtual filesystem (Phase 2+ when WASI lands).
    pub fn work_dir(name: &str) -> PathBuf {
        plugins_dir().join(name).join("work")
    }
}

/// Mother paths (cross-project graph and federation)
pub mod mother {
    use super::*;

    /// Mother data directory: `~/.patina/mother/`
    pub fn data_dir() -> PathBuf {
        patina_home().join("mother")
    }

    /// Relationship graph: `~/.patina/mother/graph.db`
    pub fn graph_db() -> PathBuf {
        data_dir().join("graph.db")
    }
}

/// Model management paths (base models shared across projects)
pub mod models {
    use super::*;

    /// Model cache directory: `~/.patina/cache/models/`
    pub fn cache_dir() -> PathBuf {
        patina_cache().join("models")
    }

    /// Specific model directory: `~/.patina/cache/models/{name}/`
    pub fn model_dir(name: &str) -> PathBuf {
        cache_dir().join(name)
    }

    /// Model ONNX file: `~/.patina/cache/models/{name}/model.onnx`
    pub fn model_onnx(name: &str) -> PathBuf {
        model_dir(name).join("model.onnx")
    }

    /// Model tokenizer: `~/.patina/cache/models/{name}/tokenizer.json`
    pub fn model_tokenizer(name: &str) -> PathBuf {
        model_dir(name).join("tokenizer.json")
    }

    /// Lock file tracking provenance: `~/.patina/models.lock`
    pub fn lock_path() -> PathBuf {
        patina_home().join("models.lock")
    }
}

// =============================================================================
// Project Level (project/.patina/)
// =============================================================================

/// Project-level paths, relative to a project root.
///
/// All functions take a `root: &Path` parameter - the project directory.
///
/// # Example
///
/// ```
/// use std::path::Path;
/// use patina::paths::project;
///
/// let root = Path::new("/home/user/myproject");
/// let db = project::db_path(root);
/// assert_eq!(db, Path::new("/home/user/myproject/.patina/local/data/patina.db"));
/// ```
pub mod project {
    use super::*;

    /// Project's patina directory: `.patina/`
    pub fn patina_dir(root: &Path) -> PathBuf {
        root.join(".patina")
    }

    /// Project config: `.patina/config.toml` (committed)
    pub fn config_path(root: &Path) -> PathBuf {
        root.join(".patina/config.toml")
    }

    /// Local state directory (gitignored): `.patina/local/`
    pub fn local_dir(root: &Path) -> PathBuf {
        root.join(".patina/local")
    }

    /// Derived data directory: `.patina/local/data/`
    pub fn data_dir(root: &Path) -> PathBuf {
        root.join(".patina/local/data")
    }

    /// Main SQLite database: `.patina/local/data/patina.db`
    pub fn db_path(root: &Path) -> PathBuf {
        root.join(".patina/local/data/patina.db")
    }

    /// Embedding indices: `.patina/local/data/embeddings/`
    pub fn embeddings_dir(root: &Path) -> PathBuf {
        root.join(".patina/local/data/embeddings")
    }

    /// Model-specific projections: `.patina/local/data/embeddings/{model}/projections/`
    pub fn model_projections_dir(root: &Path, model: &str) -> PathBuf {
        root.join(format!(
            ".patina/local/data/embeddings/{}/projections",
            model
        ))
    }

    /// Oxidize recipe: `.patina/oxidize.yaml` (committed)
    pub fn recipe_path(root: &Path) -> PathBuf {
        root.join(".patina/oxidize.yaml")
    }

    /// Version manifest: `.patina/versions.json` (committed)
    pub fn versions_path(root: &Path) -> PathBuf {
        root.join(".patina/versions.json")
    }

    /// Backup directory: `.patina/local/backups/`
    pub fn backups_dir(root: &Path) -> PathBuf {
        root.join(".patina/local/backups")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_patina_home() {
        let home = patina_home();
        assert!(home.ends_with(".patina"));
    }

    #[test]
    fn test_patina_cache() {
        let cache = patina_cache();
        assert!(cache.ends_with("cache"));
        assert!(cache.starts_with(patina_home()));
    }

    #[test]
    fn test_persona_paths() {
        let events = persona::events_dir();
        let cache = persona::cache_dir();

        assert!(events.to_string_lossy().contains("personas/default/events"));
        assert!(cache.to_string_lossy().contains("cache/personas/default"));
    }

    #[test]
    fn test_repos_cache() {
        let repos = repos::cache_dir();
        assert!(repos.to_string_lossy().contains("cache/repos"));
    }

    #[test]
    fn test_models_paths() {
        let cache = models::cache_dir();
        assert!(cache.to_string_lossy().contains("cache/models"));

        let model_dir = models::model_dir("e5-base-v2");
        assert!(model_dir
            .to_string_lossy()
            .contains("cache/models/e5-base-v2"));

        let onnx = models::model_onnx("e5-base-v2");
        assert!(onnx.to_string_lossy().ends_with("e5-base-v2/model.onnx"));

        let tokenizer = models::model_tokenizer("e5-base-v2");
        assert!(tokenizer
            .to_string_lossy()
            .ends_with("e5-base-v2/tokenizer.json"));

        let lock = models::lock_path();
        assert!(lock.to_string_lossy().ends_with("models.lock"));
        // Lock is at ~/.patina/, not in cache
        assert!(!lock.to_string_lossy().contains("cache"));
    }

    #[test]
    fn test_serve_paths() {
        let run = serve::run_dir();
        assert!(run.to_string_lossy().ends_with(".patina/run"));

        let sock = serve::socket_path();
        assert!(sock.to_string_lossy().ends_with("run/serve.sock"));

        let token = serve::token_path();
        assert!(token.to_string_lossy().ends_with("run/serve.token"));

        let pid = serve::pid_path();
        assert!(pid.to_string_lossy().ends_with("run/mother.pid"));
    }

    #[test]
    fn test_project_paths() {
        let root = Path::new("/tmp/test-project");

        assert_eq!(
            project::patina_dir(root),
            PathBuf::from("/tmp/test-project/.patina")
        );
        assert_eq!(
            project::local_dir(root),
            PathBuf::from("/tmp/test-project/.patina/local")
        );
        assert_eq!(
            project::db_path(root),
            PathBuf::from("/tmp/test-project/.patina/local/data/patina.db")
        );
        assert_eq!(
            project::model_projections_dir(root, "e5-base-v2"),
            PathBuf::from("/tmp/test-project/.patina/local/data/embeddings/e5-base-v2/projections")
        );
    }
}