Release v2.1.0
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

This commit is contained in:
Sam Valladares 2026-04-27 13:20:51 -05:00
parent 694e837898
commit d4313df759
106 changed files with 2900 additions and 128 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "vestige-core"
version = "2.0.9"
version = "2.1.0"
edition = "2024"
rust-version = "1.91"
authors = ["Vestige Team"]

View file

@ -738,7 +738,10 @@ mod tests {
// 1. schema_version advanced to V11
let version = get_current_version(&conn).expect("read schema_version");
assert_eq!(version, 11, "schema_version must be 11 after all migrations");
assert_eq!(
version, 11,
"schema_version must be 11 after all migrations"
);
// 2. knowledge_edges is gone (V11 drops it)
let knowledge_edges_rows: i64 = conn

View file

@ -1,6 +1,6 @@
[package]
name = "vestige-mcp"
version = "2.0.9"
version = "2.1.0"
edition = "2024"
description = "Cognitive memory MCP server for Claude - FSRS-6, spreading activation, synaptic tagging, 3D dashboard, and 130 years of memory research"
authors = ["samvallad33"]
@ -44,7 +44,7 @@ path = "src/bin/cli.rs"
# Only `bundled-sqlite` is always on. `embeddings` and `vector-search` are
# toggled via vestige-mcp's own feature flags below so `--no-default-features`
# actually works (previously hardcoded here, which silently defeated the flag).
vestige-core = { version = "2.0.8", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] }
vestige-core = { version = "2.1.0", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] }
# ============================================================================
# MCP Server Dependencies

View file

