use std::cell::OnceCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use memstead_schema::Schema;
use crate::backend::{BackendError, MemBackend};
use crate::graph::LouvainOutput;
use crate::mem::MemRouterSnapshot;
use crate::ops::WarningHint;
#[cfg(not(target_arch = "wasm32"))]
use crate::search_index::MemIndex;
use crate::store::Store;
use crate::workspace::{Mount, WorkspaceSettings};
pub mod apply_commit;
pub mod archive;
pub mod boot;
pub mod check_ops;
pub mod drift;
pub mod due;
pub mod error;
pub mod events;
pub mod export_html;
#[cfg(feature = "file-watcher")]
pub mod file_watcher;
pub mod history;
pub mod lifecycle;
pub mod mutation;
pub mod outcomes;
pub mod query;
pub mod review;
pub use archive::FromArchiveBytesError;
pub use error::{
BootError, EngineError, INLINE_LIST_CAP, MissingWikiLink, ReferrerInfo, SchemaSourceDiagnostic,
format_inline_list_overflow,
};
#[cfg(feature = "tokio")]
pub use events::DEFAULT_BROADCAST_CAPACITY;
pub use events::{EventCallback, MemChangedEvent, SubscriptionHandle};
#[cfg(feature = "file-watcher")]
pub use file_watcher::{FileWatcherError, MemRepoWatcher, watch_mem_repo};
pub use history::{
EntityHistoryReport, EntityTouch, HISTORY_PAGE_DEFAULT, HISTORY_PAGE_MAX, StoryStart,
};
pub use mutation::delete::DeleteReferrers;
pub use mutation::{PATCH_OLD_NOT_FOUND_CONTENT_CAP, RELATIONSHIP_CYCLE_PATH_CAP};
pub use outcomes::{
CreateEntityArgs, CreateEntityOutcome, DeleteEntityArgs, DeleteEntityOutcome, RelateAction,
RelateEntityArgs, RelateEntityOutcome, RenameEntityArgs, RenameEntityOutcome, SetSchemaOutcome,
SetSchemaResult, UpdateEntityArgs, UpdateEntityOutcome,
};
pub use review::{ReviewMarkStatus, SetReviewMarkOutcome};
pub use boot::{SchemaResolver, load_workspace_schemas, resolve_builtin_schema_pin_pub};
pub use lifecycle::SchemaStaging;
pub(crate) struct MountedBackend {
mount: Mount,
backend: Box<dyn MemBackend>,
last_known_head: Option<String>,
mem_config: Option<memstead_schema::config::MemConfig>,
archive_provenance: Option<memstead_schema::ArchiveProvenance>,
}
#[derive(Debug, Clone)]
pub struct QuarantinedMem {
pub mount: crate::workspace::Mount,
pub reason_code: String,
pub reason_message: String,
}
pub struct Engine {
mounts: Vec<MountedBackend>,
store: Store,
schemas: HashMap<String, Arc<Schema>>,
workspace_schemas: Vec<Arc<Schema>>,
builtin_schemas: Vec<Arc<Schema>>,
load_errors: Vec<(PathBuf, String)>,
community_memo: OnceCell<LouvainOutput>,
#[cfg(not(target_arch = "wasm32"))]
search_indexes_memo: OnceCell<HashMap<String, MemIndex>>,
settings: WorkspaceSettings,
create_rule_set_memo: OnceCell<crate::mem_management::CreateRuleSet>,
declared_origins: HashMap<String, crate::render::OriginClass>,
workspace_root: Option<PathBuf>,
load_warnings: Vec<WarningHint>,
quarantined: Vec<QuarantinedMem>,
boot_diagnosis: Option<(String, String)>,
pipeline_configs: crate::pipeline_store::BindingConfigs,
mem_router: Arc<MemRouterSnapshot>,
backend_factory: BackendFactory,
git_branch_ops: Option<GitBranchOps>,
event_subscribers: Arc<std::sync::Mutex<events::SubscriberRegistry>>,
pending_mem_changed: Vec<crate::ops::MemChangedNotice>,
mutation_clock: MutationClock,
current_role: crate::vcs::Role,
}
pub type MutationClock = Arc<dyn Fn() -> std::time::SystemTime + Send + Sync>;
pub type BackendFactory =
fn(&Mount) -> Result<Box<dyn MemBackend>, crate::workspace_store::InstantiateError>;
pub type GitBranchChangesSinceFn = fn(
gitdir: &Path,
branch: &str,
mem: &str,
since: &str,
rename_similarity: f32,
) -> Result<crate::ops::BackendChanges, BackendError>;
pub type GitBranchExportFn = fn(
gitdir: &Path,
branch: &str,
mem: &str,
config: &memstead_schema::MemConfig,
output_path: &Path,
workspace_root: Option<&Path>,
workspace_schemas_dir: Option<&Path>,
provenance_bytes: Option<&[u8]>,
anchors_bytes: Option<&[u8]>,
) -> Result<crate::ops::MemExportResult, BackendError>;
pub type GitBranchExportToBytesFn = fn(
gitdir: &Path,
branch: &str,
mem: &str,
config: &memstead_schema::MemConfig,
workspace_root: Option<&Path>,
workspace_schemas_dir: Option<&Path>,
provenance_bytes: Option<&[u8]>,
anchors_bytes: Option<&[u8]>,
) -> Result<crate::ops::MemExportBytes, BackendError>;
pub type GitBranchDiffFn = fn(
gitdir: &Path,
mem: &str,
ref_a: &str,
ref_b: &str,
config: &crate::ops::DiffConfig,
) -> Result<crate::ops::Diff, BackendError>;
pub type GitBranchFetchFn = fn(
gitdir: &Path,
remote: &str,
refspecs: &[String],
) -> Result<crate::ops::FetchOutcome, BackendError>;
pub type GitBranchReadTreeFn =
fn(gitdir: &Path, ref_name: &str) -> Result<Vec<(String, String)>, BackendError>;
pub type GitBranchPullFn =
fn(gitdir: &Path, remote: &str, mem: &str) -> Result<crate::ops::PullOutcome, BackendError>;
pub type GitBranchPushFn = fn(
gitdir: &Path,
remote: &str,
mem: &str,
force: bool,
) -> Result<crate::ops::PushOutcome, BackendError>;
pub type GitBranchRemoteAddFn =
fn(gitdir: &Path, name: &str, url: &str) -> Result<crate::ops::RemoteAddOutcome, BackendError>;
pub type GitBranchBranchResetFn = fn(
gitdir: &Path,
branch: &str,
target_sha: &str,
expected_head: Option<&str>,
) -> Result<crate::ops::BranchResetOutcome, BackendError>;
pub type GitBranchPruneResidueFn =
fn(gitdir: &Path, branch_full_path: &str) -> Result<(), BackendError>;
pub type GitBranchRenameMemStorageFn =
fn(gitdir: &Path, old_leaf: &str, new_leaf: &str) -> Result<(), BackendError>;
pub type GitBranchWriteSchemaFn = fn(
gitdir: &Path,
name: &str,
version: &str,
files: &[(String, Vec<u8>)],
) -> Result<String, BackendError>;
pub type GitBranchReadSchemaFileFn = fn(
gitdir: &Path,
name: &str,
version: &str,
rel: &str,
) -> Result<Option<Vec<u8>>, BackendError>;
pub type GitBranchReadRefSchemasFn =
fn(workspace_root: &Path) -> Result<Vec<Arc<memstead_schema::Schema>>, BackendError>;
#[derive(Clone, Copy)]
pub struct GitBranchOps {
pub changes_since: GitBranchChangesSinceFn,
pub diff: GitBranchDiffFn,
pub branch_reset: GitBranchBranchResetFn,
pub fetch: GitBranchFetchFn,
pub pull: GitBranchPullFn,
pub push: GitBranchPushFn,
pub remote_add: GitBranchRemoteAddFn,
pub read_tree: GitBranchReadTreeFn,
pub export: GitBranchExportFn,
pub export_to_bytes: GitBranchExportToBytesFn,
pub prune_residue: GitBranchPruneResidueFn,
pub rename_mem_storage: GitBranchRenameMemStorageFn,
pub write_schema: GitBranchWriteSchemaFn,
pub read_schema_file: GitBranchReadSchemaFileFn,
pub read_ref_schemas: GitBranchReadRefSchemasFn,
}
impl std::fmt::Debug for Engine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Engine")
.field(
"mems",
&self
.mounts
.iter()
.map(|m| m.mount.mem.as_str())
.collect::<Vec<_>>(),
)
.finish()
}
}
#[cfg(test)]
mod in_memory_mem;
#[cfg(test)]
pub(super) mod test_helpers {
use std::io::Write as _;
use std::path::{Path, PathBuf};
use memstead_schema::SchemaRef;
use crate::backend::MemBackend;
use crate::storage::FilesystemMemWriter;
use crate::vcs::{Actor, ClientId};
use crate::workspace::{Mount, MountCapability, MountLifecycle, MountStorage};
use super::{CreateEntityArgs, CreateEntityOutcome, Engine, RelateEntityArgs};
use indexmap::IndexMap;
use tempfile::TempDir;
pub(crate) fn pin(name: &str) -> SchemaRef {
let version = match name {
"default" => semver::Version::new(1, 0, 0),
_ => semver::Version::new(0, 1, 0),
};
SchemaRef::new(name, version)
}
pub(crate) fn folder_mount(mem: &str, path: PathBuf) -> Mount {
Mount {
mem: mem.to_string(),
schema: Some(pin("default")),
storage: MountStorage::Folder { path },
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
}
}
pub(crate) fn in_memory_mount(mem: &str) -> Mount {
Mount {
mem: mem.to_string(),
schema: Some(pin("default")),
storage: MountStorage::InMemory,
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: true,
migration_target: None,
}
}
pub(crate) fn archive_mount(mem: &str, path: PathBuf) -> Mount {
Mount {
mem: mem.to_string(),
schema: Some(pin("default")),
storage: MountStorage::Archive { path },
capability: MountCapability::ReadOnly,
lifecycle: MountLifecycle::Lazy,
cross_linkable: false,
migration_target: None,
}
}
pub(crate) fn build_archive(tmp: &Path, name: &str, entries: &[(&str, &[u8])]) -> PathBuf {
let path = tmp.join(format!("{name}.mem"));
let file = std::fs::File::create(&path).unwrap();
let mut writer = zip::ZipWriter::new(file);
let opts = zip::write::SimpleFileOptions::default();
for (rel, bytes) in entries {
writer.start_file(*rel, opts).unwrap();
writer.write_all(bytes).unwrap();
}
writer.finish().unwrap();
path
}
pub(crate) fn write_schema_files_with_default_type(
root: &Path,
name: &str,
manifest: &str,
types: &[&str],
) {
const TYPE_BODY: &str = r#"description: t
when_to_use: Here
sections:
- key: body
heading: Body
required: true
search_weight: 10.0
catch_all: true
write_rules: []
metadata_fields: []
title_weight: 100.0
text_fields:
- body
hierarchy_relationship: _default
no_self_loop_relationships: []
updatable_fields:
- title
- body
health_required_fields:
- body
staleness_threshold_days: 90
write_rules: []
"#;
let dir = root.join(name);
std::fs::create_dir_all(dir.join("types")).unwrap();
std::fs::write(dir.join("schema.yaml"), manifest).unwrap();
for type_name in types {
let body = format!("name: {type_name}\n{TYPE_BODY}");
std::fs::write(dir.join("types").join(format!("{type_name}.yaml")), body).unwrap();
}
}
pub(crate) fn empty_create_args(mem: &str, title: &str) -> CreateEntityArgs {
let mut sections = IndexMap::new();
sections.insert("identity".to_string(), "fixture identity body".to_string());
sections.insert("purpose".to_string(), "fixture purpose body".to_string());
CreateEntityArgs {
anchors: Vec::new(),
mem: mem.to_string(),
title: title.to_string(),
entity_type: "spec".to_string(),
sections,
metadata: IndexMap::new(),
relations: Vec::new(),
dry_run: false,
}
}
pub(crate) fn cli_actor() -> (Actor, ClientId) {
(
Actor::Cli,
ClientId {
name: "claude-code".to_string(),
version: "2.1.0".to_string(),
},
)
}
pub(crate) fn engine_with_seed(tmp: &TempDir, title: &str) -> (Engine, CreateEntityOutcome) {
let mem_dir = tmp.path().to_path_buf();
let writer = FilesystemMemWriter::new(mem_dir.clone());
let mut engine = Engine::from_mounts(vec![(
folder_mount("specs", mem_dir),
Box::new(writer) as Box<dyn MemBackend>,
)])
.unwrap();
let (actor, client) = cli_actor();
let outcome = engine
.create_entity(
empty_create_args("specs", title),
actor,
Some(&client),
None,
)
.unwrap();
(engine, outcome)
}
pub(crate) fn build_demo_engine(tmp: &TempDir) -> Engine {
let mem_dir = tmp.path().to_path_buf();
let writer = FilesystemMemWriter::new(mem_dir.clone());
let mut engine = Engine::from_mounts(vec![(
folder_mount("specs", mem_dir),
Box::new(writer) as Box<dyn MemBackend>,
)])
.unwrap();
let (actor, client) = cli_actor();
let source = engine
.create_entity(
empty_create_args("specs", "Source One"),
actor,
Some(&client),
None,
)
.unwrap();
let target = engine
.create_entity(
empty_create_args("specs", "Target Two"),
actor,
Some(&client),
None,
)
.unwrap();
engine
.create_entity(
empty_create_args("specs", "Lonely Three"),
actor,
Some(&client),
None,
)
.unwrap();
engine
.relate_entity(
RelateEntityArgs {
source: source.id.clone(),
expected_hash: Some(source.content_hash.clone()),
rel_type: "USES".to_string(),
target: target.id.clone(),
remove: false,
description: None,
dry_run: false,
},
actor,
Some(&client),
None,
)
.unwrap();
engine
}
}