kkernel/
pack_introspect.rs1use anyhow::{anyhow, Context, Result};
13use khive_runtime::pack::{PackRegistry, VerbRegistry, VerbRegistryBuilder, Visibility};
14use khive_runtime::{KhiveRuntime, RuntimeConfig};
15use serde::Serialize;
16
17#[derive(Debug, Serialize, PartialEq, Eq)]
19#[serde(rename_all = "lowercase")]
20pub enum VerbVisibility {
21 Verb,
23 Subhandler,
25}
26
27impl From<&Visibility> for VerbVisibility {
28 fn from(v: &Visibility) -> Self {
29 match v {
30 Visibility::Verb => VerbVisibility::Verb,
31 Visibility::Subhandler => VerbVisibility::Subhandler,
32 }
33 }
34}
35
36#[derive(Debug, Serialize)]
42pub struct VerbInfo {
43 pub name: String,
44 pub description: String,
45 pub visibility: VerbVisibility,
46 pub category: String,
47}
48
49#[derive(Debug, Serialize)]
51pub struct PackInfo {
52 pub name: String,
53 pub note_kinds: Vec<String>,
54 pub entity_kinds: Vec<String>,
55 pub requires: Vec<String>,
56 pub verbs: Vec<VerbInfo>,
57}
58
59fn build_registry() -> Result<(VerbRegistry, KhiveRuntime)> {
63 let config = RuntimeConfig {
64 db_path: None,
65 default_namespace: khive_runtime::Namespace::parse("kkernel-introspect")
66 .unwrap_or_else(|_| khive_runtime::Namespace::local()),
67 embedding_model: None,
68 ..RuntimeConfig::default()
69 };
70 let runtime = KhiveRuntime::new(config).context("building introspection runtime")?;
71 let mut builder = VerbRegistryBuilder::new();
72 let names: Vec<String> = PackRegistry::discovered_names()
73 .into_iter()
74 .map(str::to_string)
75 .collect();
76 PackRegistry::register_packs(&names, runtime.clone(), &mut builder)
77 .map_err(|n| anyhow!("pack {n:?} declared in inventory but factory missing"))?;
78 let registry = builder.build().context("building VerbRegistry")?;
79 Ok((registry, runtime))
80}
81
82fn pack_info_from_registry(registry: &VerbRegistry, name: &str) -> Option<PackInfo> {
83 let verbs = registry.pack_verbs(name)?;
85 Some(PackInfo {
86 name: name.to_string(),
87 note_kinds: registry
88 .pack_note_kinds(name)
89 .unwrap_or(&[])
90 .iter()
91 .map(|s| s.to_string())
92 .collect(),
93 entity_kinds: registry
94 .pack_entity_kinds(name)
95 .unwrap_or(&[])
96 .iter()
97 .map(|s| s.to_string())
98 .collect(),
99 requires: registry
100 .pack_requires(name)
101 .unwrap_or(&[])
102 .iter()
103 .map(|s| s.to_string())
104 .collect(),
105 verbs: verbs
106 .iter()
107 .map(|v| VerbInfo {
108 name: v.name.to_string(),
109 description: v.description.to_string(),
110 visibility: VerbVisibility::from(&v.visibility),
111 category: format!("{:?}", v.category),
112 })
113 .collect(),
114 })
115}
116
117pub fn list_packs() -> Result<Vec<PackInfo>> {
119 let (registry, _runtime) = build_registry()?;
120 let names: Vec<String> = registry
121 .pack_names()
122 .into_iter()
123 .map(str::to_string)
124 .collect();
125 Ok(names
126 .iter()
127 .filter_map(|n| pack_info_from_registry(®istry, n))
128 .collect())
129}
130
131pub fn pack_handler(name: &str) -> Result<Option<PackInfo>> {
136 let (registry, _runtime) = build_registry()?;
137 Ok(pack_info_from_registry(®istry, name))
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn list_packs_returns_at_least_kg() {
146 let packs = list_packs().expect("list_packs succeeds");
147 assert!(!packs.is_empty(), "at least one pack must register");
148 let names: Vec<&str> = packs.iter().map(|p| p.name.as_str()).collect();
149 assert!(
150 names.contains(&"kg"),
151 "kg pack must be registered; got {names:?}"
152 );
153 }
154
155 #[test]
156 fn pack_handler_for_kg_returns_full_surface() {
157 let info = pack_handler("kg")
158 .expect("pack_handler succeeds")
159 .expect("kg pack must exist");
160 assert_eq!(info.name, "kg");
161 assert!(
162 !info.verbs.is_empty(),
163 "kg pack must expose verbs; got {:?}",
164 info.verbs
165 );
166 assert_eq!(
168 info.verbs.len(),
169 14,
170 "kg pack must expose 14 verbs (ADR-024 + ADR-046); got {}: {:?}",
171 info.verbs.len(),
172 info.verbs.iter().map(|v| &v.name).collect::<Vec<_>>()
173 );
174 let create = info.verbs.iter().find(|v| v.name == "create").unwrap();
176 assert_eq!(
177 create.visibility,
178 VerbVisibility::Verb,
179 "kg create must have Verb visibility"
180 );
181 assert!(
182 !create.category.is_empty(),
183 "kg create must have a non-empty category"
184 );
185 }
186
187 #[test]
188 fn memory_pack_subhandlers_carry_subhandler_visibility() {
189 let info = pack_handler("memory")
190 .expect("pack_handler succeeds")
191 .expect("memory pack must exist");
192 let subhandlers: Vec<&VerbInfo> = info
194 .verbs
195 .iter()
196 .filter(|v| v.visibility == VerbVisibility::Subhandler)
197 .collect();
198 assert!(
199 !subhandlers.is_empty(),
200 "memory pack must have subhandler entries; got none in {:?}",
201 info.verbs.iter().map(|v| &v.name).collect::<Vec<_>>()
202 );
203 let embed = info
205 .verbs
206 .iter()
207 .find(|v| v.name == "recall.embed")
208 .expect("recall.embed must be in the handler list");
209 assert_eq!(
210 embed.visibility,
211 VerbVisibility::Subhandler,
212 "recall.embed must have Subhandler visibility (F119)"
213 );
214 }
215
216 #[test]
217 fn pack_handler_unknown_returns_none() {
218 let info = pack_handler("does_not_exist").unwrap();
219 assert!(info.is_none(), "unknown pack returns None, not Err");
220 }
221}