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}