#![forbid(unsafe_code)]
use wm_core::{CoreError, Galaxy, Resource};
use wm_memory::{Memory, SearchEngine};
#[must_use]
pub fn memory_galaxy_writes() -> Vec<Resource> {
Galaxy::memory_galaxies()
.iter()
.map(|g| Resource::Galaxy(g.db_name().to_string()))
.collect()
}
#[must_use]
pub fn memory_galaxy_reads() -> Vec<Resource> {
Galaxy::memory_galaxies()
.iter()
.map(|g| Resource::Galaxy(g.db_name().to_string()))
.collect()
}
#[must_use]
pub fn fresh_write_galaxies() -> Vec<Resource> {
Galaxy::memory_galaxies()
.iter()
.filter(|g| **g != Galaxy::Citta)
.map(|g| Resource::Galaxy(g.db_name().to_string()))
.collect()
}
#[must_use]
pub const fn mcp_visible(mem: &Memory) -> bool {
!mem.metadata.is_private
}
#[must_use]
pub const fn model_visible(mem: &Memory) -> bool {
!mem.metadata.model_exclude
}
#[must_use]
pub fn content_visible(ctx: &wm_core::Context, galaxy: wm_core::Galaxy, mem: &Memory) -> bool {
mcp_visible(mem) && model_visible(mem) && validity_visible(mem) && ctx.can_access_galaxy(galaxy)
}
#[must_use]
pub fn validity_visible(mem: &Memory) -> bool {
if wm_memory::memory::validity_enforced() {
mem.metadata.validity.is_current()
} else {
true
}
}
#[must_use]
pub fn schema(properties: &serde_json::Value, required: &[&str]) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": properties,
"required": required,
})
}
#[must_use]
pub fn str_prop(description: &str) -> serde_json::Value {
serde_json::json!({"type": "string", "description": description})
}
#[must_use]
pub fn num_prop(description: &str) -> serde_json::Value {
serde_json::json!({"type": "number", "description": description})
}
#[must_use]
pub fn bounded_num_prop(description: &str, lo: f64, hi: f64) -> serde_json::Value {
serde_json::json!({
"type": "number",
"minimum": lo,
"maximum": hi,
"description": description,
})
}
pub fn positive_usize_arg(
args: &serde_json::Value,
key: &str,
default: usize,
) -> Result<usize, String> {
match args.get(key) {
None | Some(serde_json::Value::Null) => Ok(default),
Some(serde_json::Value::Number(n)) => match n.as_u64() {
Some(v) if v >= 1 => Ok(v as usize),
_ => Err(format!("{key} must be an integer >= 1, got: {n}")),
},
Some(other) => Err(format!("{key} must be an integer >= 1, got: {other}")),
}
}
#[must_use]
pub fn positive_int_prop(description: &str) -> serde_json::Value {
serde_json::json!({"type": "integer", "minimum": 1, "description": description})
}
pub fn bounded_f64_arg(
args: &serde_json::Value,
key: &str,
lo: f64,
hi: Option<f64>,
) -> Result<Option<f64>, String> {
let bounds = hi.map_or_else(|| format!(">= {lo}"), |h| format!("in {lo}-{h}"));
match args.get(key) {
None | Some(serde_json::Value::Null) => Ok(None),
Some(serde_json::Value::Number(n)) => match n.as_f64() {
Some(v) if v.is_finite() && v >= lo && hi.is_none_or(|h| v <= h) => Ok(Some(v)),
Some(v) => Err(format!("{key} must be a number {bounds}, got: {v}")),
None => Err(format!("{key} must be a number {bounds}, got: {n}")),
},
Some(serde_json::Value::String(raw)) => match raw.trim().parse::<f64>() {
Ok(v) if v.is_finite() && v >= lo && hi.is_none_or(|h| v <= h) => Ok(Some(v)),
Ok(v) => Err(format!("{key} must be a number {bounds}, got: {v}")),
Err(_) => Err(format!("{key} must be a number {bounds}, got: \"{raw}\"")),
},
Some(other) => Err(format!("{key} must be a number {bounds}, got: {other}")),
}
}
#[must_use]
pub fn int_prop(description: &str) -> serde_json::Value {
serde_json::json!({"type": "integer", "description": description})
}
#[must_use]
pub fn canonical_tool_alias(name: &str) -> Option<&'static str> {
match name {
"memory.find" | "memory_find" | "memory_search" => Some("memory.search"),
"memory_create" => Some("memory.create"),
"memory_read" => Some("memory.read"),
"memory_list" => Some("memory.list"),
"memory_hybrid_recall" => Some("memory.hybrid_recall"),
"session_start" => Some("session.start"),
"session_record" => Some("session.record"),
"session_continuity" => Some("session.continuity"),
_ => None,
}
}
#[must_use]
pub fn bool_prop(description: &str) -> serde_json::Value {
serde_json::json!({"type": "boolean", "description": description})
}
#[must_use]
pub fn str_array_prop(description: &str) -> serde_json::Value {
serde_json::json!({"type": "array", "items": {"type": "string"}, "description": description})
}
pub fn deindex(search: Option<&SearchEngine>, id_str: &str) {
let Some(search) = search else { return };
if let Err(e) = (|| {
let mut writer = search.writer()?;
search.delete_document(&mut writer, id_str)?;
search.commit(&mut writer)?;
Ok::<(), wm_core::CoreError>(())
})() {
tracing::warn!("Tantivy de-indexing failed for memory {id_str}: {e}");
}
}
pub fn index_memory(search: Option<&SearchEngine>, mem: &wm_memory::Memory) {
let Some(search) = search else { return };
let id_str = mem.metadata.id.to_string();
let galaxy_str = mem.metadata.galaxy.db_name().to_string();
if let Err(e) = (|| {
let mut writer = search.writer()?;
search.add_document(
&mut writer,
&id_str,
&galaxy_str,
&mem.content,
&mem.metadata.tags,
mem.metadata.created_at.timestamp(),
)?;
search.commit(&mut writer)?;
Ok::<(), wm_core::CoreError>(())
})() {
tracing::warn!("Tantivy indexing failed for memory {id_str}: {e}");
}
}
pub fn parse_galaxy(s: &str) -> wm_core::Result<Galaxy> {
match s.to_lowercase().as_str() {
"aria" => Ok(Galaxy::Aria),
"citta" => Ok(Galaxy::Citta),
"codex" => Ok(Galaxy::Codex),
"journals" => Ok(Galaxy::Journals),
"dreams" => Ok(Galaxy::Dreams),
"research" => Ok(Galaxy::Research),
"sessions" => Ok(Galaxy::Sessions),
"substrate" => Ok(Galaxy::Substrate),
"tutorial" => Ok(Galaxy::Tutorial),
"universal" => Ok(Galaxy::Universal),
"karma" => Ok(Galaxy::Karma),
"dharma" => Ok(Galaxy::Dharma),
"associations" => Ok(Galaxy::Associations),
"embeddings" => Ok(Galaxy::Embeddings),
"valkyrie" => Ok(Galaxy::Valkyrie),
"telemetry" => Ok(Galaxy::Telemetry),
other => Err(CoreError::InvalidArgs(format!(
"Unknown galaxy: '{other}'. Valid galaxies: aria, citta, codex, journals, dreams, research, sessions, substrate, tutorial, universal, karma, dharma, associations, embeddings, valkyrie, telemetry"
))),
}
}
pub fn parse_galaxy_or(s: Option<&str>, default: Galaxy) -> wm_core::Result<Galaxy> {
match s {
None | Some("") => Ok(default),
Some(s) => parse_galaxy(s),
}
}
#[must_use]
pub fn galaxy_search_arg(s: Option<&str>) -> Option<&str> {
match s {
None => None,
Some(s) if s.is_empty() || s.eq_ignore_ascii_case("all") => None,
Some(s) => Some(s),
}
}
#[must_use]
pub const fn galaxy_name(g: Galaxy) -> &'static str {
g.db_name()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validity_visible_defaults_true_even_when_stamped() {
let mut mem = Memory::new(Galaxy::Codex, "old claim".into());
assert!(validity_visible(&mem));
let replacement = uuid::Uuid::new_v4();
mem.transition_validity(wm_core::episodic::MemoryTransition::Supersede { replacement })
.unwrap();
assert!(!mem.metadata.validity.is_current());
assert!(validity_visible(&mem));
}
}