use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
use memstead_schema::SchemaRef;
use memstead_schema::workspace_config::CrossLinkValue;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Mount {
pub mem: String,
pub schema: Option<SchemaRef>,
pub storage: MountStorage,
pub capability: MountCapability,
pub lifecycle: MountLifecycle,
pub cross_linkable: bool,
pub migration_target: Option<SchemaRef>,
}
impl Mount {
pub fn mem_path(&self) -> Option<String> {
match &self.storage {
MountStorage::GitBranch { branch, .. } => {
let leaf = branch
.strip_prefix("refs/heads/")
.unwrap_or(branch.as_str());
let after_leaf = leaf.strip_suffix(&self.mem)?;
let trimmed = after_leaf.trim_end_matches('/');
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
MountStorage::Folder { .. } | MountStorage::Archive { .. } | MountStorage::InMemory => {
None
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MountStorage {
Folder {
path: PathBuf,
},
GitBranch {
gitdir: PathBuf,
branch: String,
},
Archive {
path: PathBuf,
},
InMemory,
}
impl MountStorage {
pub fn backend_id(&self) -> &'static str {
match self {
MountStorage::Folder { .. } => "folder",
MountStorage::GitBranch { .. } => "git-branch",
MountStorage::Archive { .. } => "archive",
MountStorage::InMemory => "in-memory",
}
}
pub fn is_durable(&self) -> bool {
match self {
MountStorage::Folder { .. }
| MountStorage::GitBranch { .. }
| MountStorage::Archive { .. } => true,
MountStorage::InMemory => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MountCapability {
ReadOnly,
Write,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MountLifecycle {
Eager,
Lazy,
}
#[derive(Debug, Clone, Default)]
pub struct Workspace {
pub mounts: Vec<Mount>,
pub settings: WorkspaceSettings,
}
impl Workspace {
pub fn empty() -> Self {
Self {
mounts: Vec::new(),
settings: WorkspaceSettings::default(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct WorkspaceSettings {
pub mem_create_rules: Vec<CreateRuleSetting>,
pub mem_delete_rules: Vec<DeleteRuleSetting>,
pub cross_mem_links: BTreeMap<String, CrossLinkValue>,
pub mcp: McpSection,
pub mutations: MutationsSection,
pub plugin: HashMap<String, toml::Table>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct McpSection {
pub token_budget: Option<usize>,
pub disabled_tools: Option<Vec<String>>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct MutationsSection {
pub require_notes: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateRuleSetting {
pub pattern: String,
pub schemas: Vec<String>,
pub default_cross_links: Option<CrossLinkValue>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteRuleSetting {
pub pattern: String,
}
pub const SCHEMA_WILDCARD: &str = "*";
#[cfg(test)]
mod tests {
use super::*;
fn pin(name: &str) -> SchemaRef {
SchemaRef::new(name, semver::Version::new(1, 0, 0))
}
#[test]
fn empty_workspace_has_no_mounts() {
let ws = Workspace::empty();
assert!(ws.mounts.is_empty());
}
#[test]
fn durability_follows_storage_kind() {
let folder = MountStorage::Folder {
path: PathBuf::from("/work/mem"),
};
let git = MountStorage::GitBranch {
gitdir: PathBuf::from("/work/mem-repo/.git"),
branch: "specs".into(),
};
let archive = MountStorage::Archive {
path: PathBuf::from("/work/curated.mem"),
};
let in_memory = MountStorage::InMemory;
assert!(folder.is_durable());
assert!(git.is_durable());
assert!(archive.is_durable());
assert!(!in_memory.is_durable());
assert_eq!(folder.backend_id(), "folder");
assert_eq!(git.backend_id(), "git-branch");
assert_eq!(archive.backend_id(), "archive");
assert_eq!(in_memory.backend_id(), "in-memory");
}
#[test]
fn mount_can_describe_folder_storage() {
let m = Mount {
mem: "specs".into(),
schema: Some(pin("default")),
storage: MountStorage::Folder {
path: PathBuf::from("/work/mem"),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
};
assert_eq!(m.mem, "specs");
assert!(matches!(m.storage, MountStorage::Folder { .. }));
}
#[test]
fn mount_can_describe_git_branch_storage() {
let m = Mount {
mem: "engine".into(),
schema: Some(pin("default")),
storage: MountStorage::GitBranch {
gitdir: PathBuf::from("/work/mem-repo/.git"),
branch: "engine".into(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
};
assert!(matches!(m.storage, MountStorage::GitBranch { .. }));
}
#[test]
fn mem_path_extracts_hierarchical_prefix_from_git_branch() {
let m = Mount {
mem: "engine".into(),
schema: Some(pin("default")),
storage: MountStorage::GitBranch {
gitdir: PathBuf::from("/work/mem-repo/.git"),
branch: "memstead/engine".into(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
};
assert_eq!(m.mem_path(), Some("memstead".to_string()));
let m = Mount {
mem: "plan-foo".into(),
schema: Some(pin("default")),
storage: MountStorage::GitBranch {
gitdir: PathBuf::from("/work/mem-repo/.git"),
branch: "refs/heads/planning/plan-foo".into(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
};
assert_eq!(m.mem_path(), Some("planning".to_string()));
let m = Mount {
mem: "leaf".into(),
schema: Some(pin("default")),
storage: MountStorage::GitBranch {
gitdir: PathBuf::from("/work/mem-repo/.git"),
branch: "refs/heads/a/b/c/leaf".into(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
};
assert_eq!(m.mem_path(), Some("a/b/c".to_string()));
let m = Mount {
mem: "engine".into(),
schema: Some(pin("default")),
storage: MountStorage::GitBranch {
gitdir: PathBuf::from("/work/mem-repo/.git"),
branch: "engine".into(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
};
assert_eq!(m.mem_path(), None);
let m = Mount {
mem: "engine".into(),
schema: Some(pin("default")),
storage: MountStorage::GitBranch {
gitdir: PathBuf::from("/work/mem-repo/.git"),
branch: "refs/heads/engine".into(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
};
assert_eq!(m.mem_path(), None);
let m = Mount {
mem: "engine".into(),
schema: Some(pin("default")),
storage: MountStorage::Folder {
path: PathBuf::from("/work/mem"),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
};
assert_eq!(m.mem_path(), None);
}
#[test]
fn mount_can_describe_archive_storage() {
let m = Mount {
mem: "external".into(),
schema: Some(pin("default")),
storage: MountStorage::Archive {
path: PathBuf::from("/deps/external.mem"),
},
capability: MountCapability::ReadOnly,
lifecycle: MountLifecycle::Lazy,
cross_linkable: false,
migration_target: None,
};
assert!(matches!(m.storage, MountStorage::Archive { .. }));
assert_eq!(m.capability, MountCapability::ReadOnly);
}
#[test]
fn workspace_with_heterogeneous_mounts() {
let ws = Workspace {
mounts: vec![
Mount {
mem: "engine".into(),
schema: Some(pin("default")),
storage: MountStorage::GitBranch {
gitdir: PathBuf::from("/work/mem-repo/.git"),
branch: "engine".into(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
},
Mount {
mem: "macos".into(),
schema: Some(pin("default")),
storage: MountStorage::GitBranch {
gitdir: PathBuf::from("/work/mem-repo/.git"),
branch: "macos".into(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
},
Mount {
mem: "external".into(),
schema: Some(pin("default")),
storage: MountStorage::Archive {
path: PathBuf::from("/deps/external.mem"),
},
capability: MountCapability::ReadOnly,
lifecycle: MountLifecycle::Lazy,
cross_linkable: false,
migration_target: None,
},
],
settings: WorkspaceSettings::default(),
};
assert_eq!(ws.mounts.len(), 3);
let shared_gitdir_mounts = ws
.mounts
.iter()
.filter(|m| matches!(&m.storage, MountStorage::GitBranch { gitdir, .. } if gitdir == std::path::Path::new("/work/mem-repo/.git")))
.count();
assert_eq!(shared_gitdir_mounts, 2);
}
}