@ -153,10 +153,8 @@ pub fn spawn(
backoff_secs = SUPERVISOR_RESTART_BACKOFF_SECS,
"Autopilot event subscriber panicked — supervisor restarting"
);
tokio::time::sleep(Duration::from_secs(
SUPERVISOR_RESTART_BACKOFF_SECS,
))
.await;
tokio::time::sleep(Duration::from_secs(SUPERVISOR_RESTART_BACKOFF_SECS))
.await;
}
Err(e) => {
warn!(error = ?e, "Autopilot event subscriber join error — exiting");
@ -187,10 +185,8 @@ pub fn spawn(
backoff_secs = SUPERVISOR_RESTART_BACKOFF_SECS,
"Autopilot prospective poller panicked — supervisor restarting"
);
tokio::time::sleep(Duration::from_secs(
SUPERVISOR_RESTART_BACKOFF_SECS,
))
.await;
tokio::time::sleep(Duration::from_secs(SUPERVISOR_RESTART_BACKOFF_SECS))
.await;
}
Err(e) => {
warn!(error = ?e, "Autopilot prospective poller join error — exiting");
@ -218,14 +214,7 @@ async fn run_event_subscriber(
loop {
match rx.recv().await {
Ok(event) => {
handle_event(
event,
&cognitive,
&storage,
&event_tx,
&mut dedup_state,
)
.await;
handle_event(event, &cognitive, &storage, &event_tx, &mut dedup_state).await;
}
Err(broadcast::error::RecvError::Lagged(n)) => {
warn!("Autopilot lagged {n} events — increase channel capacity if this persists");

View file

@ -2,10 +2,12 @@
//!
//! v2.0: Adds cognitive operation endpoints (dream, explore, predict, importance, consolidation)
use std::cmp::Reverse;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{Json, Redirect};
use chrono::{Duration, Utc};
use chrono::{DateTime, Duration, Utc};
use serde::Deserialize;
use serde_json::Value;
@ -373,6 +375,13 @@ pub struct TimelineParams {
pub limit: Option<i32>,
}
#[derive(Debug, Deserialize)]
pub struct ChangelogParams {
pub start: Option<String>,
pub end: Option<String>,
pub limit: Option<i32>,
}
/// Get timeline data
pub async fn get_timeline(
State(state): State<AppState>,
@ -428,6 +437,108 @@ pub async fn get_timeline(
})))
}
/// Recent cognitive events in the same envelope used by the WebSocket event
/// stream. The pulse hook polls this endpoint once per Claude wake, so keep it
/// cheap, bounded, and tolerant of empty history.
pub async fn get_changelog(
State(state): State<AppState>,
Query(params): Query<ChangelogParams>,
) -> Result<Json<Value>, StatusCode> {
let limit = params.limit.unwrap_or(50).clamp(1, 100);
let start = parse_changelog_bound(params.start.as_deref())?;
let end = parse_changelog_bound(params.end.as_deref())?;
let fetch_limit = if start.is_some() || end.is_some() {
limit.saturating_mul(4)
} else {
limit
};
let mut events: Vec<(DateTime<Utc>, Value)> = Vec::new();
let dreams = state
.storage
.get_dream_history(fetch_limit)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
for dream in dreams {
if changelog_window_contains(dream.dreamed_at, start.as_ref(), end.as_ref()) {
events.push((dream.dreamed_at, dream_changelog_event(&dream)));
}
}
// Connections are currently persisted as graph edges rather than as audit
// rows, so filter by created_at from the connection table.
let connections = state
.storage
.get_all_connections()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
for conn in connections {
if changelog_window_contains(conn.created_at, start.as_ref(), end.as_ref()) {
events.push((conn.created_at, connection_changelog_event(&conn)));
}
}
events.sort_by_key(|event| Reverse(event.0));
events.truncate(limit as usize);
let formatted_events: Vec<Value> = events.into_iter().map(|(_, event)| event).collect();
let total_events = formatted_events.len();
Ok(Json(serde_json::json!({
"events": formatted_events,
"totalEvents": total_events,
"filter": {
"start": start.as_ref().map(DateTime::to_rfc3339),
"end": end.as_ref().map(DateTime::to_rfc3339),
"limit": limit,
},
})))
}
fn parse_changelog_bound(raw: Option<&str>) -> Result<Option<DateTime<Utc>>, StatusCode> {
match raw {
Some(value) if !value.trim().is_empty() => DateTime::parse_from_rfc3339(value)
.map(|dt| Some(dt.with_timezone(&Utc)))
.map_err(|_| StatusCode::BAD_REQUEST),
_ => Ok(None),
}
}
fn changelog_window_contains(
ts: DateTime<Utc>,
start: Option<&DateTime<Utc>>,
end: Option<&DateTime<Utc>>,
) -> bool {
start.is_none_or(|s| ts >= *s) && end.is_none_or(|e| ts <= *e)
}
fn dream_changelog_event(dream: &vestige_core::DreamHistoryRecord) -> Value {
serde_json::json!({
"type": "DreamCompleted",
"timestamp": dream.dreamed_at.to_rfc3339(),
"data": {
"memories_replayed": dream.memories_replayed,
"connections_found": dream.connections_found,
"connections_persisted": dream.connections_found,
"insights_generated": dream.insights_generated,
"duration_ms": dream.duration_ms,
"timestamp": dream.dreamed_at.to_rfc3339(),
},
})
}
fn connection_changelog_event(conn: &vestige_core::ConnectionRecord) -> Value {
serde_json::json!({
"type": "ConnectionDiscovered",
"timestamp": conn.created_at.to_rfc3339(),
"data": {
"source_id": &conn.source_id,
"target_id": &conn.target_id,
"connection_type": &conn.link_type,
"weight": conn.strength,
"timestamp": conn.created_at.to_rfc3339(),
},
})
}
/// Health check
pub async fn health_check(State(state): State<AppState>) -> Result<Json<Value>, StatusCode> {
let stats = state
@ -537,7 +648,9 @@ pub async fn get_graph(
&& edges.is_empty()
&& let Ok(fallback) = default_center_id(&state.storage, GraphSort::Connected)
&& fallback != center_id
&& let Ok((n2, e2)) = state.storage.get_memory_subgraph(&fallback, depth, max_nodes)
&& let Ok((n2, e2)) = state
.storage
.get_memory_subgraph(&fallback, depth, max_nodes)
&& n2.len() > nodes.len()
{
center_id = fallback;
@ -793,14 +906,38 @@ pub async fn trigger_dream(State(state): State<AppState>) -> Result<Json<Value>,
}
let duration_ms = start.elapsed().as_millis() as u64;
let completed_at = Utc::now();
let insights_generated = insights.len();
if let Err(e) = state
.storage
.save_dream_history(&vestige_core::DreamHistoryRecord {
dreamed_at: completed_at,
duration_ms: duration_ms as i64,
memories_replayed: dream_memories.len() as i32,
connections_found: connections_persisted as i32,
insights_generated: insights_generated as i32,
memories_strengthened: dream_result.memories_strengthened as i32,
memories_compressed: dream_result.memories_compressed as i32,
phase_nrem1_ms: None,
phase_nrem3_ms: None,
phase_rem_ms: None,
phase_integration_ms: None,
summaries_generated: None,
emotional_memories_processed: None,
creative_connections_found: None,
})
{
tracing::warn!("Failed to persist dashboard dream history: {}", e);
}
// Emit completion event
state.emit(VestigeEvent::DreamCompleted {
memories_replayed: dream_memories.len(),
connections_found: connections_persisted as usize,
insights_generated: insights.len(),
insights_generated,
duration_ms,
timestamp: Utc::now(),
timestamp: completed_at,
});
Ok(Json(serde_json::json!({
@ -1303,7 +1440,7 @@ mod tests {
use std::sync::Arc;
use tempfile::tempdir;
use vestige_core::memory::IngestInput;
use vestige_core::{ConnectionRecord, Storage};
use vestige_core::{ConnectionRecord, DreamHistoryRecord, Storage};
#[test]
fn graph_sort_parse_defaults_to_recent() {
@ -1322,6 +1459,51 @@ mod tests {
assert_eq!(GraphSort::parse(Some("Connected")), GraphSort::Connected);
}
#[test]
fn changelog_dream_event_uses_pulse_compatible_shape() {
let now = Utc::now();
let event = dream_changelog_event(&DreamHistoryRecord {
dreamed_at: now,
duration_ms: 1234,
memories_replayed: 12,
connections_found: 3,
insights_generated: 2,
memories_strengthened: 0,
memories_compressed: 0,
phase_nrem1_ms: None,
phase_nrem3_ms: None,
phase_rem_ms: None,
phase_integration_ms: None,
summaries_generated: None,
emotional_memories_processed: None,
creative_connections_found: None,
});
assert_eq!(event["type"], "DreamCompleted");
assert_eq!(event["data"]["insights_generated"], 2);
assert_eq!(event["data"]["connections_persisted"], 3);
assert_eq!(event["data"]["timestamp"], now.to_rfc3339());
}
#[test]
fn changelog_connection_event_uses_pulse_compatible_shape() {
let now = Utc::now();
let event = connection_changelog_event(&ConnectionRecord {
source_id: "source-memory".to_string(),
target_id: "target-memory".to_string(),
strength: 0.82,
link_type: "semantic".to_string(),
created_at: now,
last_activated: now,
activation_count: 1,
});
assert_eq!(event["type"], "ConnectionDiscovered");
assert_eq!(event["data"]["source_id"], "source-memory");
assert_eq!(event["data"]["target_id"], "target-memory");
assert_eq!(event["data"]["connection_type"], "semantic");
}
fn seed_storage() -> (tempfile::TempDir, Arc<Storage>) {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");

View file

@ -142,7 +142,10 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
// since v2.0.5 despite having full graph event handlers; this closes
// the gap so dashboard users can trigger inhibition without dropping
// to the MCP layer.
.route("/api/memories/{id}/suppress", post(handlers::suppress_memory))
.route(
"/api/memories/{id}/suppress",
post(handlers::suppress_memory),
)
.route(
"/api/memories/{id}/unsuppress",
post(handlers::unsuppress_memory),
@ -154,6 +157,7 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
.route("/api/health", get(handlers::health_check))
// Timeline
.route("/api/timeline", get(handlers::get_timeline))
.route("/api/changelog", get(handlers::get_changelog))
// Graph
.route("/api/graph", get(handlers::get_graph))
// Cognitive operations (v2.0)
@ -171,10 +175,7 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) {
// Reasoning Theater (v2.0.8) — 8-stage cognitive pipeline surface.
// Wraps crate::tools::cross_reference::execute. Emits
// DeepReferenceCompleted so Graph3D can glide, pulse, and arc.
.route(
"/api/deep_reference",
post(handlers::deep_reference_query),
)
.route("/api/deep_reference", post(handlers::deep_reference_query))
.layer(
ServiceBuilder::new()
.concurrency_limit(50)

View file

@ -31,8 +31,11 @@ use vestige_mcp::cognitive;
use vestige_mcp::protocol;
use vestige_mcp::server;
use directories::BaseDirs;
use std::ffi::OsString;
use std::fs;
use std::io;
use std::path::PathBuf;
use std::path::{Component, PathBuf};
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::{Level, error, info, warn};
@ -44,6 +47,9 @@ use vestige_core::Storage;
use protocol::stdio::StdioTransport;
use server::McpServer;
const DATA_DIR_ENV: &str = "VESTIGE_DATA_DIR";
const DATABASE_FILE: &str = "vestige.db";
/// Parsed CLI configuration.
struct Config {
data_dir: Option<PathBuf>,
@ -51,11 +57,24 @@ struct Config {
dashboard_enabled: bool,
}
fn data_dir_from_env() -> Option<PathBuf> {
std::env::var_os(DATA_DIR_ENV).and_then(|value| {
if value.as_os_str().is_empty() {
None
} else {
Some(PathBuf::from(value))
}
})
}
/// Parse command-line arguments into a `Config`.
/// Exits the process if `--help` or `--version` is requested.
fn parse_args() -> Config {
let args: Vec<String> = std::env::args().collect();
let mut data_dir: Option<PathBuf> = None;
parse_args_from(std::env::args_os().collect(), data_dir_from_env())
}
fn parse_args_from(args: Vec<OsString>, env_data_dir: Option<PathBuf>) -> Config {
let mut data_dir = env_data_dir;
let mut http_port: u16 = std::env::var("VESTIGE_HTTP_PORT")
.ok()
.and_then(|s| s.parse().ok())
@ -66,7 +85,8 @@ fn parse_args() -> Config {
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
let arg = args[i].to_string_lossy();
match arg.as_ref() {
"--help" | "-h" => {
println!("Vestige MCP Server v{}", env!("CARGO_PKG_VERSION"));
println!();
@ -78,10 +98,15 @@ fn parse_args() -> Config {
println!("OPTIONS:");
println!(" -h, --help Print help information");
println!(" -V, --version Print version information");
println!(" --data-dir <PATH> Custom data directory");
println!(
" --data-dir <PATH> Custom data directory (overrides VESTIGE_DATA_DIR)"
);
println!(" --http-port <PORT> HTTP transport port (default: 3928)");
println!();
println!("ENVIRONMENT:");
println!(
" VESTIGE_DATA_DIR Data directory fallback (stores vestige.db inside)"
);
println!(
" RUST_LOG Log level filter (e.g., debug, info, warn, error)"
);
@ -98,6 +123,7 @@ fn parse_args() -> Config {
println!("EXAMPLES:");
println!(" vestige-mcp");
println!(" vestige-mcp --data-dir /custom/path");
println!(" VESTIGE_DATA_DIR=~/.vestige vestige-mcp");
println!(" vestige-mcp --http-port 8080");
println!(" RUST_LOG=debug vestige-mcp");
std::process::exit(0);
@ -113,6 +139,11 @@ fn parse_args() -> Config {
eprintln!("Usage: vestige-mcp --data-dir <PATH>");
std::process::exit(1);
}
if args[i].as_os_str().is_empty() {
eprintln!("error: --data-dir requires a non-empty path argument");
eprintln!("Usage: vestige-mcp --data-dir <PATH>");
std::process::exit(1);
}
data_dir = Some(PathBuf::from(&args[i]));
}
arg if arg.starts_with("--data-dir=") => {
@ -132,10 +163,11 @@ fn parse_args() -> Config {
eprintln!("Usage: vestige-mcp --http-port <PORT>");
std::process::exit(1);
}
http_port = match args[i].parse() {
let port = args[i].to_string_lossy();
http_port = match port.parse() {
Ok(p) => p,
Err(_) => {
eprintln!("error: invalid port number '{}'", args[i]);
eprintln!("error: invalid port number '{}'", port);
std::process::exit(1);
}
};
@ -167,6 +199,42 @@ fn parse_args() -> Config {
}
}
fn expand_tilde(path: PathBuf) -> PathBuf {
let rest = {
let mut components = path.components();
match components.next() {
Some(Component::Normal(first)) if first == "~" => {
Some(components.as_path().to_path_buf())
}
_ => None,
}
};
match rest {
Some(rest) => BaseDirs::new()
.map(|dirs| dirs.home_dir().join(rest))
.unwrap_or(path),
None => path,
}
}
fn prepare_storage_path(data_dir: Option<PathBuf>) -> io::Result<Option<PathBuf>> {
let Some(data_dir) = data_dir else {
return Ok(None);
};
let data_dir = expand_tilde(data_dir);
fs::create_dir_all(&data_dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700));
}
Ok(Some(data_dir.join(DATABASE_FILE)))
}
#[tokio::main]
async fn main() {
// Parse CLI arguments first (before logging init, so --help/--version work cleanly)
@ -185,8 +253,17 @@ async fn main() {
env!("CARGO_PKG_VERSION")
);
// Initialize storage with optional custom data directory
let storage = match Storage::new(config.data_dir) {
let storage_path = match prepare_storage_path(config.data_dir) {
Ok(path) => path,
Err(e) => {
error!("Failed to prepare storage data directory: {}", e);
std::process::exit(1);
}
};
// Initialize storage with optional custom data directory.
// Storage::new(Some(...)) expects a DB file path, so map data dirs to vestige.db here.
let storage = match Storage::new(storage_path) {
Ok(s) => {
info!("Storage initialized successfully");
@ -417,3 +494,57 @@ async fn main() {
info!("Vestige MCP Server shutting down");
}
#[cfg(test)]
mod tests {
use super::*;
fn os_args(args: &[&str]) -> Vec<OsString> {
args.iter().map(OsString::from).collect()
}
#[test]
fn vestige_data_dir_env_is_used_when_cli_data_dir_is_absent() {
let config = parse_args_from(
os_args(&["vestige-mcp"]),
Some(PathBuf::from("/tmp/vestige-env")),
);
assert_eq!(config.data_dir, Some(PathBuf::from("/tmp/vestige-env")));
}
#[test]
fn cli_data_dir_takes_precedence_over_env_data_dir() {
let config = parse_args_from(
os_args(&["vestige-mcp", "--data-dir", "/tmp/vestige-cli"]),
Some(PathBuf::from("/tmp/vestige-env")),
);
assert_eq!(config.data_dir, Some(PathBuf::from("/tmp/vestige-cli")));
}
#[test]
fn prepare_storage_path_creates_dir_and_points_to_vestige_db() {
let temp = tempfile::tempdir().unwrap();
let data_dir = temp.path().join("nested").join("vestige");
let db_path = prepare_storage_path(Some(data_dir.clone())).unwrap();
assert!(data_dir.is_dir());
assert_eq!(db_path, Some(data_dir.join(DATABASE_FILE)));
}
#[test]
fn expand_tilde_expands_current_users_home_only() {
let home = BaseDirs::new().unwrap().home_dir().to_path_buf();
assert_eq!(
expand_tilde(PathBuf::from("~/vestige")),
home.join("vestige")
);
assert_eq!(
expand_tilde(PathBuf::from("~other/vestige")),
PathBuf::from("~other/vestige")
);
}
}

View file

@ -694,11 +694,9 @@ pub async fn execute(
// embedding space — even though the winning memory contains neither
// "FSRS-6" nor anything about spaced repetition).
const TOPIC_STOPWORDS: &[&str] = &[
"how", "what", "when", "where", "why", "who", "which",
"does", "did", "is", "are", "was", "were", "will",
"the", "and", "for", "with", "this", "that",
"work", "works", "use", "uses", "used", "using",
"about", "from", "into", "than", "then",
"how", "what", "when", "where", "why", "who", "which", "does", "did", "is", "are", "was",
"were", "will", "the", "and", "for", "with", "this", "that", "work", "works", "use",
"uses", "used", "using", "about", "from", "into", "than", "then",
];
let topic_terms: Vec<String> = args
.query
@ -762,15 +760,12 @@ pub async fn execute(
&non_superseded_all
};
let recommended = primary_pool
.iter()
.copied()
.max_by(|a, b| {
composite(a)
.partial_cmp(&composite(b))
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.updated_at.cmp(&b.updated_at))
});
let recommended = primary_pool.iter().copied().max_by(|a, b| {
composite(a)
.partial_cmp(&composite(b))
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.updated_at.cmp(&b.updated_at))
});
// ====================================================================
// STAGE 7: Relation Assessment (per-pair, using trust + temporal + similarity)

View file

@ -114,15 +114,18 @@ pub async fn execute(
degraded = true;
Vec::new()
});
let accuracy = cog.predictive_memory.prediction_accuracy().unwrap_or_else(|e| {
tracing::warn!(
target: "vestige::predict",
error = %e,
"prediction_accuracy failed; returning 0.0"
);
degraded = true;
0.0
});
let accuracy = cog
.predictive_memory
.prediction_accuracy()
.unwrap_or_else(|e| {
tracing::warn!(
target: "vestige::predict",
error = %e,
"prediction_accuracy failed; returning 0.0"
);
degraded = true;
0.0
});
// Build speculative context
let speculative_context = vestige_core::PredictionContext {

View file

@ -67,26 +67,25 @@ pub async fn execute(storage: &Arc<Storage>, args: Option<Value>) -> Result<Valu
// Try parsing as wrapped format first (MCP response wrapper),
// then fall back to direct RecallResult
let memories: Vec<MemoryBackup> = if let Ok(wrapper) =
serde_json::from_str::<Vec<BackupWrapper>>(&backup_content)
{
if let Some(first) = wrapper.first() {
let recall: RecallResult = serde_json::from_str(&first.text)
.map_err(|e| format!("Failed to parse backup contents: {}", e))?;
let memories: Vec<MemoryBackup> =
if let Ok(wrapper) = serde_json::from_str::<Vec<BackupWrapper>>(&backup_content) {
if let Some(first) = wrapper.first() {
let recall: RecallResult = serde_json::from_str(&first.text)
.map_err(|e| format!("Failed to parse backup contents: {}", e))?;
recall.results
} else {
return Err("Empty backup file".to_string());
}
} else if let Ok(recall) = serde_json::from_str::<RecallResult>(&backup_content) {
recall.results
} else if let Ok(nodes) = serde_json::from_str::<Vec<MemoryBackup>>(&backup_content) {
nodes
} else {
return Err("Empty backup file".to_string());
}
} else if let Ok(recall) = serde_json::from_str::<RecallResult>(&backup_content) {
recall.results
} else if let Ok(nodes) = serde_json::from_str::<Vec<MemoryBackup>>(&backup_content) {
nodes
} else {
return Err(
return Err(
"Unrecognized backup format. Expected MCP wrapper, RecallResult, or array of memories."
.to_string(),
);
};
};
let total = memories.len();
if total == 0 {

View file

@ -207,12 +207,7 @@ mod tests {
/// Ingest with explicit node_type and tags. Used by the sparse-filter
/// regression tests so the dominant and sparse sets can be told apart.
async fn ingest_typed(
storage: &Arc<Storage>,
content: &str,
node_type: &str,
tags: &[&str],
) {
async fn ingest_typed(storage: &Arc<Storage>, content: &str, node_type: &str, tags: &[&str]) {
storage
.ingest(vestige_core::IngestInput {
content: content.to_string(),
@ -392,11 +387,23 @@ mod tests {
// Dominant set: 10 facts
for i in 0..10 {
ingest_typed(&storage, &format!("Dominant memory {}", i), "fact", &["alpha"]).await;
ingest_typed(
&storage,
&format!("Dominant memory {}", i),
"fact",
&["alpha"],
)
.await;
}
// Sparse set: 2 concepts
for i in 0..2 {
ingest_typed(&storage, &format!("Sparse memory {}", i), "concept", &["beta"]).await;
ingest_typed(
&storage,
&format!("Sparse memory {}", i),
"concept",
&["beta"],
)
.await;
}
// Limit 5 against 12 total — before the fix, `retain` on `concept`
@ -426,7 +433,13 @@ mod tests {
// Dominant set: 10 memories with tag "common"
for i in 0..10 {
ingest_typed(&storage, &format!("Common memory {}", i), "fact", &["common"]).await;
ingest_typed(
&storage,
&format!("Common memory {}", i),
"fact",
&["common"],
)
.await;
}
// Sparse set: 2 memories with tag "rare"
for i in 0..2 {