use anyhow::{anyhow, Context, Result};
use khive_runtime::pack::{PackRegistry, VerbRegistry, VerbRegistryBuilder, Visibility};
use khive_runtime::{KhiveRuntime, RuntimeConfig};
use serde::Serialize;
#[derive(Debug, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VerbVisibility {
Verb,
Subhandler,
}
impl From<&Visibility> for VerbVisibility {
fn from(v: &Visibility) -> Self {
match v {
Visibility::Verb => VerbVisibility::Verb,
Visibility::Subhandler => VerbVisibility::Subhandler,
}
}
}
#[derive(Debug, Serialize)]
pub struct VerbInfo {
pub name: String,
pub description: String,
pub visibility: VerbVisibility,
pub category: String,
}
#[derive(Debug, Serialize)]
pub struct PackInfo {
pub name: String,
pub note_kinds: Vec<String>,
pub entity_kinds: Vec<String>,
pub requires: Vec<String>,
pub verbs: Vec<VerbInfo>,
}
fn build_registry() -> Result<(VerbRegistry, KhiveRuntime)> {
let config = RuntimeConfig {
db_path: None,
default_namespace: khive_runtime::Namespace::parse("kkernel-introspect")
.unwrap_or_else(|_| khive_runtime::Namespace::local()),
embedding_model: None,
..RuntimeConfig::default()
};
let runtime = KhiveRuntime::new(config).context("building introspection runtime")?;
let mut builder = VerbRegistryBuilder::new();
let names: Vec<String> = PackRegistry::discovered_names()
.into_iter()
.map(str::to_string)
.collect();
PackRegistry::register_packs(&names, runtime.clone(), &mut builder)
.map_err(|n| anyhow!("pack {n:?} declared in inventory but factory missing"))?;
let registry = builder.build().context("building VerbRegistry")?;
Ok((registry, runtime))
}
fn pack_info_from_registry(registry: &VerbRegistry, name: &str) -> Option<PackInfo> {
let verbs = registry.pack_verbs(name)?;
Some(PackInfo {
name: name.to_string(),
note_kinds: registry
.pack_note_kinds(name)
.unwrap_or(&[])
.iter()
.map(|s| s.to_string())
.collect(),
entity_kinds: registry
.pack_entity_kinds(name)
.unwrap_or(&[])
.iter()
.map(|s| s.to_string())
.collect(),
requires: registry
.pack_requires(name)
.unwrap_or(&[])
.iter()
.map(|s| s.to_string())
.collect(),
verbs: verbs
.iter()
.map(|v| VerbInfo {
name: v.name.to_string(),
description: v.description.to_string(),
visibility: VerbVisibility::from(&v.visibility),
category: format!("{:?}", v.category),
})
.collect(),
})
}
pub fn list_packs() -> Result<Vec<PackInfo>> {
let (registry, _runtime) = build_registry()?;
let names: Vec<String> = registry
.pack_names()
.into_iter()
.map(str::to_string)
.collect();
Ok(names
.iter()
.filter_map(|n| pack_info_from_registry(®istry, n))
.collect())
}
pub fn pack_handler(name: &str) -> Result<Option<PackInfo>> {
let (registry, _runtime) = build_registry()?;
Ok(pack_info_from_registry(®istry, name))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_packs_returns_at_least_kg() {
let packs = list_packs().expect("list_packs succeeds");
assert!(!packs.is_empty(), "at least one pack must register");
let names: Vec<&str> = packs.iter().map(|p| p.name.as_str()).collect();
assert!(
names.contains(&"kg"),
"kg pack must be registered; got {names:?}"
);
}
#[test]
fn pack_handler_for_kg_returns_full_surface() {
let info = pack_handler("kg")
.expect("pack_handler succeeds")
.expect("kg pack must exist");
assert_eq!(info.name, "kg");
assert!(
!info.verbs.is_empty(),
"kg pack must expose verbs; got {:?}",
info.verbs
);
assert_eq!(
info.verbs.len(),
16,
"kg pack must expose 16 verbs (ADR-024 + ADR-046 + verbs discovery + stats); got {}: {:?}",
info.verbs.len(),
info.verbs.iter().map(|v| &v.name).collect::<Vec<_>>()
);
let create = info.verbs.iter().find(|v| v.name == "create").unwrap();
assert_eq!(
create.visibility,
VerbVisibility::Verb,
"kg create must have Verb visibility"
);
assert!(
!create.category.is_empty(),
"kg create must have a non-empty category"
);
}
#[test]
fn memory_pack_subhandlers_carry_subhandler_visibility() {
let info = pack_handler("memory")
.expect("pack_handler succeeds")
.expect("memory pack must exist");
let subhandlers: Vec<&VerbInfo> = info
.verbs
.iter()
.filter(|v| v.visibility == VerbVisibility::Subhandler)
.collect();
assert!(
!subhandlers.is_empty(),
"memory pack must have subhandler entries; got none in {:?}",
info.verbs.iter().map(|v| &v.name).collect::<Vec<_>>()
);
let embed = info
.verbs
.iter()
.find(|v| v.name == "memory.recall_embed")
.expect("memory.recall_embed must be in the handler list");
assert_eq!(
embed.visibility,
VerbVisibility::Subhandler,
"memory.recall_embed must have Subhandler visibility (F119)"
);
}
#[test]
fn pack_handler_unknown_returns_none() {
let info = pack_handler("does_not_exist").unwrap();
assert!(info.is_none(), "unknown pack returns None, not Err");
}
}