From 0e9b260518e620b974718b0e6dd5b827430cde94 Mon Sep 17 00:00:00 2001 From: Sam Valladares Date: Thu, 23 Apr 2026 03:18:53 -0500 Subject: [PATCH] chore: remove 3,091 LOC of orphan code + fix ghost env-var docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine tool modules in crates/vestige-mcp/src/tools/ had zero callers after the v2.0.x unification work shipped *_unified + maintenance::* replacements. They'd been #[allow(dead_code)]-papered over and forgotten. Verified each module independently: grep for tools::::, string dispatch in server.rs, cross-crate usage — all nine returned zero external callers. Removed modules (all superseded): checkpoint (364 LOC) — no callers anywhere codebase (298) — superseded by codebase_unified consolidate (36) — superseded by maintenance::execute_consolidate ingest (456) — superseded by smart_ingest intentions (1,093) — superseded by intention_unified knowledge (106) — no callers anywhere recall (403) — superseded by search_unified search (184) — superseded by search_unified stats (132) — superseded by maintenance::execute_system_status Also removed: - EmotionCategory::base_arousal (10 LOC, zero callers) Kept (still string-dispatched from server.rs): - context, feedback, memory_states, review, tagging Doc fixes (ghost env vars that were documented but zero Rust source reads): - docs/CONFIGURATION.md — dropped VESTIGE_DATA_DIR, VESTIGE_LOG_LEVEL rows (neither is read anywhere; --data-dir CLI flag + RUST_LOG are the real mechanisms). Added the full real env-var table. - packages/vestige-mcp-npm/README.md — same two ghost rows dropped - docs/VESTIGE_STATE_AND_PLAN.md:399 — dropped VESTIGE_DATA_DIR row - docs/VESTIGE_STATE_AND_PLAN.md:709 — typo VESTIGE_API_KEY -> VESTIGE_AUTH_TOKEN (matches shipping convention), "open if unset" -> "auto-generated if unset" to match actual behavior Verified post-cleanup: - cargo check --workspace clean - cargo clippy --workspace -D warnings clean - cargo test --workspace 1,223 passing / 0 failed - cargo build --release -p vestige-mcp clean Net: -3,091 LOC (14 files), zero behavior change, zero regressions. --- .../src/neuroscience/emotional_memory.rs | 15 - crates/vestige-mcp/src/tools/checkpoint.rs | 364 ------ crates/vestige-mcp/src/tools/codebase.rs | 298 ----- crates/vestige-mcp/src/tools/consolidate.rs | 36 - crates/vestige-mcp/src/tools/ingest.rs | 456 ------- crates/vestige-mcp/src/tools/intentions.rs | 1093 ----------------- crates/vestige-mcp/src/tools/knowledge.rs | 106 -- crates/vestige-mcp/src/tools/mod.rs | 32 +- crates/vestige-mcp/src/tools/recall.rs | 403 ------ crates/vestige-mcp/src/tools/search.rs | 184 --- crates/vestige-mcp/src/tools/stats.rs | 132 -- docs/CONFIGURATION.md | 12 +- docs/VESTIGE_STATE_AND_PLAN.md | 3 +- packages/vestige-mcp-npm/README.md | 9 +- 14 files changed, 26 insertions(+), 3117 deletions(-) delete mode 100644 crates/vestige-mcp/src/tools/checkpoint.rs delete mode 100644 crates/vestige-mcp/src/tools/codebase.rs delete mode 100644 crates/vestige-mcp/src/tools/consolidate.rs delete mode 100644 crates/vestige-mcp/src/tools/ingest.rs delete mode 100644 crates/vestige-mcp/src/tools/intentions.rs delete mode 100644 crates/vestige-mcp/src/tools/knowledge.rs delete mode 100644 crates/vestige-mcp/src/tools/recall.rs delete mode 100644 crates/vestige-mcp/src/tools/search.rs delete mode 100644 crates/vestige-mcp/src/tools/stats.rs diff --git a/crates/vestige-core/src/neuroscience/emotional_memory.rs b/crates/vestige-core/src/neuroscience/emotional_memory.rs index bc5a527..2f7dc86 100644 --- a/crates/vestige-core/src/neuroscience/emotional_memory.rs +++ b/crates/vestige-core/src/neuroscience/emotional_memory.rs @@ -103,21 +103,6 @@ pub enum EmotionCategory { Neutral, } -impl EmotionCategory { - /// Get the base arousal level for this category - #[allow(dead_code)] - fn base_arousal(&self) -> f64 { - match self { - Self::Joy => 0.6, - Self::Frustration => 0.7, - Self::Urgency => 0.9, - Self::Surprise => 0.8, - Self::Confusion => 0.4, - Self::Neutral => 0.1, - } - } -} - impl std::fmt::Display for EmotionCategory { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/vestige-mcp/src/tools/checkpoint.rs b/crates/vestige-mcp/src/tools/checkpoint.rs deleted file mode 100644 index 69cab08..0000000 --- a/crates/vestige-mcp/src/tools/checkpoint.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! Session Checkpoint Tool -//! -//! Batch smart_ingest for session-end saves. Accepts up to 20 items -//! in a single call, routing each through Prediction Error Gating. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::{IngestInput, Storage}; - -/// Input schema for session_checkpoint tool -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "items": { - "type": "array", - "description": "Array of items to save (max 20). Each goes through Prediction Error Gating.", - "maxItems": 20, - "items": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "The content to remember" - }, - "tags": { - "type": "array", - "items": { "type": "string" }, - "description": "Tags for categorization" - }, - "node_type": { - "type": "string", - "description": "Type: fact, concept, event, person, place, note, pattern, decision", - "default": "fact" - }, - "source": { - "type": "string", - "description": "Source reference" - } - }, - "required": ["content"] - } - } - }, - "required": ["items"] - }) -} - -#[derive(Debug, Deserialize)] -struct CheckpointArgs { - items: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CheckpointItem { - content: String, - tags: Option>, - node_type: Option, - source: Option, -} - -pub async fn execute(storage: &Arc, args: Option) -> Result { - let args: CheckpointArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.items.is_empty() { - return Err("Items array cannot be empty".to_string()); - } - - if args.items.len() > 20 { - return Err("Maximum 20 items per checkpoint".to_string()); - } - - let mut results = Vec::new(); - let mut created = 0u32; - let mut updated = 0u32; - let mut skipped = 0u32; - let mut errors = 0u32; - - for (i, item) in args.items.into_iter().enumerate() { - if item.content.trim().is_empty() { - results.push(serde_json::json!({ - "index": i, - "status": "skipped", - "reason": "Empty content" - })); - skipped += 1; - continue; - } - - let input = IngestInput { - content: item.content, - node_type: item.node_type.unwrap_or_else(|| "fact".to_string()), - source: item.source, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: item.tags.unwrap_or_default(), - valid_from: None, - valid_until: None, - }; - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - match storage.smart_ingest(input) { - Ok(result) => { - match result.decision.as_str() { - "create" | "supersede" | "replace" => created += 1, - "update" | "reinforce" | "merge" | "add_context" => updated += 1, - _ => created += 1, - } - results.push(serde_json::json!({ - "index": i, - "status": "saved", - "decision": result.decision, - "nodeId": result.node.id, - "similarity": result.similarity, - "reason": result.reason - })); - } - Err(e) => { - errors += 1; - results.push(serde_json::json!({ - "index": i, - "status": "error", - "reason": e.to_string() - })); - } - } - } - - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - { - match storage.ingest(input) { - Ok(node) => { - created += 1; - results.push(serde_json::json!({ - "index": i, - "status": "saved", - "decision": "create", - "nodeId": node.id, - "reason": "Embeddings not available - used regular ingest" - })); - } - Err(e) => { - errors += 1; - results.push(serde_json::json!({ - "index": i, - "status": "error", - "reason": e.to_string() - })); - } - } - } - } - - Ok(serde_json::json!({ - "success": errors == 0, - "summary": { - "total": results.len(), - "created": created, - "updated": updated, - "skipped": skipped, - "errors": errors - }, - "results": results - })) -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - async fn test_storage() -> (Arc, TempDir) { - let dir = TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - (Arc::new(storage), dir) - } - - #[test] - fn test_schema_has_required_fields() { - let schema = schema(); - assert_eq!(schema["type"], "object"); - assert!(schema["properties"]["items"].is_object()); - } - - #[tokio::test] - async fn test_empty_items_fails() { - let (storage, _dir) = test_storage().await; - let result = execute(&storage, Some(serde_json::json!({ "items": [] }))).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_batch_ingest() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [ - { "content": "First checkpoint item", "tags": ["test"] }, - { "content": "Second checkpoint item", "tags": ["test"] } - ] - })), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["total"], 2); - } - - #[tokio::test] - async fn test_skips_empty_content() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [ - { "content": "Valid item" }, - { "content": "" }, - { "content": "Another valid item" } - ] - })), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["skipped"], 1); - } - - #[tokio::test] - async fn test_missing_args_fails() { - let (storage, _dir) = test_storage().await; - let result = execute(&storage, None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing arguments")); - } - - #[tokio::test] - async fn test_exceeds_20_items_fails() { - let (storage, _dir) = test_storage().await; - let items: Vec = (0..21) - .map(|i| serde_json::json!({ "content": format!("Item {}", i) })) - .collect(); - let result = execute(&storage, Some(serde_json::json!({ "items": items }))).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Maximum 20 items")); - } - - #[tokio::test] - async fn test_exactly_20_items_succeeds() { - let (storage, _dir) = test_storage().await; - let items: Vec = (0..20) - .map(|i| serde_json::json!({ "content": format!("Item {}", i) })) - .collect(); - let result = execute(&storage, Some(serde_json::json!({ "items": items }))).await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["total"], 20); - } - - #[tokio::test] - async fn test_skips_whitespace_only_content() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [ - { "content": " \t\n " }, - { "content": "Valid content" } - ] - })), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["skipped"], 1); - assert_eq!(value["summary"]["created"], 1); - } - - #[tokio::test] - async fn test_single_item_succeeds() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [{ "content": "Single item" }] - })), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["total"], 1); - assert_eq!(value["success"], true); - } - - #[tokio::test] - async fn test_items_with_all_fields() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [{ - "content": "Full fields item", - "tags": ["test", "checkpoint"], - "node_type": "decision", - "source": "test-suite" - }] - })), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["created"], 1); - } - - #[tokio::test] - async fn test_results_array_matches_items() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [ - { "content": "First" }, - { "content": "" }, - { "content": "Third" } - ] - })), - ) - .await; - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - assert_eq!(results.len(), 3); - assert_eq!(results[0]["index"], 0); - assert_eq!(results[1]["index"], 1); - assert_eq!(results[1]["status"], "skipped"); - assert_eq!(results[2]["index"], 2); - } - - #[tokio::test] - async fn test_success_false_when_errors() { - // All items empty = all skipped = 0 errors = success true - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [ - { "content": "" }, - { "content": " " } - ] - })), - ) - .await; - let value = result.unwrap(); - assert_eq!(value["success"], true); // skipped ≠ errors - assert_eq!(value["summary"]["errors"], 0); - assert_eq!(value["summary"]["skipped"], 2); - } -} diff --git a/crates/vestige-mcp/src/tools/codebase.rs b/crates/vestige-mcp/src/tools/codebase.rs deleted file mode 100644 index 3d51a4b..0000000 --- a/crates/vestige-mcp/src/tools/codebase.rs +++ /dev/null @@ -1,298 +0,0 @@ -//! Codebase Tools (Deprecated - use codebase_unified instead) -//! -//! Remember patterns, decisions, and context about codebases. -//! This is a differentiating feature for AI-assisted development. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::{IngestInput, Storage}; - -/// Input schema for remember_pattern tool -pub fn pattern_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name/title for this pattern" - }, - "description": { - "type": "string", - "description": "Detailed description of the pattern" - }, - "files": { - "type": "array", - "items": { "type": "string" }, - "description": "Files where this pattern is used" - }, - "codebase": { - "type": "string", - "description": "Codebase/project identifier (e.g., 'vestige-tauri')" - } - }, - "required": ["name", "description"] - }) -} - -/// Input schema for remember_decision tool -pub fn decision_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "decision": { - "type": "string", - "description": "The architectural or design decision made" - }, - "rationale": { - "type": "string", - "description": "Why this decision was made" - }, - "alternatives": { - "type": "array", - "items": { "type": "string" }, - "description": "Alternatives that were considered" - }, - "files": { - "type": "array", - "items": { "type": "string" }, - "description": "Files affected by this decision" - }, - "codebase": { - "type": "string", - "description": "Codebase/project identifier" - } - }, - "required": ["decision", "rationale"] - }) -} - -/// Input schema for get_codebase_context tool -pub fn context_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "codebase": { - "type": "string", - "description": "Codebase/project identifier to get context for" - }, - "limit": { - "type": "integer", - "description": "Maximum items per category (default: 10)", - "default": 10 - } - }, - "required": [] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PatternArgs { - name: String, - description: String, - files: Option>, - codebase: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DecisionArgs { - decision: String, - rationale: String, - alternatives: Option>, - files: Option>, - codebase: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ContextArgs { - codebase: Option, - limit: Option, -} - -pub async fn execute_pattern(storage: &Arc, args: Option) -> Result { - let args: PatternArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.name.trim().is_empty() { - return Err("Pattern name cannot be empty".to_string()); - } - - // Build content with structured format - let mut content = format!("# Code Pattern: {}\n\n{}", args.name, args.description); - - if let Some(ref files) = args.files - && !files.is_empty() - { - content.push_str("\n\n## Files:\n"); - for f in files { - content.push_str(&format!("- {}\n", f)); - } - } - - // Build tags - let mut tags = vec!["pattern".to_string(), "codebase".to_string()]; - if let Some(ref codebase) = args.codebase { - tags.push(format!("codebase:{}", codebase)); - } - - let input = IngestInput { - content, - node_type: "pattern".to_string(), - source: args.codebase.clone(), - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags, - valid_from: None, - valid_until: None, - }; - - let node = storage.ingest(input).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "success": true, - "nodeId": node.id, - "patternName": args.name, - "message": format!("Pattern '{}' remembered successfully", args.name), - })) -} - -pub async fn execute_decision( - storage: &Arc, - args: Option, -) -> Result { - let args: DecisionArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.decision.trim().is_empty() { - return Err("Decision cannot be empty".to_string()); - } - - // Build content with structured format (ADR-like) - let mut content = format!( - "# Decision: {}\n\n## Context\n\n{}\n\n## Decision\n\n{}", - &args.decision[..args.decision.len().min(50)], - args.rationale, - args.decision - ); - - if let Some(ref alternatives) = args.alternatives - && !alternatives.is_empty() - { - content.push_str("\n\n## Alternatives Considered:\n"); - for alt in alternatives { - content.push_str(&format!("- {}\n", alt)); - } - } - - if let Some(ref files) = args.files - && !files.is_empty() - { - content.push_str("\n\n## Affected Files:\n"); - for f in files { - content.push_str(&format!("- {}\n", f)); - } - } - - // Build tags - let mut tags = vec![ - "decision".to_string(), - "architecture".to_string(), - "codebase".to_string(), - ]; - if let Some(ref codebase) = args.codebase { - tags.push(format!("codebase:{}", codebase)); - } - - let input = IngestInput { - content, - node_type: "decision".to_string(), - source: args.codebase.clone(), - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags, - valid_from: None, - valid_until: None, - }; - - let node = storage.ingest(input).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "success": true, - "nodeId": node.id, - "message": "Architectural decision remembered successfully", - })) -} - -pub async fn execute_context(storage: &Arc, args: Option) -> Result { - let args: ContextArgs = args - .map(serde_json::from_value) - .transpose() - .map_err(|e| format!("Invalid arguments: {}", e))? - .unwrap_or(ContextArgs { - codebase: None, - limit: Some(10), - }); - - let limit = args.limit.unwrap_or(10).clamp(1, 50); - - // Build tag filter for codebase - // Tags are stored as: ["pattern", "codebase", "codebase:vestige"] - // We search for the "codebase:{name}" tag - let tag_filter = args.codebase.as_ref().map(|cb| format!("codebase:{}", cb)); - - // Query patterns by node_type and tag - let patterns = storage - .get_nodes_by_type_and_tag("pattern", tag_filter.as_deref(), limit) - .unwrap_or_default(); - - // Query decisions by node_type and tag - let decisions = storage - .get_nodes_by_type_and_tag("decision", tag_filter.as_deref(), limit) - .unwrap_or_default(); - - let formatted_patterns: Vec = patterns - .iter() - .map(|n| { - serde_json::json!({ - "id": n.id, - "content": n.content, - "tags": n.tags, - "retentionStrength": n.retention_strength, - "createdAt": n.created_at.to_rfc3339(), - }) - }) - .collect(); - - let formatted_decisions: Vec = decisions - .iter() - .map(|n| { - serde_json::json!({ - "id": n.id, - "content": n.content, - "tags": n.tags, - "retentionStrength": n.retention_strength, - "createdAt": n.created_at.to_rfc3339(), - }) - }) - .collect(); - - Ok(serde_json::json!({ - "codebase": args.codebase, - "patterns": { - "count": formatted_patterns.len(), - "items": formatted_patterns, - }, - "decisions": { - "count": formatted_decisions.len(), - "items": formatted_decisions, - }, - })) -} diff --git a/crates/vestige-mcp/src/tools/consolidate.rs b/crates/vestige-mcp/src/tools/consolidate.rs deleted file mode 100644 index ab9e22b..0000000 --- a/crates/vestige-mcp/src/tools/consolidate.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Consolidation Tool (Deprecated) -//! -//! Run memory consolidation cycle with FSRS decay and embedding generation. - -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::Storage; - -/// Input schema for run_consolidation tool -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": {}, - }) -} - -pub async fn execute(storage: &Arc) -> Result { - let result = storage.run_consolidation().map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "success": true, - "nodesProcessed": result.nodes_processed, - "nodesPromoted": result.nodes_promoted, - "nodesPruned": result.nodes_pruned, - "decayApplied": result.decay_applied, - "embeddingsGenerated": result.embeddings_generated, - "durationMs": result.duration_ms, - "message": format!( - "Consolidation complete: {} nodes processed, {} embeddings generated, {}ms", - result.nodes_processed, - result.embeddings_generated, - result.duration_ms - ), - })) -} diff --git a/crates/vestige-mcp/src/tools/ingest.rs b/crates/vestige-mcp/src/tools/ingest.rs deleted file mode 100644 index d724445..0000000 --- a/crates/vestige-mcp/src/tools/ingest.rs +++ /dev/null @@ -1,456 +0,0 @@ -//! Ingest Tool -//! -//! Add new knowledge to memory. -//! -//! v1.5.0: Enhanced with same cognitive pipeline as smart_ingest: -//! Pre-ingest: importance scoring + intent detection -//! Post-ingest: synaptic tagging + novelty model update + hippocampal indexing - -use chrono::Utc; -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; -use tokio::sync::Mutex; - -use crate::cognitive::CognitiveEngine; -use vestige_core::{ - ContentType, ImportanceContext, ImportanceEvent, ImportanceEventType, IngestInput, Storage, -}; - -/// Input schema for ingest tool -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "The content to remember" - }, - "node_type": { - "type": "string", - "description": "Type of knowledge: fact, concept, event, person, place, note, pattern, decision", - "default": "fact" - }, - "tags": { - "type": "array", - "items": { "type": "string" }, - "description": "Tags for categorization" - }, - "source": { - "type": "string", - "description": "Source or reference for this knowledge" - } - }, - "required": ["content"] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct IngestArgs { - content: String, - node_type: Option, - tags: Option>, - source: Option, -} - -pub async fn execute( - storage: &Arc, - cognitive: &Arc>, - args: Option, -) -> Result { - let args: IngestArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - // Validate content - if args.content.trim().is_empty() { - return Err("Content cannot be empty".to_string()); - } - - if args.content.len() > 1_000_000 { - return Err("Content too large (max 1MB)".to_string()); - } - - // ==================================================================== - // COGNITIVE PRE-INGEST: importance scoring + intent detection - // ==================================================================== - let mut importance_composite = 0.0_f64; - let mut tags = args.tags.unwrap_or_default(); - let mut is_novel = false; - let mut embedding_strategy = String::new(); - - if let Ok(cog) = cognitive.try_lock() { - // Full 4-channel importance scoring - let context = ImportanceContext::current(); - let importance = cog - .importance_signals - .compute_importance(&args.content, &context); - importance_composite = importance.composite; - - // Standalone novelty check (dopaminergic signal) - let novelty_ctx = vestige_core::neuroscience::importance_signals::Context::default(); - is_novel = cog.novelty_signal.is_novel(&args.content, &novelty_ctx); - - // Intent detection → auto-tag - let intent_result = cog.intent_detector.detect_intent(); - if intent_result.confidence > 0.5 { - let intent_tag = format!("intent:{:?}", intent_result.primary_intent); - let intent_tag = if intent_tag.len() > 50 { - format!("{}...", &intent_tag[..intent_tag.floor_char_boundary(47)]) - } else { - intent_tag - }; - tags.push(intent_tag); - } - - // Detect content type → select adaptive embedding strategy - let content_type = ContentType::detect(&args.content); - let strategy = cog.adaptive_embedder.select_strategy(&content_type); - embedding_strategy = format!("{:?}", strategy); - } - - let input = IngestInput { - content: args.content.clone(), - node_type: args.node_type.unwrap_or_else(|| "fact".to_string()), - source: args.source, - sentiment_score: 0.0, - sentiment_magnitude: importance_composite, - tags, - valid_from: None, - valid_until: None, - }; - - // ==================================================================== - // INGEST (storage lock) - // ==================================================================== - - // Route through smart_ingest when embeddings are available to prevent duplicates. - // Falls back to raw ingest only when embeddings aren't ready. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - let fallback_input = input.clone(); - match storage.smart_ingest(input) { - Ok(result) => { - let node_id = result.node.id.clone(); - let node_content = result.node.content.clone(); - let node_type = result.node.node_type.clone(); - let has_embedding = result.node.has_embedding.unwrap_or(false); - - run_post_ingest( - cognitive, - &node_id, - &node_content, - &node_type, - importance_composite, - ); - - Ok(serde_json::json!({ - "success": true, - "nodeId": node_id, - "decision": result.decision, - "message": format!("Knowledge ingested successfully. Node ID: {} ({})", node_id, result.decision), - "hasEmbedding": has_embedding, - "similarity": result.similarity, - "reason": result.reason, - "isNovel": is_novel, - "embeddingStrategy": embedding_strategy, - })) - } - Err(_) => { - let node = storage.ingest(fallback_input).map_err(|e| e.to_string())?; - let node_id = node.id.clone(); - let node_content = node.content.clone(); - let node_type = node.node_type.clone(); - let has_embedding = node.has_embedding.unwrap_or(false); - - run_post_ingest( - cognitive, - &node_id, - &node_content, - &node_type, - importance_composite, - ); - - Ok(serde_json::json!({ - "success": true, - "nodeId": node_id, - "decision": "create", - "message": format!("Knowledge ingested successfully. Node ID: {}", node_id), - "hasEmbedding": has_embedding, - "isNovel": is_novel, - "embeddingStrategy": embedding_strategy, - })) - } - } - } - - // Fallback for builds without embedding features - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - { - let node = storage.ingest(input).map_err(|e| e.to_string())?; - let node_id = node.id.clone(); - let node_content = node.content.clone(); - let node_type = node.node_type.clone(); - let has_embedding = node.has_embedding.unwrap_or(false); - - run_post_ingest( - cognitive, - &node_id, - &node_content, - &node_type, - importance_composite, - ); - - Ok(serde_json::json!({ - "success": true, - "nodeId": node_id, - "decision": "create", - "message": format!("Knowledge ingested successfully. Node ID: {}", node_id), - "hasEmbedding": has_embedding, - "isNovel": is_novel, - "embeddingStrategy": embedding_strategy, - })) - } -} - -/// Cognitive post-ingest side effects: synaptic tagging, novelty update, hippocampal indexing. -fn run_post_ingest( - cognitive: &Arc>, - node_id: &str, - content: &str, - node_type: &str, - importance_composite: f64, -) { - if let Ok(mut cog) = cognitive.try_lock() { - // Synaptic tagging for retroactive capture - if importance_composite > 0.3 { - cog.synaptic_tagging.tag_memory(node_id); - if importance_composite > 0.7 { - let event = ImportanceEvent::for_memory(node_id, ImportanceEventType::NoveltySpike); - let _capture = cog.synaptic_tagging.trigger_prp(event); - } - } - - // Update novelty model - cog.importance_signals.learn_content(content); - - // Record in hippocampal index - let _ = cog - .hippocampal_index - .index_memory(node_id, content, node_type, Utc::now(), None); - - // Cross-project pattern recording - cog.cross_project - .record_project_memory(node_id, "default", None); - } -} - -// ============================================================================ -// TESTS -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - use crate::cognitive::CognitiveEngine; - use tempfile::TempDir; - - fn test_cognitive() -> Arc> { - Arc::new(Mutex::new(CognitiveEngine::new())) - } - - /// Create a test storage instance with a temporary database - async fn test_storage() -> (Arc, TempDir) { - let dir = TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - (Arc::new(storage), dir) - } - - // ======================================================================== - // INPUT VALIDATION TESTS - // ======================================================================== - - #[tokio::test] - async fn test_ingest_empty_content_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "content": "" }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_ingest_whitespace_only_content_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "content": " \n\t " }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_ingest_missing_arguments_fails() { - let (storage, _dir) = test_storage().await; - let result = execute(&storage, &test_cognitive(), None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing arguments")); - } - - #[tokio::test] - async fn test_ingest_missing_content_field_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "node_type": "fact" }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Invalid arguments")); - } - - // ======================================================================== - // LARGE CONTENT TESTS - // ======================================================================== - - #[tokio::test] - async fn test_ingest_large_content_fails() { - let (storage, _dir) = test_storage().await; - // Create content larger than 1MB - let large_content = "x".repeat(1_000_001); - let args = serde_json::json!({ "content": large_content }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("too large")); - } - - #[tokio::test] - async fn test_ingest_exactly_1mb_succeeds() { - let (storage, _dir) = test_storage().await; - // Create content exactly 1MB - let exact_content = "x".repeat(1_000_000); - let args = serde_json::json!({ "content": exact_content }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - } - - // ======================================================================== - // SUCCESSFUL INGEST TESTS - // ======================================================================== - - #[tokio::test] - async fn test_ingest_basic_content_succeeds() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "This is a test fact to remember." - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - assert!(value["nodeId"].is_string()); - assert!(value["message"].as_str().unwrap().contains("successfully")); - } - - #[tokio::test] - async fn test_ingest_with_node_type() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "Error handling should use Result pattern.", - "node_type": "pattern" - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - } - - #[tokio::test] - async fn test_ingest_with_tags() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "The Rust programming language emphasizes safety.", - "tags": ["rust", "programming", "safety"] - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - } - - #[tokio::test] - async fn test_ingest_with_source() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "MCP protocol version 2024-11-05 is the current standard.", - "source": "https://modelcontextprotocol.io/spec" - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - } - - #[tokio::test] - async fn test_ingest_with_all_optional_fields() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "Complex memory with all metadata.", - "node_type": "decision", - "tags": ["architecture", "design"], - "source": "team meeting notes" - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - assert!(value["nodeId"].is_string()); - } - - // ======================================================================== - // NODE TYPE DEFAULTS - // ======================================================================== - - #[tokio::test] - async fn test_ingest_default_node_type_is_fact() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "Default type test content." - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - // Verify node was created - the default type is "fact" - let node_id = result.unwrap()["nodeId"].as_str().unwrap().to_string(); - let node = storage.get_node(&node_id).unwrap().unwrap(); - assert_eq!(node.node_type, "fact"); - } - - // ======================================================================== - // SCHEMA TESTS - // ======================================================================== - - #[test] - fn test_schema_has_required_fields() { - let schema_value = schema(); - assert_eq!(schema_value["type"], "object"); - assert!(schema_value["properties"]["content"].is_object()); - assert!( - schema_value["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("content")) - ); - } - - #[test] - fn test_schema_has_optional_fields() { - let schema_value = schema(); - assert!(schema_value["properties"]["node_type"].is_object()); - assert!(schema_value["properties"]["tags"].is_object()); - assert!(schema_value["properties"]["source"].is_object()); - } -} diff --git a/crates/vestige-mcp/src/tools/intentions.rs b/crates/vestige-mcp/src/tools/intentions.rs deleted file mode 100644 index 169ac43..0000000 --- a/crates/vestige-mcp/src/tools/intentions.rs +++ /dev/null @@ -1,1093 +0,0 @@ -//! Intentions Tools (Deprecated - use intention_unified instead) -//! -//! Prospective memory tools for setting and checking future intentions. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use chrono::{DateTime, Duration, Utc}; -use uuid::Uuid; - -use vestige_core::{IntentionRecord, Storage}; - -/// Schema for set_intention tool -pub fn set_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "description": { - "type": "string", - "description": "What to remember to do" - }, - "trigger": { - "type": "object", - "description": "When to trigger this intention", - "properties": { - "type": { - "type": "string", - "enum": ["time", "context", "event"], - "description": "Trigger type: time-based, context-based, or event-based" - }, - "at": { - "type": "string", - "description": "ISO timestamp for time-based triggers" - }, - "in_minutes": { - "type": "integer", - "description": "Minutes from now for duration-based triggers" - }, - "codebase": { - "type": "string", - "description": "Trigger when working in this codebase" - }, - "file_pattern": { - "type": "string", - "description": "Trigger when editing files matching this pattern" - }, - "topic": { - "type": "string", - "description": "Trigger when discussing this topic" - }, - "condition": { - "type": "string", - "description": "Natural language condition for event triggers" - } - } - }, - "priority": { - "type": "string", - "enum": ["low", "normal", "high", "critical"], - "default": "normal", - "description": "Priority level" - }, - "deadline": { - "type": "string", - "description": "Optional deadline (ISO timestamp)" - } - }, - "required": ["description"] - }) -} - -/// Schema for check_intentions tool -pub fn check_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "context": { - "type": "object", - "description": "Current context for matching intentions", - "properties": { - "current_time": { - "type": "string", - "description": "Current ISO timestamp (defaults to now)" - }, - "codebase": { - "type": "string", - "description": "Current codebase/project name" - }, - "file": { - "type": "string", - "description": "Current file path" - }, - "topics": { - "type": "array", - "items": { "type": "string" }, - "description": "Current discussion topics" - } - } - }, - "include_snoozed": { - "type": "boolean", - "default": false, - "description": "Include snoozed intentions" - } - } - }) -} - -/// Schema for complete_intention tool -pub fn complete_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "intentionId": { - "type": "string", - "description": "ID of the intention to mark as complete" - } - }, - "required": ["intentionId"] - }) -} - -/// Schema for snooze_intention tool -pub fn snooze_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "intentionId": { - "type": "string", - "description": "ID of the intention to snooze" - }, - "minutes": { - "type": "integer", - "description": "Minutes to snooze for", - "default": 30 - } - }, - "required": ["intentionId"] - }) -} - -/// Schema for list_intentions tool -pub fn list_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["active", "fulfilled", "cancelled", "snoozed", "all"], - "default": "active", - "description": "Filter by status" - }, - "limit": { - "type": "integer", - "default": 20, - "description": "Maximum number to return" - } - } - }) -} - -#[derive(Debug, Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct TriggerSpec { - #[serde(rename = "type")] - trigger_type: Option, - at: Option, - in_minutes: Option, - codebase: Option, - file_pattern: Option, - topic: Option, - condition: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SetIntentionArgs { - description: String, - trigger: Option, - priority: Option, - deadline: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ContextSpec { - #[allow(dead_code)] // Deserialized from JSON but not yet used in context matching - current_time: Option, - codebase: Option, - file: Option, - topics: Option>, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CheckIntentionsArgs { - context: Option, - #[allow(dead_code)] // Deserialized from JSON for future snoozed intentions filter - include_snoozed: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct IntentionIdArgs { - intention_id: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SnoozeArgs { - intention_id: String, - minutes: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ListArgs { - status: Option, - limit: Option, -} - -/// Execute set_intention tool -pub async fn execute_set(storage: &Arc, args: Option) -> Result { - let args: SetIntentionArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.description.trim().is_empty() { - return Err("Description cannot be empty".to_string()); - } - - let now = Utc::now(); - let id = Uuid::new_v4().to_string(); - - // Determine trigger type and data - let (trigger_type, trigger_data) = if let Some(trigger) = &args.trigger { - let t_type = trigger - .trigger_type - .clone() - .unwrap_or_else(|| "time".to_string()); - let data = serde_json::to_string(trigger).unwrap_or_else(|_| "{}".to_string()); - (t_type, data) - } else { - ("manual".to_string(), "{}".to_string()) - }; - - // Parse priority - let priority = match args.priority.as_deref() { - Some("low") => 1, - Some("high") => 3, - Some("critical") => 4, - _ => 2, // normal - }; - - // Parse deadline - let deadline = args.deadline.and_then(|s| { - DateTime::parse_from_rfc3339(&s) - .ok() - .map(|dt| dt.with_timezone(&Utc)) - }); - - // Calculate trigger time if specified - let trigger_at = if let Some(trigger) = &args.trigger { - if let Some(at) = &trigger.at { - DateTime::parse_from_rfc3339(at) - .ok() - .map(|dt| dt.with_timezone(&Utc)) - } else { - trigger.in_minutes.map(|mins| now + Duration::minutes(mins)) - } - } else { - None - }; - - let record = IntentionRecord { - id: id.clone(), - content: args.description.clone(), - trigger_type, - trigger_data, - priority, - status: "active".to_string(), - created_at: now, - deadline, - fulfilled_at: None, - reminder_count: 0, - last_reminded_at: None, - notes: None, - tags: vec![], - related_memories: vec![], - snoozed_until: None, - source_type: "mcp".to_string(), - source_data: None, - }; - - storage.save_intention(&record).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "success": true, - "intentionId": id, - "message": format!("Intention created: {}", args.description), - "priority": priority, - "triggerAt": trigger_at.map(|dt| dt.to_rfc3339()), - "deadline": deadline.map(|dt| dt.to_rfc3339()), - })) -} - -/// Execute check_intentions tool -pub async fn execute_check(storage: &Arc, args: Option) -> Result { - let args: CheckIntentionsArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => CheckIntentionsArgs { - context: None, - include_snoozed: None, - }, - }; - - let now = Utc::now(); - - // Get active intentions - let intentions = storage.get_active_intentions().map_err(|e| e.to_string())?; - - let mut triggered = Vec::new(); - let mut pending = Vec::new(); - - for intention in intentions { - // Parse trigger data - let trigger: Option = serde_json::from_str(&intention.trigger_data).ok(); - - // Check if triggered - let is_triggered = if let Some(t) = &trigger { - match t.trigger_type.as_deref() { - Some("time") => { - if let Some(at) = &t.at { - if let Ok(trigger_time) = DateTime::parse_from_rfc3339(at) { - trigger_time.with_timezone(&Utc) <= now - } else { - false - } - } else if let Some(mins) = t.in_minutes { - let trigger_time = intention.created_at + Duration::minutes(mins); - trigger_time <= now - } else { - false - } - } - Some("context") => { - if let Some(ctx) = &args.context { - // Check codebase match - if let (Some(trigger_codebase), Some(current_codebase)) = - (&t.codebase, &ctx.codebase) - { - current_codebase - .to_lowercase() - .contains(&trigger_codebase.to_lowercase()) - // Check file pattern match - } else if let (Some(pattern), Some(file)) = (&t.file_pattern, &ctx.file) { - file.contains(pattern) - // Check topic match - } else if let (Some(topic), Some(topics)) = (&t.topic, &ctx.topics) { - topics - .iter() - .any(|t| t.to_lowercase().contains(&topic.to_lowercase())) - } else { - false - } - } else { - false - } - } - _ => false, - } - } else { - false - }; - - // Check if overdue - let is_overdue = intention.deadline.map(|d| d < now).unwrap_or(false); - - let item = serde_json::json!({ - "id": intention.id, - "description": intention.content, - "priority": match intention.priority { - 1 => "low", - 3 => "high", - 4 => "critical", - _ => "normal", - }, - "createdAt": intention.created_at.to_rfc3339(), - "deadline": intention.deadline.map(|d| d.to_rfc3339()), - "isOverdue": is_overdue, - }); - - if is_triggered || is_overdue { - triggered.push(item); - } else { - pending.push(item); - } - } - - Ok(serde_json::json!({ - "triggered": triggered, - "pending": pending, - "checkedAt": now.to_rfc3339(), - })) -} - -/// Execute complete_intention tool -pub async fn execute_complete( - storage: &Arc, - args: Option, -) -> Result { - let args: IntentionIdArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing intention_id".to_string()), - }; - - let updated = storage - .update_intention_status(&args.intention_id, "fulfilled") - .map_err(|e| e.to_string())?; - - if updated { - Ok(serde_json::json!({ - "success": true, - "message": "Intention marked as complete", - "intentionId": args.intention_id, - })) - } else { - Err(format!("Intention not found: {}", args.intention_id)) - } -} - -/// Execute snooze_intention tool -pub async fn execute_snooze(storage: &Arc, args: Option) -> Result { - let args: SnoozeArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing intention_id".to_string()), - }; - - let minutes = args.minutes.unwrap_or(30); - let snooze_until = Utc::now() + Duration::minutes(minutes); - - let updated = storage - .snooze_intention(&args.intention_id, snooze_until) - .map_err(|e| e.to_string())?; - - if updated { - Ok(serde_json::json!({ - "success": true, - "message": format!("Intention snoozed for {} minutes", minutes), - "intentionId": args.intention_id, - "snoozedUntil": snooze_until.to_rfc3339(), - })) - } else { - Err(format!("Intention not found: {}", args.intention_id)) - } -} - -/// Execute list_intentions tool -pub async fn execute_list(storage: &Arc, args: Option) -> Result { - let args: ListArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => ListArgs { - status: None, - limit: None, - }, - }; - - let status = args.status.as_deref().unwrap_or("active"); - - let intentions = if status == "all" { - // Get all by combining different statuses - let mut all = storage.get_active_intentions().map_err(|e| e.to_string())?; - all.extend( - storage - .get_intentions_by_status("fulfilled") - .map_err(|e| e.to_string())?, - ); - all.extend( - storage - .get_intentions_by_status("cancelled") - .map_err(|e| e.to_string())?, - ); - all.extend( - storage - .get_intentions_by_status("snoozed") - .map_err(|e| e.to_string())?, - ); - all - } else if status == "active" { - // Use get_active_intentions for proper priority ordering - storage.get_active_intentions().map_err(|e| e.to_string())? - } else { - storage - .get_intentions_by_status(status) - .map_err(|e| e.to_string())? - }; - - let limit = args.limit.unwrap_or(20) as usize; - let now = Utc::now(); - - let items: Vec = intentions - .into_iter() - .take(limit) - .map(|i| { - let is_overdue = i.deadline.map(|d| d < now).unwrap_or(false); - serde_json::json!({ - "id": i.id, - "description": i.content, - "status": i.status, - "priority": match i.priority { - 1 => "low", - 3 => "high", - 4 => "critical", - _ => "normal", - }, - "createdAt": i.created_at.to_rfc3339(), - "deadline": i.deadline.map(|d| d.to_rfc3339()), - "isOverdue": is_overdue, - "snoozedUntil": i.snoozed_until.map(|d| d.to_rfc3339()), - }) - }) - .collect(); - - Ok(serde_json::json!({ - "intentions": items, - "total": items.len(), - "status": status, - })) -} - -// ============================================================================ -// TESTS -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - /// Create a test storage instance with a temporary database - async fn test_storage() -> (Arc, TempDir) { - let dir = TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - (Arc::new(storage), dir) - } - - /// Helper to create an intention and return its ID - async fn create_test_intention(storage: &Arc, description: &str) -> String { - let args = serde_json::json!({ - "description": description - }); - let result = execute_set(storage, Some(args)).await.unwrap(); - result["intentionId"].as_str().unwrap().to_string() - } - - // ======================================================================== - // SET_INTENTION TESTS - // ======================================================================== - - #[tokio::test] - async fn test_set_intention_empty_description_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "description": "" }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_set_intention_whitespace_only_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "description": " \t\n " }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_set_intention_missing_arguments_fails() { - let (storage, _dir) = test_storage().await; - let result = execute_set(&storage, None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing arguments")); - } - - #[tokio::test] - async fn test_set_intention_basic_succeeds() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "description": "Remember to write unit tests" - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - assert!(value["intentionId"].is_string()); - assert!( - value["message"] - .as_str() - .unwrap() - .contains("Intention created") - ); - } - - #[tokio::test] - async fn test_set_intention_with_priority() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "description": "Critical bug fix needed", - "priority": "critical" - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["priority"], 4); // critical = 4 - } - - #[tokio::test] - async fn test_set_intention_with_time_trigger() { - let (storage, _dir) = test_storage().await; - let future_time = (Utc::now() + Duration::hours(1)).to_rfc3339(); - let args = serde_json::json!({ - "description": "Meeting reminder", - "trigger": { - "type": "time", - "at": future_time - } - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["triggerAt"].is_string()); - } - - #[tokio::test] - async fn test_set_intention_with_duration_trigger() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "description": "Check build status", - "trigger": { - "type": "time", - "inMinutes": 30 - } - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["triggerAt"].is_string()); - } - - #[tokio::test] - async fn test_set_intention_with_context_trigger() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "description": "Review error handling", - "trigger": { - "type": "context", - "codebase": "payments" - } - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_set_intention_with_deadline() { - let (storage, _dir) = test_storage().await; - let deadline = (Utc::now() + Duration::days(7)).to_rfc3339(); - let args = serde_json::json!({ - "description": "Complete feature by end of week", - "deadline": deadline - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["deadline"].is_string()); - } - - // ======================================================================== - // CHECK_INTENTIONS TESTS - // ======================================================================== - - #[tokio::test] - async fn test_check_intentions_empty_succeeds() { - let (storage, _dir) = test_storage().await; - let result = execute_check(&storage, None).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["triggered"].is_array()); - assert!(value["pending"].is_array()); - assert!(value["checkedAt"].is_string()); - } - - #[tokio::test] - async fn test_check_intentions_returns_pending() { - let (storage, _dir) = test_storage().await; - // Create an intention without immediate trigger - create_test_intention(&storage, "Future task").await; - - let result = execute_check(&storage, None).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let pending = value["pending"].as_array().unwrap(); - assert!(!pending.is_empty()); - } - - #[tokio::test] - async fn test_check_intentions_with_context() { - let (storage, _dir) = test_storage().await; - - // Create context-triggered intention - let args = serde_json::json!({ - "description": "Check tests in payments", - "trigger": { - "type": "context", - "codebase": "payments" - } - }); - execute_set(&storage, Some(args)).await.unwrap(); - - // Check with matching context - let check_args = serde_json::json!({ - "context": { - "codebase": "payments-service" - } - }); - let result = execute_check(&storage, Some(check_args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let triggered = value["triggered"].as_array().unwrap(); - assert!(!triggered.is_empty()); - } - - #[tokio::test] - async fn test_check_intentions_time_triggered() { - let (storage, _dir) = test_storage().await; - - // Create time-triggered intention in the past - let past_time = (Utc::now() - Duration::hours(1)).to_rfc3339(); - let args = serde_json::json!({ - "description": "Past due task", - "trigger": { - "type": "time", - "at": past_time - } - }); - execute_set(&storage, Some(args)).await.unwrap(); - - let result = execute_check(&storage, None).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let triggered = value["triggered"].as_array().unwrap(); - assert!(!triggered.is_empty()); - } - - // ======================================================================== - // COMPLETE_INTENTION TESTS - // ======================================================================== - - #[tokio::test] - async fn test_complete_intention_succeeds() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task to complete").await; - - let args = serde_json::json!({ - "intentionId": intention_id - }); - let result = execute_complete(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - assert!(value["message"].as_str().unwrap().contains("complete")); - } - - #[tokio::test] - async fn test_complete_intention_nonexistent_fails() { - let (storage, _dir) = test_storage().await; - let fake_id = Uuid::new_v4().to_string(); - - let args = serde_json::json!({ - "intentionId": fake_id - }); - let result = execute_complete(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("not found")); - } - - #[tokio::test] - async fn test_complete_intention_missing_id_fails() { - let (storage, _dir) = test_storage().await; - let result = execute_complete(&storage, None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing intention_id")); - } - - #[tokio::test] - async fn test_completed_intention_not_in_active_list() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task to hide").await; - - // Complete it - let args = serde_json::json!({ "intentionId": intention_id }); - execute_complete(&storage, Some(args)).await.unwrap(); - - // Check active intentions - should not include completed - let list_args = serde_json::json!({ "status": "active" }); - let result = execute_list(&storage, Some(list_args)).await.unwrap(); - let intentions = result["intentions"].as_array().unwrap(); - - let ids: Vec<&str> = intentions - .iter() - .map(|i| i["id"].as_str().unwrap()) - .collect(); - assert!(!ids.contains(&intention_id.as_str())); - } - - // ======================================================================== - // SNOOZE_INTENTION TESTS - // ======================================================================== - - #[tokio::test] - async fn test_snooze_intention_succeeds() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task to snooze").await; - - let args = serde_json::json!({ - "intentionId": intention_id, - "minutes": 30 - }); - let result = execute_snooze(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - assert!(value["snoozedUntil"].is_string()); - assert!(value["message"].as_str().unwrap().contains("snoozed")); - } - - #[tokio::test] - async fn test_snooze_intention_default_minutes() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task with default snooze").await; - - let args = serde_json::json!({ - "intentionId": intention_id - }); - let result = execute_snooze(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["message"].as_str().unwrap().contains("30 minutes")); - } - - #[tokio::test] - async fn test_snooze_intention_nonexistent_fails() { - let (storage, _dir) = test_storage().await; - let fake_id = Uuid::new_v4().to_string(); - - let args = serde_json::json!({ - "intentionId": fake_id, - "minutes": 15 - }); - let result = execute_snooze(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("not found")); - } - - #[tokio::test] - async fn test_snooze_intention_missing_id_fails() { - let (storage, _dir) = test_storage().await; - let result = execute_snooze(&storage, None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing intention_id")); - } - - // ======================================================================== - // LIST_INTENTIONS TESTS - // ======================================================================== - - #[tokio::test] - async fn test_list_intentions_empty_succeeds() { - let (storage, _dir) = test_storage().await; - let result = execute_list(&storage, None).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["intentions"].is_array()); - assert_eq!(value["total"], 0); - assert_eq!(value["status"], "active"); - } - - #[tokio::test] - async fn test_list_intentions_returns_created() { - let (storage, _dir) = test_storage().await; - create_test_intention(&storage, "First task").await; - create_test_intention(&storage, "Second task").await; - - let result = execute_list(&storage, None).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["total"], 2); - } - - #[tokio::test] - async fn test_list_intentions_filter_by_status() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task to complete").await; - - // Complete one - let args = serde_json::json!({ "intentionId": intention_id }); - execute_complete(&storage, Some(args)).await.unwrap(); - - // Create another active one - create_test_intention(&storage, "Active task").await; - - // List fulfilled - let list_args = serde_json::json!({ "status": "fulfilled" }); - let result = execute_list(&storage, Some(list_args)).await.unwrap(); - assert_eq!(result["total"], 1); - assert_eq!(result["status"], "fulfilled"); - } - - #[tokio::test] - async fn test_list_intentions_with_limit() { - let (storage, _dir) = test_storage().await; - for i in 0..5 { - create_test_intention(&storage, &format!("Task {}", i)).await; - } - - let args = serde_json::json!({ "limit": 3 }); - let result = execute_list(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let intentions = value["intentions"].as_array().unwrap(); - assert!(intentions.len() <= 3); - } - - #[tokio::test] - async fn test_list_intentions_all_status() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task to complete").await; - create_test_intention(&storage, "Active task").await; - - // Complete one - let args = serde_json::json!({ "intentionId": intention_id }); - execute_complete(&storage, Some(args)).await.unwrap(); - - // List all - let list_args = serde_json::json!({ "status": "all" }); - let result = execute_list(&storage, Some(list_args)).await.unwrap(); - assert_eq!(result["total"], 2); - } - - // ======================================================================== - // INTENTION LIFECYCLE TESTS - // ======================================================================== - - #[tokio::test] - async fn test_intention_full_lifecycle() { - let (storage, _dir) = test_storage().await; - - // 1. Create intention - let intention_id = create_test_intention(&storage, "Full lifecycle test").await; - - // 2. Verify it appears in list - let list_result = execute_list(&storage, None).await.unwrap(); - assert_eq!(list_result["total"], 1); - - // 3. Snooze it - let snooze_args = serde_json::json!({ - "intentionId": intention_id, - "minutes": 5 - }); - let snooze_result = execute_snooze(&storage, Some(snooze_args)).await; - assert!(snooze_result.is_ok()); - - // 4. Complete it - let complete_args = serde_json::json!({ "intentionId": intention_id }); - let complete_result = execute_complete(&storage, Some(complete_args)).await; - assert!(complete_result.is_ok()); - - // 5. Verify it's no longer active - let final_list = execute_list(&storage, None).await.unwrap(); - assert_eq!(final_list["total"], 0); - - // 6. Verify it's in fulfilled list - let fulfilled_args = serde_json::json!({ "status": "fulfilled" }); - let fulfilled_list = execute_list(&storage, Some(fulfilled_args)).await.unwrap(); - assert_eq!(fulfilled_list["total"], 1); - } - - #[tokio::test] - async fn test_intention_priority_ordering() { - let (storage, _dir) = test_storage().await; - - // Create intentions with different priorities - let args_low = serde_json::json!({ - "description": "Low priority task", - "priority": "low" - }); - execute_set(&storage, Some(args_low)).await.unwrap(); - - let args_critical = serde_json::json!({ - "description": "Critical task", - "priority": "critical" - }); - execute_set(&storage, Some(args_critical)).await.unwrap(); - - let args_normal = serde_json::json!({ - "description": "Normal task", - "priority": "normal" - }); - execute_set(&storage, Some(args_normal)).await.unwrap(); - - // List and verify ordering (critical should be first due to priority DESC ordering) - let list_result = execute_list(&storage, None).await.unwrap(); - let intentions = list_result["intentions"].as_array().unwrap(); - - assert!(intentions.len() >= 3); - // Critical (4) should come before normal (2) and low (1) - let first_priority = intentions[0]["priority"].as_str().unwrap(); - assert_eq!(first_priority, "critical"); - } - - // ======================================================================== - // SCHEMA TESTS - // ======================================================================== - - #[test] - fn test_set_schema_has_required_fields() { - let schema_value = set_schema(); - assert_eq!(schema_value["type"], "object"); - assert!(schema_value["properties"]["description"].is_object()); - assert!( - schema_value["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("description")) - ); - } - - #[test] - fn test_complete_schema_has_required_fields() { - let schema_value = complete_schema(); - assert!(schema_value["properties"]["intentionId"].is_object()); - assert!( - schema_value["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("intentionId")) - ); - } - - #[test] - fn test_snooze_schema_has_required_fields() { - let schema_value = snooze_schema(); - assert!(schema_value["properties"]["intentionId"].is_object()); - assert!(schema_value["properties"]["minutes"].is_object()); - assert!( - schema_value["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("intentionId")) - ); - } - - #[test] - fn test_list_schema_has_optional_fields() { - let schema_value = list_schema(); - assert!(schema_value["properties"]["status"].is_object()); - assert!(schema_value["properties"]["limit"].is_object()); - } - - #[test] - fn test_check_schema_has_context_field() { - let schema_value = check_schema(); - assert!(schema_value["properties"]["context"].is_object()); - } -} diff --git a/crates/vestige-mcp/src/tools/knowledge.rs b/crates/vestige-mcp/src/tools/knowledge.rs deleted file mode 100644 index f26646d..0000000 --- a/crates/vestige-mcp/src/tools/knowledge.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Knowledge Tools (Deprecated - use memory_unified instead) -//! -//! Get and delete specific knowledge nodes. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::Storage; - -/// Input schema for get_knowledge tool -pub fn get_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The ID of the knowledge node to retrieve" - } - }, - "required": ["id"] - }) -} - -/// Input schema for delete_knowledge tool -pub fn delete_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The ID of the knowledge node to delete" - } - }, - "required": ["id"] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct KnowledgeArgs { - id: String, -} - -pub async fn execute_get(storage: &Arc, args: Option) -> Result { - let args: KnowledgeArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - // Validate UUID - uuid::Uuid::parse_str(&args.id).map_err(|_| "Invalid node ID format".to_string())?; - - let node = storage.get_node(&args.id).map_err(|e| e.to_string())?; - - match node { - Some(n) => Ok(serde_json::json!({ - "found": true, - "node": { - "id": n.id, - "content": n.content, - "nodeType": n.node_type, - "createdAt": n.created_at.to_rfc3339(), - "updatedAt": n.updated_at.to_rfc3339(), - "lastAccessed": n.last_accessed.to_rfc3339(), - "stability": n.stability, - "difficulty": n.difficulty, - "reps": n.reps, - "lapses": n.lapses, - "storageStrength": n.storage_strength, - "retrievalStrength": n.retrieval_strength, - "retentionStrength": n.retention_strength, - "sentimentScore": n.sentiment_score, - "sentimentMagnitude": n.sentiment_magnitude, - "nextReview": n.next_review.map(|d| d.to_rfc3339()), - "source": n.source, - "tags": n.tags, - "hasEmbedding": n.has_embedding, - "embeddingModel": n.embedding_model, - } - })), - None => Ok(serde_json::json!({ - "found": false, - "nodeId": args.id, - "message": "Node not found", - })), - } -} - -pub async fn execute_delete(storage: &Arc, args: Option) -> Result { - let args: KnowledgeArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - // Validate UUID - uuid::Uuid::parse_str(&args.id).map_err(|_| "Invalid node ID format".to_string())?; - - let deleted = storage.delete_node(&args.id).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "success": deleted, - "nodeId": args.id, - "message": if deleted { "Node deleted successfully" } else { "Node not found" }, - })) -} diff --git a/crates/vestige-mcp/src/tools/mod.rs b/crates/vestige-mcp/src/tools/mod.rs index 6d2d674..b1331cf 100644 --- a/crates/vestige-mcp/src/tools/mod.rs +++ b/crates/vestige-mcp/src/tools/mod.rs @@ -43,35 +43,23 @@ pub mod cross_reference; // v2.0.5: Active Forgetting — Anderson 2025 + Davis Rac1 pub mod suppress; -// Deprecated/internal tools — not advertised in the public MCP tools/list, -// but some functions are actively dispatched for backwards compatibility -// and internal cognitive operations. #[allow(dead_code)] suppresses warnings -// for the unused schema/struct items within these modules. -#[allow(dead_code)] -pub mod checkpoint; -#[allow(dead_code)] -pub mod codebase; -#[allow(dead_code)] -pub mod consolidate; +// Internal/backwards-compat tools still dispatched by server.rs for specific +// tool names. Each module below has live callers via string dispatch in +// `server.rs` (match arms on request.name). The #[allow(dead_code)] +// suppresses warnings for the per-module schema/struct items that aren't +// yet consumed. +// +// The nine legacy siblings here pre-v2.0.8 (checkpoint, codebase, consolidate, +// ingest, intentions, knowledge, recall, search, stats) were removed in the +// post-v2.0.8 dead-code sweep — all nine had zero callers after the +// unification work landed `*_unified` + `maintenance::*` replacements. #[allow(dead_code)] pub mod context; #[allow(dead_code)] pub mod feedback; #[allow(dead_code)] -pub mod ingest; -#[allow(dead_code)] -pub mod intentions; -#[allow(dead_code)] -pub mod knowledge; -#[allow(dead_code)] pub mod memory_states; #[allow(dead_code)] -pub mod recall; -#[allow(dead_code)] pub mod review; #[allow(dead_code)] -pub mod search; -#[allow(dead_code)] -pub mod stats; -#[allow(dead_code)] pub mod tagging; diff --git a/crates/vestige-mcp/src/tools/recall.rs b/crates/vestige-mcp/src/tools/recall.rs deleted file mode 100644 index 28ff8c4..0000000 --- a/crates/vestige-mcp/src/tools/recall.rs +++ /dev/null @@ -1,403 +0,0 @@ -//! Recall Tool (Deprecated - use search_unified instead) -//! -//! Search and retrieve knowledge from memory. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::{RecallInput, SearchMode, Storage}; - -/// Input schema for recall tool -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query" - }, - "limit": { - "type": "integer", - "description": "Maximum number of results (default: 10)", - "default": 10, - "minimum": 1, - "maximum": 100 - }, - "min_retention": { - "type": "number", - "description": "Minimum retention strength (0.0-1.0, default: 0.0)", - "default": 0.0, - "minimum": 0.0, - "maximum": 1.0 - } - }, - "required": ["query"] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RecallArgs { - query: String, - limit: Option, - min_retention: Option, -} - -pub async fn execute(storage: &Arc, args: Option) -> Result { - let args: RecallArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.query.trim().is_empty() { - return Err("Query cannot be empty".to_string()); - } - - let input = RecallInput { - query: args.query.clone(), - limit: args.limit.unwrap_or(10).clamp(1, 100), - min_retention: args.min_retention.unwrap_or(0.0).clamp(0.0, 1.0), - search_mode: SearchMode::Hybrid, - valid_at: None, - }; - - let nodes = storage.recall(input).map_err(|e| e.to_string())?; - - let results: Vec = nodes - .iter() - .map(|n| { - serde_json::json!({ - "id": n.id, - "content": n.content, - "nodeType": n.node_type, - "retentionStrength": n.retention_strength, - "stability": n.stability, - "difficulty": n.difficulty, - "reps": n.reps, - "tags": n.tags, - "source": n.source, - "createdAt": n.created_at.to_rfc3339(), - "lastAccessed": n.last_accessed.to_rfc3339(), - "nextReview": n.next_review.map(|d| d.to_rfc3339()), - }) - }) - .collect(); - - Ok(serde_json::json!({ - "query": args.query, - "total": results.len(), - "results": results, - })) -} - -// ============================================================================ -// TESTS -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - use vestige_core::IngestInput; - - /// Create a test storage instance with a temporary database - async fn test_storage() -> (Arc, TempDir) { - let dir = TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - (Arc::new(storage), dir) - } - - /// Helper to ingest test content - async fn ingest_test_content(storage: &Arc, content: &str) -> String { - let input = IngestInput { - content: content.to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec![], - valid_from: None, - valid_until: None, - }; - let node = storage.ingest(input).unwrap(); - node.id - } - - // ======================================================================== - // QUERY VALIDATION TESTS - // ======================================================================== - - #[tokio::test] - async fn test_recall_empty_query_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "query": "" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_recall_whitespace_only_query_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "query": " \t\n " }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_recall_missing_arguments_fails() { - let (storage, _dir) = test_storage().await; - let result = execute(&storage, None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing arguments")); - } - - #[tokio::test] - async fn test_recall_missing_query_field_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "limit": 10 }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Invalid arguments")); - } - - // ======================================================================== - // LIMIT CLAMPING TESTS - // ======================================================================== - - #[tokio::test] - async fn test_recall_limit_clamped_to_minimum() { - let (storage, _dir) = test_storage().await; - // Ingest some content first - ingest_test_content(&storage, "Test content for limit clamping").await; - - // Try with limit 0 - should clamp to 1 - let args = serde_json::json!({ - "query": "test", - "limit": 0 - }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_recall_limit_clamped_to_maximum() { - let (storage, _dir) = test_storage().await; - // Ingest some content first - ingest_test_content(&storage, "Test content for max limit").await; - - // Try with limit 1000 - should clamp to 100 - let args = serde_json::json!({ - "query": "test", - "limit": 1000 - }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_recall_negative_limit_clamped() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Test content for negative limit").await; - - let args = serde_json::json!({ - "query": "test", - "limit": -5 - }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - } - - // ======================================================================== - // MIN_RETENTION CLAMPING TESTS - // ======================================================================== - - #[tokio::test] - async fn test_recall_min_retention_clamped_to_zero() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Test content for retention clamping").await; - - let args = serde_json::json!({ - "query": "test", - "min_retention": -0.5 - }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_recall_min_retention_clamped_to_one() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Test content for max retention").await; - - let args = serde_json::json!({ - "query": "test", - "min_retention": 1.5 - }); - let result = execute(&storage, Some(args)).await; - // Should succeed but return no results (retention > 1.0 clamped to 1.0) - assert!(result.is_ok()); - } - - // ======================================================================== - // SUCCESSFUL RECALL TESTS - // ======================================================================== - - #[tokio::test] - async fn test_recall_basic_query_succeeds() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "The Rust programming language is memory safe.").await; - - let args = serde_json::json!({ "query": "rust" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["query"], "rust"); - assert!(value["total"].is_number()); - assert!(value["results"].is_array()); - } - - #[tokio::test] - async fn test_recall_returns_matching_content() { - let (storage, _dir) = test_storage().await; - let node_id = - ingest_test_content(&storage, "Python is a dynamic programming language.").await; - - let args = serde_json::json!({ "query": "python" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - assert!(!results.is_empty()); - assert_eq!(results[0]["id"], node_id); - } - - #[tokio::test] - async fn test_recall_with_limit() { - let (storage, _dir) = test_storage().await; - // Ingest multiple items - ingest_test_content(&storage, "Testing content one").await; - ingest_test_content(&storage, "Testing content two").await; - ingest_test_content(&storage, "Testing content three").await; - - let args = serde_json::json!({ - "query": "testing", - "limit": 2 - }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - assert!(results.len() <= 2); - } - - #[tokio::test] - async fn test_recall_empty_database_returns_empty_array() { - // With hybrid search (keyword + semantic), any query against content - // may return low-similarity matches. The true "no matches" case - // is an empty database. - let (storage, _dir) = test_storage().await; - // Don't ingest anything - database is empty - - let args = serde_json::json!({ "query": "anything" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["total"], 0); - assert!(value["results"].as_array().unwrap().is_empty()); - } - - #[tokio::test] - async fn test_recall_result_contains_expected_fields() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Testing field presence in recall results.").await; - - let args = serde_json::json!({ "query": "testing" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - if !results.is_empty() { - let first = &results[0]; - assert!(first["id"].is_string()); - assert!(first["content"].is_string()); - assert!(first["nodeType"].is_string()); - assert!(first["retentionStrength"].is_number()); - assert!(first["stability"].is_number()); - assert!(first["difficulty"].is_number()); - assert!(first["reps"].is_number()); - assert!(first["createdAt"].is_string()); - assert!(first["lastAccessed"].is_string()); - } - } - - // ======================================================================== - // DEFAULT VALUES TESTS - // ======================================================================== - - #[tokio::test] - async fn test_recall_default_limit_is_10() { - let (storage, _dir) = test_storage().await; - // Ingest more than 10 items - for i in 0..15 { - ingest_test_content(&storage, &format!("Item number {}", i)).await; - } - - let args = serde_json::json!({ "query": "item" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - assert!(results.len() <= 10); - } - - // ======================================================================== - // SCHEMA TESTS - // ======================================================================== - - #[test] - fn test_schema_has_required_fields() { - let schema_value = schema(); - assert_eq!(schema_value["type"], "object"); - assert!(schema_value["properties"]["query"].is_object()); - assert!( - schema_value["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("query")) - ); - } - - #[test] - fn test_schema_has_optional_fields() { - let schema_value = schema(); - assert!(schema_value["properties"]["limit"].is_object()); - assert!(schema_value["properties"]["min_retention"].is_object()); - } - - #[test] - fn test_schema_limit_has_bounds() { - let schema_value = schema(); - let limit_schema = &schema_value["properties"]["limit"]; - assert_eq!(limit_schema["minimum"], 1); - assert_eq!(limit_schema["maximum"], 100); - assert_eq!(limit_schema["default"], 10); - } - - #[test] - fn test_schema_min_retention_has_bounds() { - let schema_value = schema(); - let retention_schema = &schema_value["properties"]["min_retention"]; - assert_eq!(retention_schema["minimum"], 0.0); - assert_eq!(retention_schema["maximum"], 1.0); - assert_eq!(retention_schema["default"], 0.0); - } -} diff --git a/crates/vestige-mcp/src/tools/search.rs b/crates/vestige-mcp/src/tools/search.rs deleted file mode 100644 index 7653cec..0000000 --- a/crates/vestige-mcp/src/tools/search.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! Search Tools (Deprecated - use search_unified instead) -//! -//! Semantic and hybrid search implementations. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::Storage; - -/// Input schema for semantic_search tool -pub fn semantic_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query for semantic similarity" - }, - "limit": { - "type": "integer", - "description": "Maximum number of results (default: 10)", - "default": 10, - "minimum": 1, - "maximum": 50 - }, - "min_similarity": { - "type": "number", - "description": "Minimum similarity threshold (0.0-1.0, default: 0.5)", - "default": 0.5, - "minimum": 0.0, - "maximum": 1.0 - } - }, - "required": ["query"] - }) -} - -/// Input schema for hybrid_search tool -pub fn hybrid_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query" - }, - "limit": { - "type": "integer", - "description": "Maximum number of results (default: 10)", - "default": 10, - "minimum": 1, - "maximum": 50 - }, - "keyword_weight": { - "type": "number", - "description": "Weight for keyword search (0.0-1.0, default: 0.5)", - "default": 0.5, - "minimum": 0.0, - "maximum": 1.0 - }, - "semantic_weight": { - "type": "number", - "description": "Weight for semantic search (0.0-1.0, default: 0.5)", - "default": 0.5, - "minimum": 0.0, - "maximum": 1.0 - } - }, - "required": ["query"] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SemanticSearchArgs { - query: String, - limit: Option, - min_similarity: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct HybridSearchArgs { - query: String, - limit: Option, - keyword_weight: Option, - semantic_weight: Option, -} - -pub async fn execute_semantic( - storage: &Arc, - args: Option, -) -> Result { - let args: SemanticSearchArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.query.trim().is_empty() { - return Err("Query cannot be empty".to_string()); - } - - // Check if embeddings are ready - if !storage.is_embedding_ready() { - return Ok(serde_json::json!({ - "error": "Embedding service not ready", - "hint": "Run consolidation first to initialize embeddings, or the model may still be loading.", - })); - } - - let results = storage - .semantic_search( - &args.query, - args.limit.unwrap_or(10).clamp(1, 50), - args.min_similarity.unwrap_or(0.5).clamp(0.0, 1.0), - ) - .map_err(|e| e.to_string())?; - - let formatted: Vec = results - .iter() - .map(|r| { - serde_json::json!({ - "id": r.node.id, - "content": r.node.content, - "similarity": r.similarity, - "nodeType": r.node.node_type, - "tags": r.node.tags, - "retentionStrength": r.node.retention_strength, - }) - }) - .collect(); - - Ok(serde_json::json!({ - "query": args.query, - "method": "semantic", - "total": formatted.len(), - "results": formatted, - })) -} - -pub async fn execute_hybrid(storage: &Arc, args: Option) -> Result { - let args: HybridSearchArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.query.trim().is_empty() { - return Err("Query cannot be empty".to_string()); - } - - let results = storage - .hybrid_search( - &args.query, - args.limit.unwrap_or(10).clamp(1, 50), - args.keyword_weight.unwrap_or(0.3).clamp(0.0, 1.0), - args.semantic_weight.unwrap_or(0.7).clamp(0.0, 1.0), - ) - .map_err(|e| e.to_string())?; - - let formatted: Vec = results - .iter() - .map(|r| { - serde_json::json!({ - "id": r.node.id, - "content": r.node.content, - "combinedScore": r.combined_score, - "keywordScore": r.keyword_score, - "semanticScore": r.semantic_score, - "matchType": format!("{:?}", r.match_type), - "nodeType": r.node.node_type, - "tags": r.node.tags, - "retentionStrength": r.node.retention_strength, - }) - }) - .collect(); - - Ok(serde_json::json!({ - "query": args.query, - "method": "hybrid", - "total": formatted.len(), - "results": formatted, - })) -} diff --git a/crates/vestige-mcp/src/tools/stats.rs b/crates/vestige-mcp/src/tools/stats.rs deleted file mode 100644 index f9f31aa..0000000 --- a/crates/vestige-mcp/src/tools/stats.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! Stats Tools (Deprecated - use memory_unified instead) -//! -//! Memory statistics and health check. - -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::{MemoryStats, Storage}; - -/// Input schema for get_stats tool -pub fn stats_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": {}, - }) -} - -/// Input schema for health_check tool -pub fn health_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": {}, - }) -} - -pub async fn execute_stats(storage: &Arc) -> Result { - let stats = storage.get_stats().map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "totalNodes": stats.total_nodes, - "nodesDueForReview": stats.nodes_due_for_review, - "averageRetention": stats.average_retention, - "averageStorageStrength": stats.average_storage_strength, - "averageRetrievalStrength": stats.average_retrieval_strength, - "oldestMemory": stats.oldest_memory.map(|d| d.to_rfc3339()), - "newestMemory": stats.newest_memory.map(|d| d.to_rfc3339()), - "nodesWithEmbeddings": stats.nodes_with_embeddings, - "embeddingModel": stats.embedding_model, - "embeddingServiceReady": storage.is_embedding_ready(), - })) -} - -pub async fn execute_health(storage: &Arc) -> Result { - let stats = storage.get_stats().map_err(|e| e.to_string())?; - - // Determine health status - let status = if stats.total_nodes == 0 { - "empty" - } else if stats.average_retention < 0.3 { - "critical" - } else if stats.average_retention < 0.5 { - "degraded" - } else { - "healthy" - }; - - let mut warnings = Vec::new(); - - if stats.average_retention < 0.5 && stats.total_nodes > 0 { - warnings.push( - "Low average retention - consider running consolidation or reviewing memories" - .to_string(), - ); - } - - if stats.nodes_due_for_review > 10 { - warnings.push(format!( - "{} memories are due for review", - stats.nodes_due_for_review - )); - } - - if stats.total_nodes > 0 && stats.nodes_with_embeddings == 0 { - warnings.push( - "No embeddings generated - semantic search unavailable. Run consolidation.".to_string(), - ); - } - - let embedding_coverage = if stats.total_nodes > 0 { - (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 - } else { - 0.0 - }; - - if embedding_coverage < 50.0 && stats.total_nodes > 10 { - warnings.push(format!( - "Only {:.1}% of memories have embeddings", - embedding_coverage - )); - } - - Ok(serde_json::json!({ - "status": status, - "totalNodes": stats.total_nodes, - "nodesDueForReview": stats.nodes_due_for_review, - "averageRetention": stats.average_retention, - "embeddingCoverage": format!("{:.1}%", embedding_coverage), - "embeddingServiceReady": storage.is_embedding_ready(), - "warnings": warnings, - "recommendations": get_recommendations(&stats, status), - })) -} - -fn get_recommendations(stats: &MemoryStats, status: &str) -> Vec { - let mut recommendations = Vec::new(); - - if status == "critical" { - recommendations.push("CRITICAL: Many memories have very low retention. Review important memories with 'mark_reviewed'.".to_string()); - } - - if stats.nodes_due_for_review > 5 { - recommendations.push("Review due memories to strengthen retention.".to_string()); - } - - if stats.nodes_with_embeddings < stats.total_nodes { - recommendations.push( - "Run 'run_consolidation' to generate embeddings for better semantic search." - .to_string(), - ); - } - - if stats.total_nodes > 100 && stats.average_retention < 0.7 { - recommendations - .push("Consider running periodic consolidation to maintain memory health.".to_string()); - } - - if recommendations.is_empty() { - recommendations.push("Memory system is healthy!".to_string()); - } - - recommendations -} diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 52c6b08..f80bf00 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -31,10 +31,16 @@ export FASTEMBED_CACHE_PATH="/custom/path" | Variable | Default | Description | |----------|---------|-------------| -| `VESTIGE_DATA_DIR` | Platform default | Custom database location | -| `VESTIGE_LOG_LEVEL` | `info` | Logging verbosity | -| `RUST_LOG` | - | Detailed tracing output | +| `RUST_LOG` | `info` (via tracing-subscriber) | Log verbosity + per-module filtering | | `FASTEMBED_CACHE_PATH` | `./.fastembed_cache` | Embedding model cache location | +| `VESTIGE_DASHBOARD_PORT` | `3927` | Dashboard HTTP + WebSocket port | +| `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port | +| `VESTIGE_HTTP_BIND` | `127.0.0.1` | HTTP bind address | +| `VESTIGE_AUTH_TOKEN` | auto-generated | Dashboard + MCP HTTP bearer auth | +| `VESTIGE_DASHBOARD_ENABLED` | `true` | Set `false` to disable the web dashboard | +| `VESTIGE_CONSOLIDATION_INTERVAL_HOURS` | `6` | FSRS-6 decay cycle cadence | + +> **Storage location** is controlled by the `--data-dir ` CLI flag (see below), not an env var. Default is your OS's per-user data directory: `~/Library/Application Support/com.vestige.core/` on macOS, `~/.local/share/vestige/` on Linux, `%APPDATA%\vestige\core\data\` on Windows. --- diff --git a/docs/VESTIGE_STATE_AND_PLAN.md b/docs/VESTIGE_STATE_AND_PLAN.md index da1f2c2..ff753e6 100644 --- a/docs/VESTIGE_STATE_AND_PLAN.md +++ b/docs/VESTIGE_STATE_AND_PLAN.md @@ -396,7 +396,6 @@ Binary crate. Wraps `vestige-core` behind an MCP JSON-RPC 2.0 server, plus an em | Var | Default | Purpose | |---|---|---| -| `VESTIGE_DATA_DIR` | `~/.vestige/` | Storage root | | `VESTIGE_DASHBOARD_PORT` | `3927` | Dashboard HTTP + WebSocket port | | `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port | | `VESTIGE_HTTP_BIND` | `127.0.0.1` | HTTP bind address | @@ -706,7 +705,7 @@ vestige-cloud/ ├── Cargo.toml # binary: vestige-http └── src/ ├── main.rs # Axum server on :3927, auth + cors middleware - ├── auth.rs # Single bearer token via VESTIGE_API_KEY env, open if unset + ├── auth.rs # Single bearer token via VESTIGE_AUTH_TOKEN env (auto-generated if unset, stored in data-dir) ├── cors.rs # prod: allowlist vestige.dev + app.vestige.dev; dev: permissive ├── state.rs # Arc> shared state (SINGLE TENANT) ├── sse.rs # /mcp/sse STUB — 3 TODOs, returns one static "endpoint" event diff --git a/packages/vestige-mcp-npm/README.md b/packages/vestige-mcp-npm/README.md index f0d6ee7..3ca06fb 100644 --- a/packages/vestige-mcp-npm/README.md +++ b/packages/vestige-mcp-npm/README.md @@ -91,9 +91,12 @@ export FASTEMBED_CACHE_PATH="$HOME/.fastembed_cache" | Variable | Description | Default | |----------|-------------|---------| -| `VESTIGE_DATA_DIR` | Data storage directory | `~/.vestige` | -| `VESTIGE_LOG_LEVEL` | Log verbosity | `info` | -| `FASTEMBED_CACHE_PATH` | Embeddings model location | `./.fastembed_cache` | +| `RUST_LOG` | Log verbosity + per-module filter | `info` | +| `FASTEMBED_CACHE_PATH` | Embeddings model cache | `./.fastembed_cache` | +| `VESTIGE_DASHBOARD_PORT` | Dashboard port | `3927` | +| `VESTIGE_AUTH_TOKEN` | Bearer auth for dashboard + HTTP MCP | auto-generated | + +Storage location is the `--data-dir ` CLI flag (defaults to your OS's per-user data directory). ## Troubleshooting