use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Family {
Core,
Lifecycle,
Graph,
Governance,
Power,
Meta,
Archive,
Other,
}
pub const ALWAYS_ON_TOOLS: &[&str] = &[crate::mcp::registry::tool_names::MEMORY_CAPABILITIES];
impl Family {
#[must_use]
pub fn for_tool(name: &str) -> Option<Self> {
use crate::mcp::registry::tool_names as tn;
match name {
tn::MEMORY_STORE | tn::MEMORY_RECALL | tn::MEMORY_LIST | tn::MEMORY_GET
| tn::MEMORY_SEARCH | tn::MEMORY_LOAD_FAMILY | tn::MEMORY_SMART_LOAD => {
Some(Self::Core)
}
tn::MEMORY_UPDATE | tn::MEMORY_DELETE | tn::MEMORY_FORGET | tn::MEMORY_GC
| tn::MEMORY_PROMOTE | tn::MEMORY_CAPTURE_TURN => Some(Self::Lifecycle),
tn::MEMORY_KG_QUERY
| tn::MEMORY_KG_TIMELINE
| tn::MEMORY_KG_INVALIDATE
| tn::MEMORY_LINK
| tn::MEMORY_GET_LINKS
| tn::MEMORY_ENTITY_REGISTER
| tn::MEMORY_ENTITY_GET_BY_ALIAS
| tn::MEMORY_GET_TAXONOMY
| tn::MEMORY_REPLAY
| tn::MEMORY_VERIFY
| tn::MEMORY_FIND_PATHS => Some(Self::Graph),
tn::MEMORY_PENDING_LIST
| tn::MEMORY_PENDING_APPROVE
| tn::MEMORY_PENDING_REJECT
| tn::MEMORY_NAMESPACE_SET_STANDARD
| tn::MEMORY_NAMESPACE_GET_STANDARD
| tn::MEMORY_NAMESPACE_CLEAR_STANDARD
| tn::MEMORY_SUBSCRIBE
| tn::MEMORY_UNSUBSCRIBE => Some(Self::Governance),
tn::MEMORY_CONSOLIDATE
| tn::MEMORY_DETECT_CONTRADICTION
| tn::MEMORY_CHECK_DUPLICATE
| tn::MEMORY_AUTO_TAG
| tn::MEMORY_EXPAND_QUERY
| tn::MEMORY_INBOX
| tn::MEMORY_SUBSCRIPTION_REPLAY
| tn::MEMORY_SUBSCRIPTION_DLQ_LIST
| tn::MEMORY_QUOTA_STATUS
| tn::MEMORY_REFLECT
| tn::MEMORY_REFLECTION_ORIGIN
| tn::MEMORY_EXPORT_REFLECTION
| tn::MEMORY_PERSONA
| tn::MEMORY_PERSONA_GENERATE
| tn::MEMORY_CALIBRATE_CONFIDENCE
| tn::MEMORY_DEPENDENTS_OF_INVALIDATED
| tn::MEMORY_CHECK_AGENT_ACTION
| tn::MEMORY_RULE_LIST
| tn::MEMORY_OFFLOAD
| tn::MEMORY_DEREF
| tn::MEMORY_ATOMISE
| tn::MEMORY_INGEST_MULTISTEP
| tn::MEMORY_SHARE => Some(Self::Power),
tn::MEMORY_CAPABILITIES
| tn::MEMORY_AGENT_REGISTER
| tn::MEMORY_AGENT_LIST
| tn::MEMORY_SESSION_START
| tn::MEMORY_STATS
| tn::MEMORY_RECALL_OBSERVATIONS => Some(Self::Meta),
tn::MEMORY_ARCHIVE_LIST
| tn::MEMORY_ARCHIVE_PURGE
| tn::MEMORY_ARCHIVE_RESTORE
| tn::MEMORY_ARCHIVE_STATS => Some(Self::Archive),
tn::MEMORY_LIST_SUBSCRIPTIONS
| tn::MEMORY_NOTIFY
| tn::MEMORY_SKILL_REGISTER
| tn::MEMORY_SKILL_LIST
| tn::MEMORY_SKILL_GET
| tn::MEMORY_SKILL_RESOURCE
| tn::MEMORY_SKILL_EXPORT
| tn::MEMORY_SKILL_PROMOTE_FROM_REFLECTION
| tn::MEMORY_SKILL_COMPOSITIONAL_CONTEXT => Some(Self::Other),
_ => None,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Core => "core",
Self::Lifecycle => "lifecycle",
Self::Graph => "graph",
Self::Governance => crate::models::field_names::GOVERNANCE,
Self::Power => "power",
Self::Meta => "meta",
Self::Archive => "archive",
Self::Other => "other",
}
}
#[must_use]
pub const fn all() -> &'static [Family] {
&[
Self::Core,
Self::Lifecycle,
Self::Graph,
Self::Governance,
Self::Power,
Self::Meta,
Self::Archive,
Self::Other,
]
}
#[must_use]
pub const fn expected_tool_count(self) -> usize {
self.tool_names().len()
}
#[must_use]
pub const fn tool_names(self) -> &'static [&'static str] {
use crate::mcp::registry::tool_names as tn;
match self {
Self::Core => &[
tn::MEMORY_STORE,
tn::MEMORY_RECALL,
tn::MEMORY_LIST,
tn::MEMORY_GET,
tn::MEMORY_SEARCH,
tn::MEMORY_LOAD_FAMILY,
tn::MEMORY_SMART_LOAD,
],
Self::Lifecycle => &[
tn::MEMORY_UPDATE,
tn::MEMORY_DELETE,
tn::MEMORY_FORGET,
tn::MEMORY_GC,
tn::MEMORY_PROMOTE,
tn::MEMORY_CAPTURE_TURN,
],
Self::Graph => &[
tn::MEMORY_KG_QUERY,
tn::MEMORY_KG_TIMELINE,
tn::MEMORY_KG_INVALIDATE,
tn::MEMORY_LINK,
tn::MEMORY_GET_LINKS,
tn::MEMORY_ENTITY_REGISTER,
tn::MEMORY_ENTITY_GET_BY_ALIAS,
tn::MEMORY_GET_TAXONOMY,
tn::MEMORY_REPLAY,
tn::MEMORY_VERIFY,
tn::MEMORY_FIND_PATHS,
],
Self::Governance => &[
tn::MEMORY_PENDING_LIST,
tn::MEMORY_PENDING_APPROVE,
tn::MEMORY_PENDING_REJECT,
tn::MEMORY_NAMESPACE_SET_STANDARD,
tn::MEMORY_NAMESPACE_GET_STANDARD,
tn::MEMORY_NAMESPACE_CLEAR_STANDARD,
tn::MEMORY_SUBSCRIBE,
tn::MEMORY_UNSUBSCRIBE,
],
Self::Power => &[
tn::MEMORY_CONSOLIDATE,
tn::MEMORY_DETECT_CONTRADICTION,
tn::MEMORY_CHECK_DUPLICATE,
tn::MEMORY_AUTO_TAG,
tn::MEMORY_EXPAND_QUERY,
tn::MEMORY_INBOX,
tn::MEMORY_SUBSCRIPTION_REPLAY,
tn::MEMORY_SUBSCRIPTION_DLQ_LIST,
tn::MEMORY_QUOTA_STATUS,
tn::MEMORY_REFLECT,
tn::MEMORY_REFLECTION_ORIGIN,
tn::MEMORY_DEPENDENTS_OF_INVALIDATED,
tn::MEMORY_CHECK_AGENT_ACTION,
tn::MEMORY_RULE_LIST,
tn::MEMORY_EXPORT_REFLECTION,
tn::MEMORY_OFFLOAD,
tn::MEMORY_DEREF,
tn::MEMORY_ATOMISE,
tn::MEMORY_PERSONA,
tn::MEMORY_PERSONA_GENERATE,
tn::MEMORY_INGEST_MULTISTEP,
tn::MEMORY_CALIBRATE_CONFIDENCE,
tn::MEMORY_SHARE,
],
Self::Meta => &[
tn::MEMORY_CAPABILITIES,
tn::MEMORY_AGENT_REGISTER,
tn::MEMORY_AGENT_LIST,
tn::MEMORY_SESSION_START,
tn::MEMORY_STATS,
tn::MEMORY_RECALL_OBSERVATIONS,
],
Self::Archive => &[
tn::MEMORY_ARCHIVE_LIST,
tn::MEMORY_ARCHIVE_PURGE,
tn::MEMORY_ARCHIVE_RESTORE,
tn::MEMORY_ARCHIVE_STATS,
],
Self::Other => &[
tn::MEMORY_LIST_SUBSCRIPTIONS,
tn::MEMORY_NOTIFY,
tn::MEMORY_SKILL_REGISTER,
tn::MEMORY_SKILL_LIST,
tn::MEMORY_SKILL_GET,
tn::MEMORY_SKILL_RESOURCE,
tn::MEMORY_SKILL_EXPORT,
tn::MEMORY_SKILL_PROMOTE_FROM_REFLECTION,
tn::MEMORY_SKILL_COMPOSITIONAL_CONTEXT,
],
}
}
}
impl FromStr for Family {
type Err = ProfileParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.chars().any(|c| c.is_ascii_uppercase()) {
return Err(ProfileParseError::CaseMismatch(s.to_string()));
}
match s {
"core" => Ok(Self::Core),
"lifecycle" => Ok(Self::Lifecycle),
"graph" => Ok(Self::Graph),
crate::models::field_names::GOVERNANCE => Ok(Self::Governance),
"power" => Ok(Self::Power),
"meta" => Ok(Self::Meta),
"archive" => Ok(Self::Archive),
"other" => Ok(Self::Other),
unknown => Err(ProfileParseError::UnknownFamily(unknown.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Profile {
families: Vec<Family>,
}
impl Profile {
#[must_use]
pub fn core() -> Self {
Self {
families: vec![Family::Core],
}
}
#[must_use]
pub fn graph() -> Self {
Self {
families: vec![Family::Core, Family::Graph],
}
}
#[must_use]
pub fn admin() -> Self {
Self {
families: vec![Family::Core, Family::Lifecycle, Family::Governance],
}
}
#[must_use]
pub fn power() -> Self {
Self {
families: vec![Family::Core, Family::Power],
}
}
#[must_use]
pub fn full() -> Self {
Self {
families: Family::all().to_vec(),
}
}
#[must_use]
pub fn families(&self) -> &[Family] {
&self.families
}
#[must_use]
pub fn includes(&self, family: Family) -> bool {
self.families.contains(&family)
}
#[must_use]
pub fn expected_tool_count(&self) -> usize {
self.families.iter().map(|f| f.expected_tool_count()).sum()
}
#[must_use]
pub fn loads(&self, tool_name: &str) -> bool {
if ALWAYS_ON_TOOLS.contains(&tool_name) {
return true;
}
Family::for_tool(tool_name).is_some_and(|f| self.includes(f))
}
pub fn parse(s: &str) -> Result<Self, ProfileParseError> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Ok(Self::core());
}
if trimmed.chars().any(|c| c.is_ascii_uppercase()) {
return Err(ProfileParseError::CaseMismatch(trimmed.to_string()));
}
match trimmed {
"core" => return Ok(Self::core()),
"graph" => return Ok(Self::graph()),
"admin" => return Ok(Self::admin()),
"power" => return Ok(Self::power()),
"full" => return Ok(Self::full()),
_ => {}
}
let mut families = Vec::with_capacity(8);
for raw_token in trimmed.split(',') {
let token = raw_token.trim();
if token.is_empty() {
continue;
}
match token {
"core" => merge(&mut families, Self::core().families()),
"graph" => merge(&mut families, Self::graph().families()),
"admin" => merge(&mut families, Self::admin().families()),
"power" => merge(&mut families, Self::power().families()),
"full" => return Ok(Self::full()),
_ => {
let f = Family::from_str(token)?;
if !families.contains(&f) {
families.push(f);
}
}
}
}
if !families.contains(&Family::Core) {
families.insert(0, Family::Core);
}
families.sort_unstable();
families.dedup();
Ok(Self { families })
}
}
impl Default for Profile {
fn default() -> Self {
Self::core()
}
}
fn merge(dst: &mut Vec<Family>, src: &[Family]) {
for f in src {
if !dst.contains(f) {
dst.push(*f);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProfileParseError {
UnknownFamily(String),
CaseMismatch(String),
}
impl std::fmt::Display for ProfileParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownFamily(name) => {
let valid: Vec<&str> = Family::all().iter().map(|f| f.name()).collect();
let profiles = "core, graph, admin, power, full";
write!(
f,
"unknown profile or family '{name}'. \
Valid profiles: {profiles}. \
Valid families: {valid}.",
valid = valid.join(", ")
)
}
Self::CaseMismatch(s) => {
write!(
f,
"profile '{s}' contains uppercase letters; \
profile vocabulary is case-sensitive lowercase \
(e.g. 'core', not 'Core')"
)
}
}
}
}
impl std::error::Error for ProfileParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn family_all_has_eight_entries() {
assert_eq!(Family::all().len(), 8);
}
#[test]
fn family_tool_names_cover_registry_all() {
let family_total: usize = Family::all().iter().map(|f| f.tool_names().len()).sum();
assert_eq!(
family_total,
crate::mcp::registry::tool_names::ALL.len(),
"per-family tool_names slices must cover exactly the registry ALL set; \
a tool was added to one side but not the other"
);
}
#[test]
fn family_from_str_lowercase_canonical() {
assert_eq!(Family::from_str("core").unwrap(), Family::Core);
assert_eq!(Family::from_str("meta").unwrap(), Family::Meta);
assert_eq!(Family::from_str("graph").unwrap(), Family::Graph);
}
#[test]
fn family_from_str_rejects_mixed_case() {
assert!(matches!(
Family::from_str("Core"),
Err(ProfileParseError::CaseMismatch(_))
));
assert!(matches!(
Family::from_str("CORE"),
Err(ProfileParseError::CaseMismatch(_))
));
}
#[test]
fn family_from_str_unknown_returns_diagnostic() {
let err = Family::from_str("xyz").unwrap_err();
match err {
ProfileParseError::UnknownFamily(s) => assert_eq!(s, "xyz"),
_ => panic!("expected UnknownFamily, got {err:?}"),
}
}
#[test]
fn profile_core_loads_only_core_family() {
let p = Profile::core();
assert_eq!(p.expected_tool_count(), Family::Core.tool_names().len());
assert!(p.includes(Family::Core));
assert!(!p.includes(Family::Meta));
assert!(!p.includes(Family::Lifecycle));
}
#[test]
fn profile_graph_loads_core_plus_graph() {
let p = Profile::graph();
assert_eq!(
p.expected_tool_count(),
Family::Core.tool_names().len() + Family::Graph.tool_names().len()
);
assert!(p.includes(Family::Graph));
}
#[test]
fn profile_admin_loads_core_lifecycle_governance() {
let p = Profile::admin();
assert_eq!(
p.expected_tool_count(),
Family::Core.tool_names().len()
+ Family::Lifecycle.tool_names().len()
+ Family::Governance.tool_names().len()
);
}
#[test]
fn profile_power_loads_core_plus_power() {
let p = Profile::power();
assert_eq!(
p.expected_tool_count(),
Family::Core.tool_names().len() + Family::Power.tool_names().len()
);
}
#[test]
fn profile_full_matches_registry_all() {
let p = Profile::full();
assert_eq!(
p.expected_tool_count(),
crate::mcp::registry::tool_names::ALL.len()
);
assert_eq!(
Profile::power().expected_tool_count(),
Family::Core.tool_names().len() + Family::Power.tool_names().len()
);
}
#[test]
fn parse_empty_returns_core() {
assert_eq!(Profile::parse("").unwrap(), Profile::core());
assert_eq!(Profile::parse(" ").unwrap(), Profile::core());
}
#[test]
fn parse_named_profiles() {
assert_eq!(Profile::parse("core").unwrap(), Profile::core());
assert_eq!(Profile::parse("graph").unwrap(), Profile::graph());
assert_eq!(Profile::parse("admin").unwrap(), Profile::admin());
assert_eq!(Profile::parse("power").unwrap(), Profile::power());
assert_eq!(Profile::parse("full").unwrap(), Profile::full());
}
#[test]
fn parse_custom_comma_list_dedup() {
let p = Profile::parse("core,graph").unwrap();
assert!(p.includes(Family::Core));
assert!(!p.includes(Family::Meta));
assert!(p.includes(Family::Graph));
assert_eq!(
p.expected_tool_count(),
Family::Core.tool_names().len() + Family::Graph.tool_names().len()
);
}
#[test]
fn parse_custom_dedupes_repeated_token() {
let p = Profile::parse("core,core").unwrap();
assert_eq!(p, Profile::core());
}
#[test]
fn parse_custom_with_full_subsumes() {
let p = Profile::parse("graph,full").unwrap();
assert_eq!(p, Profile::full());
}
#[test]
fn parse_custom_implicitly_includes_core() {
let p = Profile::parse("archive").unwrap();
assert!(p.includes(Family::Core));
assert!(p.includes(Family::Archive));
}
#[test]
fn parse_custom_unknown_family_errors() {
let err = Profile::parse("core,xyz").unwrap_err();
match err {
ProfileParseError::UnknownFamily(s) => assert_eq!(s, "xyz"),
_ => panic!("expected UnknownFamily, got {err:?}"),
}
}
#[test]
fn parse_rejects_mixed_case() {
assert!(matches!(
Profile::parse("Core"),
Err(ProfileParseError::CaseMismatch(_))
));
assert!(matches!(
Profile::parse("core,Graph"),
Err(ProfileParseError::CaseMismatch(_))
));
}
#[test]
fn parse_skips_whitespace_only_tokens() {
let p = Profile::parse("core, ,graph").unwrap();
assert_eq!(p, Profile::graph());
}
#[test]
fn parse_order_independence() {
let a = Profile::parse("core,graph").unwrap();
let b = Profile::parse("graph,core").unwrap();
assert_eq!(a, b);
}
#[test]
fn parse_diagnostic_error_lists_valid_options() {
let err = Profile::parse("xyz").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("core"));
assert!(msg.contains("graph"));
assert!(msg.contains("full"));
assert!(msg.contains("xyz"));
}
#[test]
fn default_is_core() {
assert_eq!(Profile::default(), Profile::core());
}
#[test]
fn family_for_tool_resolves_every_baseline_name() {
let baseline = [
"memory_store",
"memory_recall",
"memory_list",
"memory_get",
"memory_search",
"memory_load_family",
"memory_smart_load",
"memory_update",
"memory_delete",
"memory_forget",
"memory_gc",
"memory_promote",
"memory_kg_query",
"memory_kg_timeline",
"memory_kg_invalidate",
"memory_link",
"memory_get_links",
"memory_entity_register",
"memory_entity_get_by_alias",
"memory_get_taxonomy",
"memory_replay",
"memory_verify",
"memory_find_paths",
"memory_pending_list",
"memory_pending_approve",
"memory_pending_reject",
"memory_namespace_set_standard",
"memory_namespace_get_standard",
"memory_namespace_clear_standard",
"memory_subscribe",
"memory_unsubscribe",
"memory_consolidate",
"memory_detect_contradiction",
"memory_check_duplicate",
"memory_auto_tag",
"memory_expand_query",
"memory_inbox",
"memory_subscription_replay",
"memory_subscription_dlq_list",
"memory_quota_status",
"memory_reflect",
"memory_capabilities",
"memory_agent_register",
"memory_agent_list",
"memory_session_start",
"memory_stats",
"memory_archive_list",
"memory_archive_purge",
"memory_archive_restore",
"memory_archive_stats",
"memory_list_subscriptions",
"memory_notify",
"memory_skill_register",
"memory_skill_list",
"memory_skill_get",
"memory_skill_resource",
"memory_skill_export",
"memory_skill_promote_from_reflection",
"memory_skill_compositional_context",
"memory_export_reflection",
"memory_offload",
"memory_deref",
"memory_atomise",
"memory_ingest_multistep",
"memory_calibrate_confidence",
"memory_share",
];
assert_eq!(
baseline.len(),
66,
"baseline list = 43 (v0.6.3.1) + 1 (v0.7.0 I4 memory_replay) + \
1 (v0.7 H4 memory_verify) + 1 (v0.7 B1 memory_load_family) + \
1 (v0.7 B2 memory_smart_load) + \
2 (v0.7 K7 memory_subscription_replay + memory_subscription_dlq_list) + \
1 (v0.7 J7 memory_find_paths) + 1 (v0.7 K8 memory_quota_status) + \
1 (v0.7.0 Task 4/8 memory_reflect) + \
5 (v0.7.0 L1-5 skill tools) + \
1 (v0.7.0 L2-6 memory_skill_promote_from_reflection) + \
1 (v0.7.0 L2-7 memory_skill_compositional_context) + \
1 (v0.7.0 QW-1 memory_export_reflection) + \
2 (v0.7.0 QW-3 follow-up memory_offload + memory_deref) + \
1 (v0.7.0 WT-1-C memory_atomise) + \
1 (v0.7.0 Form 3 memory_ingest_multistep) + \
1 (v0.7.0 Form 5 memory_calibrate_confidence) + \
1 (v0.7.0 issues #224 + #311 memory_share) = 66"
);
for name in baseline {
assert!(
Family::for_tool(name).is_some(),
"Family::for_tool({name}) returned None — update the family map"
);
}
}
#[test]
fn family_for_tool_returns_none_for_unknown() {
assert!(Family::for_tool("memory_does_not_exist").is_none());
assert!(Family::for_tool("").is_none());
}
#[test]
fn loads_includes_core_tools_under_core_profile() {
let p = Profile::core();
assert!(p.loads("memory_store"));
assert!(p.loads("memory_recall"));
assert!(!p.loads("memory_kg_query"));
assert!(p.loads("memory_capabilities"));
}
#[test]
fn loads_full_profile_includes_every_tool() {
let p = Profile::full();
for name in [
"memory_store",
"memory_kg_query",
"memory_consolidate",
"memory_archive_list",
"memory_notify",
"memory_capabilities",
] {
assert!(p.loads(name), "full profile should load {name}");
}
}
#[test]
fn loads_unknown_tool_returns_false() {
let p = Profile::full();
assert!(!p.loads("memory_does_not_exist"));
}
#[test]
fn always_on_tools_loaded_in_every_profile() {
for p in [
Profile::core(),
Profile::graph(),
Profile::admin(),
Profile::power(),
Profile::full(),
] {
for name in ALWAYS_ON_TOOLS {
assert!(p.loads(name), "{name} must load in every profile");
}
}
}
}