chore: remove 3,091 LOC of orphan code + fix ghost env-var docs
Some checks are pending
CI / Test (macos-latest) (push) Waiting to run
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Release Build (aarch64-apple-darwin) (push) Blocked by required conditions
CI / Release Build (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
CI / Release Build (x86_64-apple-darwin) (push) Blocked by required conditions
Test Suite / Unit Tests (push) Waiting to run
Test Suite / MCP E2E Tests (push) Waiting to run
Test Suite / User Journey Tests (push) Blocked by required conditions
Test Suite / Dashboard Build (push) Waiting to run
Test Suite / Code Coverage (push) Waiting to run

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::<name>::, 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.
This commit is contained in:
Sam Valladares 2026-04-23 03:18:53 -05:00
parent 6a807698ef
commit 0e9b260518
14 changed files with 26 additions and 3117 deletions

View file

@ -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 {

View file

@ -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<CheckpointItem>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CheckpointItem {
content: String,
tags: Option<Vec<String>>,
node_type: Option<String>,
source: Option<String>,
}
pub async fn execute(storage: &Arc<Storage>, args: Option<Value>) -> Result<Value, String> {
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<Storage>, 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<serde_json::Value> = (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<serde_json::Value> = (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);
}
}

View file

@ -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<Vec<String>>,
codebase: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DecisionArgs {
decision: String,
rationale: String,
alternatives: Option<Vec<String>>,
files: Option<Vec<String>>,
codebase: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ContextArgs {
codebase: Option<String>,
limit: Option<i32>,
}
pub async fn execute_pattern(storage: &Arc<Storage>, args: Option<Value>) -> Result<Value, String> {
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<Storage>,
args: Option<Value>,
) -> Result<Value, String> {
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<Storage>, args: Option<Value>) -> Result<Value, String> {
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<Value> = 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<Value> = 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,
},
}))
}

View file

@ -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<Storage>) -> Result<Value, String> {
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
),
}))
}

View file

@ -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<String>,
tags: Option<Vec<String>>,
source: Option<String>,
}
pub async fn execute(
storage: &Arc<Storage>,
cognitive: &Arc<Mutex<CognitiveEngine>>,
args: Option<Value>,
) -> Result<Value, String> {
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<Mutex<CognitiveEngine>>,
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<Mutex<CognitiveEngine>> {
Arc::new(Mutex::new(CognitiveEngine::new()))
}
/// Create a test storage instance with a temporary database
async fn test_storage() -> (Arc<Storage>, 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<T, E> 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());
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<Storage>, args: Option<Value>) -> Result<Value, String> {
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<Storage>, args: Option<Value>) -> Result<Value, String> {
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" },
}))
}

View file

@ -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;

View file

@ -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<i32>,
min_retention: Option<f64>,
}
pub async fn execute(storage: &Arc<Storage>, args: Option<Value>) -> Result<Value, String> {
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<Value> = 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<Storage>, 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<Storage>, 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);
}
}

View file

@ -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<i32>,
min_similarity: Option<f32>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct HybridSearchArgs {
query: String,
limit: Option<i32>,
keyword_weight: Option<f32>,
semantic_weight: Option<f32>,
}
pub async fn execute_semantic(
storage: &Arc<Storage>,
args: Option<Value>,
) -> Result<Value, String> {
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<Value> = 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<Storage>, args: Option<Value>) -> Result<Value, String> {
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<Value> = 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,
}))
}

View file

@ -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<Storage>) -> Result<Value, String> {
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<Storage>) -> Result<Value, String> {
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<String> {
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
}

View file

@ -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 <path>` 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.
---

View file

@ -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<Mutex<Storage>> shared state (SINGLE TENANT)
├── sse.rs # /mcp/sse STUB — 3 TODOs, returns one static "endpoint" event

View file

@ -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 <path>` CLI flag (defaults to your OS's per-user data directory).
## Troubleshooting