lore_cli/sync/mod.rs
1//! Serverless git-ref sync for Lore.
2//!
3//! This module stores AI reasoning history in the user's own git repository
4//! under `refs/lore/*`, with no hosted service. A lore store ref points at a
5//! commit whose tree holds one encrypted blob per session plus a plaintext
6//! salt, so the reasoning rides along with the code over plain git.
7//!
8//! The module is intentionally split into focused submodules:
9//!
10//! - [`encryption`] - Argon2id key derivation and AES-256-GCM encryption on
11//! raw bytes.
12//! - [`keystore`] - passphrase-to-key derivation, salt generation, and
13//! persistence of the derived key (file or OS keychain).
14//! - [`store`] - the consolidated session-blob pipeline: serialize a full
15//! reasoning record, gzip it, encrypt it, and the inverse.
16//! - [`gitref`] - git plumbing (shelling out to the user's `git` binary) for
17//! reading and writing `refs/lore/*`.
18//!
19//! Only the foundational layers are implemented here. The CLI command, the
20//! global personal store, and daemon wiring are built in later phases on top of
21//! these primitives.
22
23pub mod encryption;
24pub mod gitref;
25pub mod keystore;
26pub mod store;
27
28/// Errors produced by the git-ref sync subsystem.
29///
30/// A single error type spans the encryption, key storage, blob pipeline, and
31/// git plumbing layers so callers can propagate failures with a single `?`.
32#[derive(Debug, thiserror::Error)]
33pub enum SyncError {
34 /// Encryption or decryption failed.
35 #[error("Encryption error: {0}")]
36 Encryption(String),
37
38 /// Storing, loading, or deleting the encryption key failed.
39 #[error("Key storage error: {0}")]
40 KeyStorage(String),
41
42 /// Serializing or deserializing a session record failed.
43 #[error("Serialization error: {0}")]
44 Serialization(String),
45
46 /// Gzip compression or decompression failed.
47 #[error("Compression error: {0}")]
48 Compression(String),
49
50 /// A shelled-out `git` command failed.
51 #[error("Git command failed: {0}")]
52 Git(String),
53
54 /// A checked (compare-and-swap) ref update was rejected because the ref did
55 /// not hold the expected old value. A caller may re-read and retry.
56 #[error("Ref update rejected (concurrent change): {0}")]
57 RefCasMismatch(String),
58
59 /// An underlying I/O operation failed.
60 #[error("I/O error: {0}")]
61 Io(#[from] std::io::Error),
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn test_sync_error_display_encryption() {
70 let err = SyncError::Encryption("bad key".to_string());
71 assert!(err.to_string().contains("bad key"));
72 }
73
74 #[test]
75 fn test_sync_error_display_git() {
76 let err = SyncError::Git("not a repository".to_string());
77 assert!(err.to_string().contains("not a repository"));
78 }
79
80 #[test]
81 fn test_sync_error_from_io() {
82 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
83 let err: SyncError = io_err.into();
84 assert!(matches!(err, SyncError::Io(_)));
85 }
86}