mirror of
https://github.com/samvallad33/vestige.git
synced 2026-04-25 00:36:22 +02:00
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
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:
parent
6a807698ef
commit
0e9b260518
14 changed files with 26 additions and 3117 deletions
|
|
@ -103,21 +103,6 @@ pub enum EmotionCategory {
|
||||||
Neutral,
|
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 {
|
impl std::fmt::Display for EmotionCategory {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
@ -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" },
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
@ -43,35 +43,23 @@ pub mod cross_reference;
|
||||||
// v2.0.5: Active Forgetting — Anderson 2025 + Davis Rac1
|
// v2.0.5: Active Forgetting — Anderson 2025 + Davis Rac1
|
||||||
pub mod suppress;
|
pub mod suppress;
|
||||||
|
|
||||||
// Deprecated/internal tools — not advertised in the public MCP tools/list,
|
// Internal/backwards-compat tools still dispatched by server.rs for specific
|
||||||
// but some functions are actively dispatched for backwards compatibility
|
// tool names. Each module below has live callers via string dispatch in
|
||||||
// and internal cognitive operations. #[allow(dead_code)] suppresses warnings
|
// `server.rs` (match arms on request.name). The #[allow(dead_code)]
|
||||||
// for the unused schema/struct items within these modules.
|
// suppresses warnings for the per-module schema/struct items that aren't
|
||||||
#[allow(dead_code)]
|
// yet consumed.
|
||||||
pub mod checkpoint;
|
//
|
||||||
#[allow(dead_code)]
|
// The nine legacy siblings here pre-v2.0.8 (checkpoint, codebase, consolidate,
|
||||||
pub mod codebase;
|
// ingest, intentions, knowledge, recall, search, stats) were removed in the
|
||||||
#[allow(dead_code)]
|
// post-v2.0.8 dead-code sweep — all nine had zero callers after the
|
||||||
pub mod consolidate;
|
// unification work landed `*_unified` + `maintenance::*` replacements.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub mod context;
|
pub mod context;
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub mod feedback;
|
pub mod feedback;
|
||||||
#[allow(dead_code)]
|
#[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;
|
pub mod memory_states;
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub mod recall;
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub mod review;
|
pub mod review;
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub mod search;
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub mod stats;
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub mod tagging;
|
pub mod tagging;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -31,10 +31,16 @@ export FASTEMBED_CACHE_PATH="/custom/path"
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `VESTIGE_DATA_DIR` | Platform default | Custom database location |
|
| `RUST_LOG` | `info` (via tracing-subscriber) | Log verbosity + per-module filtering |
|
||||||
| `VESTIGE_LOG_LEVEL` | `info` | Logging verbosity |
|
|
||||||
| `RUST_LOG` | - | Detailed tracing output |
|
|
||||||
| `FASTEMBED_CACHE_PATH` | `./.fastembed_cache` | Embedding model cache location |
|
| `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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -396,7 +396,6 @@ Binary crate. Wraps `vestige-core` behind an MCP JSON-RPC 2.0 server, plus an em
|
||||||
|
|
||||||
| Var | Default | Purpose |
|
| Var | Default | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `VESTIGE_DATA_DIR` | `~/.vestige/` | Storage root |
|
|
||||||
| `VESTIGE_DASHBOARD_PORT` | `3927` | Dashboard HTTP + WebSocket port |
|
| `VESTIGE_DASHBOARD_PORT` | `3927` | Dashboard HTTP + WebSocket port |
|
||||||
| `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port |
|
| `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port |
|
||||||
| `VESTIGE_HTTP_BIND` | `127.0.0.1` | HTTP bind address |
|
| `VESTIGE_HTTP_BIND` | `127.0.0.1` | HTTP bind address |
|
||||||
|
|
@ -706,7 +705,7 @@ vestige-cloud/
|
||||||
├── Cargo.toml # binary: vestige-http
|
├── Cargo.toml # binary: vestige-http
|
||||||
└── src/
|
└── src/
|
||||||
├── main.rs # Axum server on :3927, auth + cors middleware
|
├── 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
|
├── cors.rs # prod: allowlist vestige.dev + app.vestige.dev; dev: permissive
|
||||||
├── state.rs # Arc<Mutex<Storage>> shared state (SINGLE TENANT)
|
├── state.rs # Arc<Mutex<Storage>> shared state (SINGLE TENANT)
|
||||||
├── sse.rs # /mcp/sse STUB — 3 TODOs, returns one static "endpoint" event
|
├── sse.rs # /mcp/sse STUB — 3 TODOs, returns one static "endpoint" event
|
||||||
|
|
|
||||||
|
|
@ -91,9 +91,12 @@ export FASTEMBED_CACHE_PATH="$HOME/.fastembed_cache"
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| `VESTIGE_DATA_DIR` | Data storage directory | `~/.vestige` |
|
| `RUST_LOG` | Log verbosity + per-module filter | `info` |
|
||||||
| `VESTIGE_LOG_LEVEL` | Log verbosity | `info` |
|
| `FASTEMBED_CACHE_PATH` | Embeddings model cache | `./.fastembed_cache` |
|
||||||
| `FASTEMBED_CACHE_PATH` | Embeddings model location | `./.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
|
## Troubleshooting
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue