Skip to main content

kkernel/
pack_introspect.rs

1//! `kkernel pack list` and `kkernel pack handler` — introspection over
2//! registered packs.
3//!
4//! Both subcommands operate on a `VerbRegistry` built from the active pack
5//! set. They return data — JSON for machines, a table for humans — without
6//! invoking any handler.
7//!
8//! ADR-076 establishes that pack registration eventually lives in the kernel.
9//! For now both binaries collect the same set via `inventory!`; this module
10//! consumes whatever is registered and prints it.
11
12use anyhow::{anyhow, Context, Result};
13use khive_runtime::pack::{PackRegistry, VerbRegistry, VerbRegistryBuilder, Visibility};
14use khive_runtime::{KhiveRuntime, RuntimeConfig};
15use serde::Serialize;
16
17/// Visibility tier of a registered handler (ADR-017 §Visibility).
18#[derive(Debug, Serialize, PartialEq, Eq)]
19#[serde(rename_all = "lowercase")]
20pub enum VerbVisibility {
21    /// Externally invokable — surfaced on the MCP `request` tool wire.
22    Verb,
23    /// Internal pipeline step — addressable via the DSL but NOT on the MCP wire.
24    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/// Description of a single registered handler (ADR-017 §Introspection, F126).
37///
38/// Includes `visibility` and `category` alongside `name` and `description`
39/// so introspection clients can distinguish MCP-exposed verbs from internal
40/// subhandlers and surface speech-act classification.
41#[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/// Description of a single registered pack.
50#[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
59/// Build an in-memory introspection registry containing every discoverable
60/// pack. Returns `(registry, runtime)` so the caller can hold the runtime
61/// alive for the duration of the introspection call.
62fn 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    // pack_verbs returns None if name isn't registered — gate everything off it.
84    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
117/// Enumerate all registered packs and their full surface.
118pub 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(&registry, n))
128        .collect())
129}
130
131/// Return the full handler surface for one pack — its verbs with descriptions,
132/// note kinds, entity kinds, and required pack dependencies.
133///
134/// Returns `Ok(None)` if no pack with `name` is registered.
135pub fn pack_handler(name: &str) -> Result<Option<PackInfo>> {
136    let (registry, _runtime) = build_registry()?;
137    Ok(pack_info_from_registry(&registry, 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        // ADR-024 requires 11 KG verbs; ADR-046 adds propose/review/withdraw → 14 total
167        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        // F126: VerbInfo must include visibility and category fields.
175        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        // recall.embed, recall.candidates, recall.fuse, recall.score are Subhandler.
193        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        // recall.embed must be a subhandler.
204        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}