gestura_core_knowledge/
lib.rs

1//! Knowledge system for agent expertise and contextual guidance.
2//!
3//! `gestura-core-knowledge` provides a progressive-disclosure knowledge base
4//! that lets the runtime expose specialized expertise only when it is relevant
5//! to the current request. This keeps default prompts lean while still allowing
6//! rich built-in guidance for areas such as Rust, Tauri, CLI workflows, MCP,
7//! voice, and other expert domains.
8//!
9//! ## Design role
10//!
11//! This crate sits beside, not inside, the request-context system:
12//!
13//! - `gestura-core-context` decides *what categories of context* are relevant
14//! - `gestura-core-knowledge` provides curated expert content that can be
15//!   enabled, matched, and loaded when those requests benefit from it
16//!
17//! Knowledge items can come from built-in expert documents or user-managed
18//! additions persisted on disk.
19//!
20//! ## Main concepts
21//!
22//! - `KnowledgeStore`: registry and persistence layer for knowledge items
23//! - `KnowledgeItem`: a single expert document with metadata, triggers, and
24//!   optional reference material
25//! - `KnowledgeQuery`: query-time filter and ranking input
26//! - `KnowledgeSettingsManager`: per-session/default enablement for knowledge
27//!   items so users and sessions can opt into specific expertise
28//!
29//! ## Built-in knowledge structure
30//!
31//! Built-in experts follow a compact core-plus-references pattern:
32//!
33//! ```text
34//! knowledge/
35//! ├── rust-expert/
36//! │   ├── KNOWLEDGE.md
37//! │   └── references/
38//! └── tauri-expert/
39//!     ├── KNOWLEDGE.md
40//!     └── references/
41//! ```
42//!
43//! The goal is to keep the top-level expert doc concise and load reference
44//! material only when that extra depth is needed.
45//!
46//! ## Usage
47//!
48//! ```rust,ignore
49//! use gestura_core::knowledge::{KnowledgeStore, KnowledgeQuery};
50//!
51//! let store = KnowledgeStore::with_default_dir();
52//! register_builtin_knowledge(&store);
53//!
54//! let query = KnowledgeQuery {
55//!     query: "Help me with async Rust".to_string(),
56//!     ..Default::default()
57//! };
58//!
59//! let matches = store.find(&query);
60//! for m in matches {
61//!     println!("Matched: {} (score: {})", m.item.name, m.score);
62//! }
63//! ```
64//!
65//! ## Stable import paths
66//!
67//! Most code should import through `gestura_core::knowledge::*`.
68
69pub mod session_settings;
70mod store;
71mod types;
72
73pub use session_settings::DEFAULT_KNOWLEDGE_SETTINGS_SESSION_ID;
74pub use session_settings::{KnowledgeSettingsManager, SessionKnowledgeSettings};
75pub use store::{KnowledgeError, KnowledgeStore, register_builtin_knowledge};
76pub use types::{KnowledgeItem, KnowledgeMatch, KnowledgeQuery, KnowledgeReference, LoadCondition};
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use tempfile::tempdir;
82
83    #[test]
84    fn test_store_creation() {
85        let store = KnowledgeStore::with_default_dir();
86        assert_eq!(store.count(), 0);
87    }
88
89    #[test]
90    fn test_register_and_find() {
91        let tmp = tempdir().unwrap();
92        let store = KnowledgeStore::new(tmp.path());
93
94        let item = KnowledgeItem::new("test-item", "Test Item", "A test knowledge item")
95            .with_triggers(["test", "example"]);
96
97        store.register(item);
98        assert_eq!(store.count(), 1);
99
100        let query = KnowledgeQuery {
101            query: "help me with a test".to_string(),
102            ..Default::default()
103        };
104
105        let matches = store.find(&query);
106        assert_eq!(matches.len(), 1);
107        assert_eq!(matches[0].item.id, "test-item");
108    }
109
110    #[test]
111    fn test_builtin_knowledge() {
112        let tmp = tempdir().unwrap();
113        let store = KnowledgeStore::new(tmp.path());
114        register_builtin_knowledge(&store);
115
116        // Should have builtin items
117        assert!(store.count() > 0);
118
119        // Should find rust expert
120        let query = KnowledgeQuery {
121            query: "help with rust ownership".to_string(),
122            ..Default::default()
123        };
124        let matches = store.find(&query);
125        assert!(!matches.is_empty());
126    }
127
128    #[test]
129    fn test_builtin_queries_resolve_expected_experts() {
130        let tmp = tempdir().unwrap();
131        let store = KnowledgeStore::new(tmp.path());
132        register_builtin_knowledge(&store);
133
134        for (query, expected_id) in [
135            (
136                "help me fix cargo clippy warnings in async rust with tokio and serde",
137                "rust-expert",
138            ),
139            (
140                "build a tauri plugin command with an invoke handler and capabilities",
141                "tauri-expert",
142            ),
143            (
144                "add a clap subcommand with --json output and no_color-friendly terminal ux",
145                "cli-expert",
146            ),
147            (
148                "transcribe microphone audio with whisper-rs, cpal, vad, and speech-to-text",
149                "voice-expert",
150            ),
151            (
152                "implement an mcp server with tools/list, tools/call, resources/read, and notifications/initialized",
153                "mcp-expert",
154            ),
155            (
156                "publish an agent card for a remote agent and stream task delegation with sendSubscribe",
157                "a2a-expert",
158            ),
159        ] {
160            let matches = store.find(&KnowledgeQuery {
161                query: query.to_string(),
162                limit: Some(3),
163                ..Default::default()
164            });
165
166            assert!(!matches.is_empty(), "expected matches for query: {query}");
167            assert_eq!(matches[0].item.id, expected_id, "query: {query}");
168            assert!(
169                !matches[0].matched_triggers.is_empty(),
170                "expected trigger matches for query: {query}"
171            );
172        }
173    }
174
175    #[test]
176    fn test_specialty_queries_resolve_expected_experts() {
177        let tmp = tempdir().unwrap();
178        let store = KnowledgeStore::new(tmp.path());
179        register_builtin_knowledge(&store);
180
181        for (query, expected_id) in [
182            (
183                "design an analytics metric tree with instrumentation, retention cohorts, and experimentation readouts",
184                "analytics-expert",
185            ),
186            (
187                "review a scientific study for experimental design, evidence quality, reproducibility, and confounders",
188                "science-expert",
189            ),
190            (
191                "work through mathematical modeling, optimization, derivations, and estimation checks",
192                "math-expert",
193            ),
194            (
195                "develop marketing positioning, messaging, go to market planning, and growth campaigns for an ICP",
196                "marketing-expert",
197            ),
198            (
199                "review software system design with api contracts, observability, architecture, and production reliability",
200                "software-systems-expert",
201            ),
202            (
203                "design a robotics autonomy stack covering perception, localization, controls, and robot safety",
204                "robotics-expert",
205            ),
206            (
207                "evaluate a mechanical engineering concept for loads, materials, tolerances, manufacturability, and fatigue",
208                "mechanical-engineering-expert",
209            ),
210            (
211                "review an electrical engineering design for power electronics, pcb interfaces, signal integrity, and validation",
212                "electrical-engineering-expert",
213            ),
214            (
215                "plan a civil engineering site with grading, drainage, structures, constructability, and foundation constraints",
216                "civil-engineering-expert",
217            ),
218            (
219                "analyze a chemical engineering process with mass balance, thermodynamics, reaction kinetics, unit operations, and process safety",
220                "chemical-engineering-expert",
221            ),
222            (
223                "evaluate an aerospace mission architecture with flight dynamics, guidance, control, and verification",
224                "aerospace-expert",
225            ),
226        ] {
227            let matches = store.find(&KnowledgeQuery {
228                query: query.to_string(),
229                limit: Some(3),
230                ..Default::default()
231            });
232
233            assert!(!matches.is_empty(), "expected matches for query: {query}");
234            assert_eq!(matches[0].item.id, expected_id, "query: {query}");
235            assert!(
236                !matches[0].matched_triggers.is_empty(),
237                "expected trigger matches for query: {query}"
238            );
239        }
240    }
241
242    #[test]
243    fn test_categories() {
244        let tmp = tempdir().unwrap();
245        let store = KnowledgeStore::new(tmp.path());
246        register_builtin_knowledge(&store);
247
248        let cats = store.categories();
249        assert!(cats.contains(&"language".to_string()));
250        assert!(cats.contains(&"framework".to_string()));
251        assert!(cats.contains(&"analytics".to_string()));
252        assert!(cats.contains(&"robotics".to_string()));
253    }
254
255    #[test]
256    fn persist_and_reload_user_item() {
257        let tmp = tempdir().unwrap();
258
259        let store = KnowledgeStore::new(tmp.path());
260        register_builtin_knowledge(&store);
261
262        let user_item = KnowledgeItem::new(
263            "my-custom",
264            "My Custom",
265            "Custom knowledge for testing persistence",
266        )
267        .with_triggers(["custom", "persist"])
268        .with_category("user")
269        .with_content("Hello from disk");
270
271        store.upsert_user_item(user_item).unwrap();
272
273        // New store should be able to load it.
274        let store2 = KnowledgeStore::new(tmp.path());
275        register_builtin_knowledge(&store2);
276        let loaded = store2.load_user_items().unwrap();
277        assert_eq!(loaded, 1);
278
279        let fetched = store2.get("my-custom").unwrap();
280        assert_eq!(fetched.name, "My Custom");
281        assert!(fetched.core_content.contains("Hello from disk"));
282        assert_eq!(
283            fetched.metadata.get("origin").map(|s| s.as_str()),
284            Some("user")
285        );
286    }
287}