Skip to main content

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}