Skip to main content

memstead_base/
storage.rs

1//! Mem write-side trait. The [`MemWriter`] surface is
2//! backend-neutral: it deals in mem-relative paths, raw bytes, and
3//! opaque [`CommitId`] strings. Two adapters live in the workspace
4//! today: the git-tree adapter in `memstead_git_branch::storage::git_tree`
5//! (mem-repo writes) and [`filesystem::FilesystemMemWriter`]
6//! in this crate (filesystem-only writes — no gix, no commit
7//! history).
8//!
9//! # Trait surface
10//!
11//! Four operations cover today's mutation surface:
12//! - [`MemWriter::write_entity`] — upsert raw bytes at a mem-relative path.
13//! - [`MemWriter::delete_entity`] — remove a mem-relative path.
14//! - [`MemWriter::move_entity`] — rename within the mem.
15//! - [`MemWriter::commit`] — flush pending mutations into a single commit.
16//!
17//! # Errors
18//!
19//! All four methods return [`MemWriterError`]. Engine-layer code in
20//! `memstead-git-branch` wraps this into its `EngineError::MemWriter` (a
21//! `#[from]` conversion) and the MCP layer wraps it as a
22//! `MEM_WRITER_ERROR`-coded envelope.
23
24pub mod archive;
25pub mod filesystem;
26pub mod in_memory;
27
28pub use archive::ArchiveBackend;
29pub use filesystem::FilesystemMemWriter;
30pub use in_memory::InMemoryBackend;
31
32use std::path::Path;
33
34use crate::vcs::CommitContext;
35
36/// Opaque commit identifier returned by [`MemWriter::commit`].
37/// Backend-defined string — the git-tree adapter formats it as a
38/// hex-encoded object id (40 chars for sha-1, 64 for sha-256), but the
39/// trait surface treats it as opaque. Callers carry it back into the
40/// engine's `HashMismatch.current` envelope when the adapter detects a
41/// CAS conflict.
42pub type CommitId = String;
43
44/// Write-side abstraction for mem content. Implementations are
45/// `Send + Sync` so the engine can hold a `Box<dyn MemWriter>` on
46/// each `MemState` and reach it from any caller.
47pub trait MemWriter: Send + Sync {
48    /// Upsert `content` at `rel_path` (mem-relative). Pending until
49    /// [`Self::commit`].
50    fn write_entity(&self, rel_path: &Path, content: &[u8]) -> Result<(), MemWriterError>;
51
52    /// Remove `rel_path` (mem-relative). Idempotent: no-op when the
53    /// path is already absent. Pending until [`Self::commit`].
54    fn delete_entity(&self, rel_path: &Path) -> Result<(), MemWriterError>;
55
56    /// Rename `from` to `to` (both mem-relative). Pending until
57    /// [`Self::commit`]. Errors if `to` already exists.
58    fn move_entity(&self, from: &Path, to: &Path) -> Result<(), MemWriterError>;
59
60    /// Flush pending mutations into a single commit. The implementation
61    /// picks up the actor / committer / trailer information from `ctx`.
62    /// Returns the resulting [`CommitId`] (opaque; the git-tree adapter
63    /// formats it as a hex object id).
64    fn commit(&self, message: &str, ctx: &CommitContext<'_>) -> Result<CommitId, MemWriterError>;
65}
66
67/// Errors surfaced by [`MemWriter`].
68#[derive(Debug, thiserror::Error)]
69pub enum MemWriterError {
70    #[error("mem writer io error: {0}")]
71    Io(#[from] std::io::Error),
72    /// Commit-time CAS conflict: the adapter snapshotted parent commit
73    /// `X` at write-time, but by commit-time the underlying store has
74    /// advanced to `current`. Surfaced by adapters that perform
75    /// commit-tip CAS (the git-tree adapter); mapped onto the engine's
76    /// `HashMismatch` envelope so MCP agents see a single
77    /// `HASH_MISMATCH` code regardless of whether the conflict was
78    /// detected at the entity-hash level or at the commit level.
79    #[error("mem writer cas conflict: current commit is now {current}")]
80    HashMismatch {
81        /// New commit identifier observed when the CAS check failed.
82        /// Opaque to base callers; the git-tree adapter populates it
83        /// with a hex commit object id.
84        current: CommitId,
85    },
86    /// Path-related rejection that does not map onto the IO case —
87    /// e.g. an empty relative path or a path that escapes the mem
88    /// root.
89    #[error("mem writer path error: {0}")]
90    Path(String),
91}