Archived
rename crates/ to engrams/, bindings/ to receptors/
- crates/ → engrams/ (Rust engrams live here) - bindings/ → receptors/ (cross-language access points into the graph) - Cargo.toml workspace paths updated
This commit is contained in:
Generated
+1
@@ -526,6 +526,7 @@ dependencies = [
|
||||
"instant-distance",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sled",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
|
||||
+12
-12
@@ -1,17 +1,17 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/engram-core",
|
||||
"crates/engram-ffi",
|
||||
"crates/engram-jni",
|
||||
"crates/engram-migrate",
|
||||
"crates/engram-sync",
|
||||
"crates/engram-server",
|
||||
"crates/engram-projection",
|
||||
"crates/engram-tx",
|
||||
"crates/engram-crypto",
|
||||
"crates/engram-reasoning",
|
||||
# engram-wasm is in bindings/ and compiled separately via wasm-pack
|
||||
"engrams/engram-core",
|
||||
"engrams/engram-ffi",
|
||||
"engrams/engram-jni",
|
||||
"engrams/engram-migrate",
|
||||
"engrams/engram-sync",
|
||||
"engrams/engram-server",
|
||||
"engrams/engram-projection",
|
||||
"engrams/engram-tx",
|
||||
"engrams/engram-crypto",
|
||||
"engrams/engram-reasoning",
|
||||
# engram-wasm is in receptors/ and compiled separately via wasm-pack
|
||||
# (wasm targets can't be in the same workspace build as native targets)
|
||||
]
|
||||
|
||||
@@ -34,4 +34,4 @@ edition = "2021"
|
||||
migration = ["engram-core/migration"]
|
||||
|
||||
[dependencies]
|
||||
engram-core = { path = "crates/engram-core" }
|
||||
engram-core = { path = "engrams/engram-core" }
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
segment_size: 524288
|
||||
use_compression: false
|
||||
version: 0.34
|
||||
vQ�
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
segment_size: 524288
|
||||
use_compression: false
|
||||
version: 0.34
|
||||
vQ�
|
||||
Binary file not shown.
Binary file not shown.
@@ -15,6 +15,7 @@ migration = ["dep:rusqlite"]
|
||||
sled = { version = "0.34", optional = true }
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
bincode = "1"
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
@@ -88,8 +88,9 @@ pub fn consolidate(db: &Db, config: &ConsolidationConfig) -> EngramResult<Consol
|
||||
&& node.salience >= config.salience_floor
|
||||
{
|
||||
// Promote: change tier to Semantic and persist.
|
||||
// Use overwrite_node — this is an internal state update, not a new node.
|
||||
node.tier = MemoryTier::Semantic;
|
||||
graph::put_node(db, &node)?;
|
||||
graph::overwrite_node(db, &node)?;
|
||||
promoted_count += 1;
|
||||
|
||||
if promoted_count >= config.max_promotions_per_run {
|
||||
@@ -108,7 +109,7 @@ pub fn consolidate(db: &Db, config: &ConsolidationConfig) -> EngramResult<Consol
|
||||
if new_sal != node.salience {
|
||||
node.salience = new_sal;
|
||||
storage::write_salience(db, node.id, new_sal)?;
|
||||
graph::put_node(db, &node)?;
|
||||
graph::overwrite_node(db, &node)?;
|
||||
decayed_count += 1;
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,12 @@
|
||||
mod sled_impl {
|
||||
use crate::activation;
|
||||
use crate::consolidation::{self, ConsolidationConfig, ConsolidationReport};
|
||||
use crate::edge_type;
|
||||
use crate::error::{EngramError, EngramResult};
|
||||
use crate::graph;
|
||||
use crate::salience;
|
||||
use crate::storage;
|
||||
use crate::types::{ActivatedNode, Edge, Node, RelationType, ScoredNode};
|
||||
use crate::types::{ActivatedNode, Edge, EdgeTypeDef, Node, ScoredNode, now_ms};
|
||||
use crate::vector;
|
||||
use sled::Db;
|
||||
use std::path::Path;
|
||||
@@ -28,16 +29,32 @@ mod sled_impl {
|
||||
pub(crate) db: Db,
|
||||
}
|
||||
|
||||
impl Clone for EngramDb {
|
||||
/// Clone shares the same underlying sled instance (single file lock,
|
||||
/// multiple in-process handles). This is safe and avoids re-opening.
|
||||
fn clone(&self) -> Self {
|
||||
Self { db: self.db.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl EngramDb {
|
||||
/// Open (or create) an engram database at the given path.
|
||||
///
|
||||
/// Seeds all built-in edge types on first open. Idempotent — existing
|
||||
/// definitions are not overwritten on subsequent opens.
|
||||
pub fn open(path: &Path) -> EngramResult<Self> {
|
||||
let db = sled::open(path)?;
|
||||
edge_type::seed_builtin_types(&db)?;
|
||||
Ok(Self { db })
|
||||
}
|
||||
|
||||
// ── Node operations ───────────────────────────────────────────────────
|
||||
|
||||
/// Persist a node. Returns the node's UUID.
|
||||
/// Persist a new node. Returns the node's UUID.
|
||||
///
|
||||
/// Returns `EngramError::NodeAlreadyExists` if the ID already exists.
|
||||
/// Nodes are immutable — to update, create a new node and add a
|
||||
/// `supersedes` edge from new → old.
|
||||
pub fn put_node(&self, node: Node) -> EngramResult<Uuid> {
|
||||
let id = graph::put_node(&self.db, &node)?;
|
||||
// Mark HNSW index dirty so next search rebuilds it.
|
||||
@@ -109,15 +126,90 @@ mod sled_impl {
|
||||
// ── Graph traversal ───────────────────────────────────────────────────
|
||||
|
||||
/// BFS traversal from `from`, following edges up to `max_depth` hops.
|
||||
///
|
||||
/// If `relation` is `Some("causes")`, only edges with that relation
|
||||
/// name are followed. Pass `None` to follow all edges.
|
||||
pub fn traverse(
|
||||
&self,
|
||||
from: Uuid,
|
||||
relation: Option<RelationType>,
|
||||
relation: Option<&str>,
|
||||
max_depth: u8,
|
||||
) -> EngramResult<Vec<Node>> {
|
||||
graph::traverse(&self.db, from, relation, max_depth)
|
||||
}
|
||||
|
||||
// ── Edge type registry (native bindings for el) ───────────────────────
|
||||
//
|
||||
// These are the el-callable surfaces. All intelligence about when to
|
||||
// create types, how to score confidence, and when to merge/split lives
|
||||
// in el. Rust stores and retrieves.
|
||||
|
||||
/// Create or update an edge type. If the type already exists its
|
||||
/// description and confidence are updated; id, first_observed,
|
||||
/// instance_count, derived_from, supersedes, and deprecated are preserved.
|
||||
pub fn native_edge_type_put(
|
||||
&self,
|
||||
name: &str,
|
||||
description: &str,
|
||||
confidence: f32,
|
||||
) -> EngramResult<()> {
|
||||
let confidence = confidence.clamp(0.0, 1.0);
|
||||
if let Some(mut existing) = edge_type::get_edge_type(&self.db, name)? {
|
||||
existing.description = description.to_string();
|
||||
existing.confidence = confidence;
|
||||
edge_type::register_edge_type(&self.db, &existing)?;
|
||||
} else {
|
||||
let def = EdgeTypeDef {
|
||||
id: Uuid::new_v4(),
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
first_observed: now_ms(),
|
||||
instance_count: 0,
|
||||
confidence,
|
||||
derived_from: None,
|
||||
supersedes: None,
|
||||
deprecated: false,
|
||||
};
|
||||
edge_type::register_edge_type(&self.db, &def)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve an edge type as a JSON string. Returns empty string if not found.
|
||||
pub fn native_edge_type_get(&self, name: &str) -> EngramResult<String> {
|
||||
match edge_type::get_edge_type(&self.db, name)? {
|
||||
Some(def) => Ok(serde_json::to_string(&def).unwrap_or_default()),
|
||||
None => Ok(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// List all registered edge types as a JSON array string.
|
||||
pub fn native_edge_type_list(&self) -> EngramResult<String> {
|
||||
let defs = edge_type::all_edge_types(&self.db)?;
|
||||
Ok(serde_json::to_string(&defs).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Increment the instance_count for a named edge type by one.
|
||||
pub fn native_edge_type_increment_count(&self, name: &str) -> EngramResult<()> {
|
||||
edge_type::increment_edge_type_count(&self.db, name)
|
||||
}
|
||||
|
||||
/// Update the description and confidence of an existing edge type.
|
||||
/// instance_count, id, first_observed, and other metadata are preserved.
|
||||
pub fn native_edge_type_update(
|
||||
&self,
|
||||
name: &str,
|
||||
description: &str,
|
||||
confidence: f32,
|
||||
) -> EngramResult<()> {
|
||||
if let Some(mut def) = edge_type::get_edge_type(&self.db, name)? {
|
||||
def.description = description.to_string();
|
||||
def.confidence = confidence.clamp(0.0, 1.0);
|
||||
edge_type::register_edge_type(&self.db, &def)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Salience management ───────────────────────────────────────────────
|
||||
|
||||
/// Mark a node as recently activated — update last_activated, increment
|
||||
@@ -132,7 +224,8 @@ mod sled_impl {
|
||||
node.last_activated,
|
||||
node.activation_count,
|
||||
);
|
||||
graph::put_node(&self.db, &node)?;
|
||||
// Use overwrite_node — touch is an internal state update, not a new node.
|
||||
graph::overwrite_node(&self.db, &node)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -154,7 +247,7 @@ mod sled_impl {
|
||||
if new_salience != node.salience {
|
||||
node.salience = new_salience;
|
||||
storage::write_salience(&self.db, node.id, new_salience)?;
|
||||
graph::put_node(&self.db, &node)?;
|
||||
graph::overwrite_node(&self.db, &node)?;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
@@ -226,9 +319,9 @@ mod wasm_impl {
|
||||
use crate::error::{EngramError, EngramResult};
|
||||
use crate::mem_storage::MemStore;
|
||||
use crate::salience;
|
||||
use crate::types::{ActivatedNode, Edge, Node, RelationType, ScoredNode};
|
||||
use crate::types::{ActivatedNode, Edge, Node, ScoredNode};
|
||||
use crate::vector;
|
||||
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -332,7 +425,7 @@ mod wasm_impl {
|
||||
pub fn traverse(
|
||||
&self,
|
||||
from: Uuid,
|
||||
relation: Option<RelationType>,
|
||||
relation: Option<&str>,
|
||||
max_depth: u8,
|
||||
) -> EngramResult<Vec<Node>> {
|
||||
let store = self
|
||||
@@ -352,8 +445,8 @@ mod wasm_impl {
|
||||
}
|
||||
let edges = store.read_edges_from(current_id)?;
|
||||
for edge in edges {
|
||||
if let Some(ref rel) = relation {
|
||||
if &edge.relation != rel {
|
||||
if let Some(rel) = relation {
|
||||
if edge.relation != rel {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
/// Edge type registry — dynamic, first-class edge type management.
|
||||
///
|
||||
/// Edge types are stored under the key prefix `edge_types:<name>` in the sled
|
||||
/// database. Each value is a bincode-encoded `EdgeTypeDef`.
|
||||
///
|
||||
/// # Operations
|
||||
///
|
||||
/// - `register_edge_type` — create or update a type definition
|
||||
/// - `get_edge_type` — look up by name
|
||||
/// - `update_edge_type_description` — change the human-readable description
|
||||
/// - `increment_edge_type_count` — bump the instance counter when a new edge is created
|
||||
/// - `merge_edge_types` — retag all edges from one type to another and deprecate the source
|
||||
/// - `split_edge_type` — record a split operation; the predicate is stored as a description note
|
||||
/// - `deprecate_edge_type` — mark a type as no longer current
|
||||
/// - `all_edge_types` — return every registered type
|
||||
/// - `edge_types_by_confidence` — filter by minimum confidence score
|
||||
|
||||
use crate::error::EngramResult;
|
||||
use crate::storage;
|
||||
use crate::types::{now_ms, Edge, EdgeTypeDef};
|
||||
use sled::Db;
|
||||
use uuid::Uuid;
|
||||
|
||||
// ── Key helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn edge_type_key(name: &str) -> Vec<u8> {
|
||||
format!("edge_types:{}", name).into_bytes()
|
||||
}
|
||||
|
||||
const EDGE_TYPE_PREFIX: &[u8] = b"edge_types:";
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Register a new edge type, or overwrite an existing definition.
|
||||
///
|
||||
/// Returns the UUID of the stored `EdgeTypeDef`.
|
||||
pub fn register_edge_type(db: &Db, def: &EdgeTypeDef) -> EngramResult<Uuid> {
|
||||
let key = edge_type_key(&def.name);
|
||||
let val = bincode::serialize(def)?;
|
||||
db.insert(key, val)?;
|
||||
Ok(def.id)
|
||||
}
|
||||
|
||||
/// Look up an edge type by its canonical name.
|
||||
pub fn get_edge_type(db: &Db, name: &str) -> EngramResult<Option<EdgeTypeDef>> {
|
||||
match db.get(edge_type_key(name))? {
|
||||
Some(bytes) => Ok(Some(bincode::deserialize(&bytes)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the human-readable description of an existing edge type.
|
||||
///
|
||||
/// No-ops silently if the type does not exist.
|
||||
pub fn update_edge_type_description(db: &Db, name: &str, description: &str) -> EngramResult<()> {
|
||||
if let Some(mut def) = get_edge_type(db, name)? {
|
||||
def.description = description.to_string();
|
||||
let val = bincode::serialize(&def)?;
|
||||
db.insert(edge_type_key(name), val)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Increment the instance counter for an edge type.
|
||||
///
|
||||
/// Called whenever a new edge with this type is persisted. No-ops if the type
|
||||
/// is not registered (the counter stays in-registry, not in the edge itself).
|
||||
pub fn increment_edge_type_count(db: &Db, name: &str) -> EngramResult<()> {
|
||||
if let Some(mut def) = get_edge_type(db, name)? {
|
||||
def.instance_count = def.instance_count.saturating_add(1);
|
||||
let val = bincode::serialize(&def)?;
|
||||
db.insert(edge_type_key(name), val)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Merge two edge types: retag all edges from `from_name` to `into_name`,
|
||||
/// then deprecate `from_name`.
|
||||
///
|
||||
/// After this call every edge that carried `from_name` will carry `into_name`
|
||||
/// instead. The `from_name` definition is marked deprecated and its `supersedes`
|
||||
/// field records the merge destination.
|
||||
pub fn merge_edge_types(db: &Db, from_name: &str, into_name: &str) -> EngramResult<()> {
|
||||
// Collect and retag every edge carrying from_name
|
||||
let prefix = b"edges:from:";
|
||||
let mut edges_to_retag: Vec<Edge> = Vec::new();
|
||||
for result in db.scan_prefix(prefix) {
|
||||
let (_k, v) = result?;
|
||||
let edge: Edge = bincode::deserialize(&v)?;
|
||||
if edge.relation == from_name {
|
||||
edges_to_retag.push(edge);
|
||||
}
|
||||
}
|
||||
|
||||
for mut edge in edges_to_retag {
|
||||
edge.relation = into_name.to_string();
|
||||
storage::write_edge(db, &edge)?;
|
||||
}
|
||||
|
||||
// Deprecate the source type and record the merge destination
|
||||
if let Some(mut def) = get_edge_type(db, from_name)? {
|
||||
def.deprecated = true;
|
||||
def.supersedes = Some(into_name.to_string());
|
||||
let val = bincode::serialize(&def)?;
|
||||
db.insert(edge_type_key(from_name), val)?;
|
||||
}
|
||||
|
||||
// Update instance count on the destination to account for the absorbed edges
|
||||
if let Some(mut into_def) = get_edge_type(db, into_name)? {
|
||||
// Recount from graph (simple: scan all edges for into_name)
|
||||
let mut count = 0u64;
|
||||
for result in db.scan_prefix(prefix) {
|
||||
let (_k, v) = result?;
|
||||
let edge: Edge = bincode::deserialize(&v)?;
|
||||
if edge.relation == into_name {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
into_def.instance_count = count;
|
||||
let val = bincode::serialize(&into_def)?;
|
||||
db.insert(edge_type_key(into_name), val)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Record a split of `name` into `new_name_a` and `new_name_b`.
|
||||
///
|
||||
/// The predicate that drives the split is stored as a descriptive note on
|
||||
/// both new types. This does NOT automatically retag edges — the caller is
|
||||
/// responsible for deciding which edges go to `new_name_a` vs `new_name_b`
|
||||
/// and calling `storage::write_edge` for each. The split records the intent;
|
||||
/// the actual retagging is domain-specific.
|
||||
///
|
||||
/// The original type is deprecated with a note referencing the two successors.
|
||||
pub fn split_edge_type(
|
||||
db: &Db,
|
||||
name: &str,
|
||||
new_name_a: &str,
|
||||
new_name_b: &str,
|
||||
predicate: &str,
|
||||
) -> EngramResult<()> {
|
||||
let now = now_ms();
|
||||
|
||||
// Deprecate the original
|
||||
if let Some(mut original) = get_edge_type(db, name)? {
|
||||
original.deprecated = true;
|
||||
original.description = format!(
|
||||
"{} [SPLIT into '{}' and '{}' via predicate: {}]",
|
||||
original.description, new_name_a, new_name_b, predicate
|
||||
);
|
||||
let val = bincode::serialize(&original)?;
|
||||
db.insert(edge_type_key(name), val)?;
|
||||
}
|
||||
|
||||
// Register new_name_a if it doesn't already exist
|
||||
if get_edge_type(db, new_name_a)?.is_none() {
|
||||
let def_a = EdgeTypeDef {
|
||||
id: Uuid::new_v4(),
|
||||
name: new_name_a.to_string(),
|
||||
description: format!(
|
||||
"Split from '{}' — predicate: {}",
|
||||
name, predicate
|
||||
),
|
||||
first_observed: now,
|
||||
instance_count: 0,
|
||||
confidence: 0.0,
|
||||
derived_from: Some(format!("split from '{}'", name)),
|
||||
supersedes: None,
|
||||
deprecated: false,
|
||||
};
|
||||
register_edge_type(db, &def_a)?;
|
||||
}
|
||||
|
||||
// Register new_name_b if it doesn't already exist
|
||||
if get_edge_type(db, new_name_b)?.is_none() {
|
||||
let def_b = EdgeTypeDef {
|
||||
id: Uuid::new_v4(),
|
||||
name: new_name_b.to_string(),
|
||||
description: format!(
|
||||
"Split from '{}' — predicate: {}",
|
||||
name, predicate
|
||||
),
|
||||
first_observed: now,
|
||||
instance_count: 0,
|
||||
confidence: 0.0,
|
||||
derived_from: Some(format!("split from '{}'", name)),
|
||||
supersedes: None,
|
||||
deprecated: false,
|
||||
};
|
||||
register_edge_type(db, &def_b)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark an edge type as deprecated. Deprecated types should not be used on
|
||||
/// new edges, but existing edges carrying this type remain valid.
|
||||
pub fn deprecate_edge_type(db: &Db, name: &str) -> EngramResult<()> {
|
||||
if let Some(mut def) = get_edge_type(db, name)? {
|
||||
def.deprecated = true;
|
||||
let val = bincode::serialize(&def)?;
|
||||
db.insert(edge_type_key(name), val)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return all registered edge type definitions.
|
||||
pub fn all_edge_types(db: &Db) -> EngramResult<Vec<EdgeTypeDef>> {
|
||||
let mut types = Vec::new();
|
||||
for result in db.scan_prefix(EDGE_TYPE_PREFIX) {
|
||||
let (_k, v) = result?;
|
||||
let def: EdgeTypeDef = bincode::deserialize(&v)?;
|
||||
types.push(def);
|
||||
}
|
||||
Ok(types)
|
||||
}
|
||||
|
||||
/// Return all edge types with a confidence score at or above `min_confidence`.
|
||||
pub fn edge_types_by_confidence(db: &Db, min_confidence: f32) -> EngramResult<Vec<EdgeTypeDef>> {
|
||||
let mut types = all_edge_types(db)?;
|
||||
types.retain(|t| t.confidence >= min_confidence);
|
||||
types.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
|
||||
Ok(types)
|
||||
}
|
||||
|
||||
// ── Built-in type seed ────────────────────────────────────────────────────────
|
||||
|
||||
/// Register all built-in edge types if they have not already been registered.
|
||||
///
|
||||
/// Called once from `EngramDb::open`. Idempotent — existing definitions are
|
||||
/// not overwritten, so user customisations survive restarts.
|
||||
pub fn seed_builtin_types(db: &Db) -> EngramResult<()> {
|
||||
let now = now_ms();
|
||||
|
||||
// Original relational types — confidence 1.0 (canonical, well-established)
|
||||
let originals: &[(&str, &str)] = &[
|
||||
("supersedes", "This node replaces or obsoletes another"),
|
||||
("causes", "This node is a causal precursor to another"),
|
||||
("contains", "This node hierarchically contains another"),
|
||||
("references", "This node cites another as supporting context"),
|
||||
("contradicts", "This node is in logical tension with another"),
|
||||
("exemplifies", "This node is a concrete instance of a more abstract node"),
|
||||
("activates", "Co-activation: firing this tends to fire the other"),
|
||||
("temporally_precedes", "Temporal ordering: this node came before the other"),
|
||||
];
|
||||
|
||||
for (name, description) in originals {
|
||||
if get_edge_type(db, name)?.is_none() {
|
||||
let def = EdgeTypeDef {
|
||||
id: Uuid::new_v4(),
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
first_observed: now,
|
||||
instance_count: 0,
|
||||
confidence: 1.0,
|
||||
derived_from: None,
|
||||
supersedes: None,
|
||||
deprecated: false,
|
||||
};
|
||||
register_edge_type(db, &def)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Personhood / relational types — confidence 0.9 (well-understood but newer)
|
||||
let personhood: &[(&str, &str)] = &[
|
||||
("grounded_in", "This value or belief is rooted in this experience"),
|
||||
("reinforced_by", "This pattern kept being confirmed by this"),
|
||||
("derives_from", "This preference or belief flows from this value"),
|
||||
("in_tension_with", "These two things pull against each other"),
|
||||
("expressed_through","This value surfaces in this voice or behavior"),
|
||||
("shaped_by", "This pattern was formed by this relationship or experience"),
|
||||
("challenged_by", "This belief was tested by this experience"),
|
||||
("resonates_with", "This memory echoes this value"),
|
||||
];
|
||||
|
||||
for (name, description) in personhood {
|
||||
if get_edge_type(db, name)?.is_none() {
|
||||
let def = EdgeTypeDef {
|
||||
id: Uuid::new_v4(),
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
first_observed: now,
|
||||
instance_count: 0,
|
||||
confidence: 0.9,
|
||||
derived_from: None,
|
||||
supersedes: None,
|
||||
deprecated: false,
|
||||
};
|
||||
register_edge_type(db, &def)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn open_tmp() -> (Db, tempfile::TempDir) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db = sled::open(dir.path()).unwrap();
|
||||
(db, dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_and_retrieve() {
|
||||
let (db, _dir) = open_tmp();
|
||||
let def = EdgeTypeDef {
|
||||
id: Uuid::new_v4(),
|
||||
name: "causes".to_string(),
|
||||
description: "A causes B".to_string(),
|
||||
first_observed: 0,
|
||||
instance_count: 0,
|
||||
confidence: 1.0,
|
||||
derived_from: None,
|
||||
supersedes: None,
|
||||
deprecated: false,
|
||||
};
|
||||
register_edge_type(&db, &def).unwrap();
|
||||
let got = get_edge_type(&db, "causes").unwrap().unwrap();
|
||||
assert_eq!(got.name, "causes");
|
||||
assert!((got.confidence - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_description() {
|
||||
let (db, _dir) = open_tmp();
|
||||
let def = EdgeTypeDef {
|
||||
id: Uuid::new_v4(),
|
||||
name: "test_type".to_string(),
|
||||
description: "old".to_string(),
|
||||
first_observed: 0,
|
||||
instance_count: 0,
|
||||
confidence: 0.5,
|
||||
derived_from: None,
|
||||
supersedes: None,
|
||||
deprecated: false,
|
||||
};
|
||||
register_edge_type(&db, &def).unwrap();
|
||||
update_edge_type_description(&db, "test_type", "new description").unwrap();
|
||||
let got = get_edge_type(&db, "test_type").unwrap().unwrap();
|
||||
assert_eq!(got.description, "new description");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn increment_count() {
|
||||
let (db, _dir) = open_tmp();
|
||||
let def = EdgeTypeDef {
|
||||
id: Uuid::new_v4(),
|
||||
name: "references".to_string(),
|
||||
description: "refs".to_string(),
|
||||
first_observed: 0,
|
||||
instance_count: 5,
|
||||
confidence: 1.0,
|
||||
derived_from: None,
|
||||
supersedes: None,
|
||||
deprecated: false,
|
||||
};
|
||||
register_edge_type(&db, &def).unwrap();
|
||||
increment_edge_type_count(&db, "references").unwrap();
|
||||
let got = get_edge_type(&db, "references").unwrap().unwrap();
|
||||
assert_eq!(got.instance_count, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deprecate() {
|
||||
let (db, _dir) = open_tmp();
|
||||
let def = EdgeTypeDef {
|
||||
id: Uuid::new_v4(),
|
||||
name: "old_type".to_string(),
|
||||
description: "going away".to_string(),
|
||||
first_observed: 0,
|
||||
instance_count: 0,
|
||||
confidence: 0.3,
|
||||
derived_from: None,
|
||||
supersedes: None,
|
||||
deprecated: false,
|
||||
};
|
||||
register_edge_type(&db, &def).unwrap();
|
||||
deprecate_edge_type(&db, "old_type").unwrap();
|
||||
let got = get_edge_type(&db, "old_type").unwrap().unwrap();
|
||||
assert!(got.deprecated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_types_and_confidence_filter() {
|
||||
let (db, _dir) = open_tmp();
|
||||
seed_builtin_types(&db).unwrap();
|
||||
|
||||
let all = all_edge_types(&db).unwrap();
|
||||
assert!(all.len() >= 16); // 8 originals + 8 personhood
|
||||
|
||||
// All original types have confidence 1.0
|
||||
let high = edge_types_by_confidence(&db, 1.0).unwrap();
|
||||
assert!(high.len() >= 8);
|
||||
for t in &high {
|
||||
assert!((t.confidence - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
// personhood types have confidence 0.9 — included when threshold is <= 0.9
|
||||
let wide = edge_types_by_confidence(&db, 0.9).unwrap();
|
||||
assert!(wide.len() >= 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_is_idempotent() {
|
||||
let (db, _dir) = open_tmp();
|
||||
seed_builtin_types(&db).unwrap();
|
||||
seed_builtin_types(&db).unwrap(); // second call must not panic or duplicate
|
||||
let all = all_edge_types(&db).unwrap();
|
||||
// All names should be distinct
|
||||
let mut names: Vec<String> = all.iter().map(|t| t.name.clone()).collect();
|
||||
names.sort();
|
||||
names.dedup();
|
||||
assert_eq!(names.len(), all.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_type_records_both_halves() {
|
||||
let (db, _dir) = open_tmp();
|
||||
let original = EdgeTypeDef {
|
||||
id: Uuid::new_v4(),
|
||||
name: "relates_to".to_string(),
|
||||
description: "generic relation".to_string(),
|
||||
first_observed: 0,
|
||||
instance_count: 0,
|
||||
confidence: 0.5,
|
||||
derived_from: None,
|
||||
supersedes: None,
|
||||
deprecated: false,
|
||||
};
|
||||
register_edge_type(&db, &original).unwrap();
|
||||
split_edge_type(&db, "relates_to", "causes", "references", "directionality").unwrap();
|
||||
let orig = get_edge_type(&db, "relates_to").unwrap().unwrap();
|
||||
assert!(orig.deprecated);
|
||||
assert!(get_edge_type(&db, "causes").unwrap().is_some());
|
||||
assert!(get_edge_type(&db, "references").unwrap().is_some());
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ pub enum EngramError {
|
||||
#[error("Node not found: {0}")]
|
||||
NotFound(uuid::Uuid),
|
||||
|
||||
#[error("Node already exists: {0} — nodes are immutable; supersede via edge")]
|
||||
NodeAlreadyExists(uuid::Uuid),
|
||||
|
||||
#[error("Invalid embedding: expected {expected} dimensions, got {got}")]
|
||||
DimensionMismatch { expected: usize, got: usize },
|
||||
|
||||
@@ -4,17 +4,25 @@
|
||||
/// in both directions so that forward and backward traversals are equally cheap.
|
||||
use crate::error::EngramResult;
|
||||
use crate::storage;
|
||||
use crate::types::{Edge, Node, RelationType};
|
||||
use crate::types::{Edge, Node};
|
||||
use sled::Db;
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Persist a node and its embedding. Overwrites any existing node with the same id.
|
||||
/// Persist a new node and its embedding. Enforces node immutability — returns
|
||||
/// `EngramError::NodeAlreadyExists` if the ID is already in the store.
|
||||
pub fn put_node(db: &Db, node: &Node) -> EngramResult<Uuid> {
|
||||
storage::write_node(db, node)?;
|
||||
Ok(node.id)
|
||||
}
|
||||
|
||||
/// Overwrite a node unconditionally. Used internally for salience/tier mutations
|
||||
/// (touch, decay, consolidation). Do NOT call this for user-visible node creation.
|
||||
pub fn overwrite_node(db: &Db, node: &Node) -> EngramResult<Uuid> {
|
||||
storage::overwrite_node(db, node)?;
|
||||
Ok(node.id)
|
||||
}
|
||||
|
||||
/// Retrieve a node by id. Returns None if not found.
|
||||
pub fn get_node(db: &Db, id: Uuid) -> EngramResult<Option<Node>> {
|
||||
storage::read_node(db, id)
|
||||
@@ -37,13 +45,13 @@ pub fn edges_to(db: &Db, to_id: Uuid) -> EngramResult<Vec<Edge>> {
|
||||
|
||||
/// Breadth-first traversal starting from `from`, following forward edges only.
|
||||
///
|
||||
/// If `relation` is Some, only edges of that type are followed.
|
||||
/// The BFS respects `max_depth` hops. The seed node itself is NOT included.
|
||||
/// Visited nodes are deduplicated.
|
||||
/// If `relation` is `Some(&str)`, only edges whose `relation` field matches
|
||||
/// that string are followed. The BFS respects `max_depth` hops. The seed node
|
||||
/// itself is NOT included. Visited nodes are deduplicated.
|
||||
pub fn traverse(
|
||||
db: &Db,
|
||||
from: Uuid,
|
||||
relation: Option<RelationType>,
|
||||
relation: Option<&str>,
|
||||
max_depth: u8,
|
||||
) -> EngramResult<Vec<Node>> {
|
||||
let mut visited: HashSet<Uuid> = HashSet::new();
|
||||
@@ -60,8 +68,8 @@ pub fn traverse(
|
||||
let edges = edges_from(db, current_id)?;
|
||||
for edge in edges {
|
||||
// Filter by relation type if specified
|
||||
if let Some(ref rel) = relation {
|
||||
if &edge.relation != rel {
|
||||
if let Some(rel) = relation {
|
||||
if edge.relation != rel {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
/// # Quick Start
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, RelationType};
|
||||
/// use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, EDGE_SUPERSEDES};
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let db = EngramDb::open(Path::new("/tmp/my-engram")).unwrap();
|
||||
@@ -32,6 +32,8 @@
|
||||
pub mod activation;
|
||||
pub mod consolidation;
|
||||
pub mod db;
|
||||
#[cfg(not(feature = "wasm"))]
|
||||
pub mod edge_type;
|
||||
pub mod error;
|
||||
pub mod graph;
|
||||
pub mod salience;
|
||||
@@ -48,6 +50,8 @@ pub mod migration;
|
||||
pub use db::EngramDb;
|
||||
pub use error::{EngramError, EngramResult};
|
||||
pub use types::{
|
||||
ActivatedNode, Edge, MemoryTier, Node, NodeType, RelationType, ScoredNode, now_ms,
|
||||
ActivatedNode, Edge, EdgeTypeDef, MemoryTier, Node, NodeType, ScoredNode, now_ms,
|
||||
EDGE_ACTIVATES, EDGE_CAUSES, EDGE_CONTAINS, EDGE_CONTRADICTS, EDGE_EXEMPLIFIES,
|
||||
EDGE_REFERENCES, EDGE_SUPERSEDES, EDGE_TEMPORALLY_PRECEDES,
|
||||
};
|
||||
pub use consolidation::{ConsolidationConfig, ConsolidationReport};
|
||||
@@ -10,9 +10,8 @@
|
||||
/// | `memory_nodes` | `Node { tier: Episodic, node_type: Memory }` |
|
||||
/// | `knowledge_entries` | `Node { tier: Semantic, node_type: Concept }` |
|
||||
///
|
||||
/// Edges from `graph_edges` (type = "supersedes" or "Supersedes") are converted
|
||||
/// to `Edge { relation: RelationType::Supersedes }`. Other edge types become
|
||||
/// `RelationType::References`.
|
||||
/// Edges from `graph_edges` are converted using their `edge_type` string directly
|
||||
/// (normalised to lowercase). Unknown types map to `"references"`.
|
||||
///
|
||||
/// # Embeddings
|
||||
///
|
||||
@@ -23,7 +22,7 @@
|
||||
/// TODO: wire in real embeddings from all-MiniLM-L6-v2 via the ONNX runtime.
|
||||
|
||||
use crate::error::{EngramError, EngramResult};
|
||||
use crate::types::{Edge, MemoryTier, Node, NodeType, RelationType};
|
||||
use crate::types::{Edge, MemoryTier, Node, NodeType};
|
||||
use rusqlite::{Connection, OpenFlags};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
@@ -227,7 +226,7 @@ pub fn migrate_from_neuron(config: &MigrationConfig) -> EngramResult<MigrationRe
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let relation = edge_type_to_relation(&edge_type);
|
||||
let relation = normalise_edge_type(&edge_type);
|
||||
let edge = Edge::new(from_uuid, to_uuid, relation, weight as f32);
|
||||
|
||||
match engram_db.put_edge(edge) {
|
||||
@@ -265,18 +264,22 @@ fn importance_string_to_f32(importance: &str) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Neuron edge type string to an Engram RelationType.
|
||||
fn edge_type_to_relation(edge_type: &str) -> RelationType {
|
||||
/// Normalise a Neuron edge type string to a canonical Engram edge type name.
|
||||
///
|
||||
/// Known variants (including old PascalCase forms from the Rust enum era) are
|
||||
/// mapped to their lowercase canonical names. Unknown types fall back to
|
||||
/// `"references"` as a safe, non-destructive default.
|
||||
fn normalise_edge_type(edge_type: &str) -> &'static str {
|
||||
match edge_type.to_lowercase().as_str() {
|
||||
"supersedes" | "superseded_by" => RelationType::Supersedes,
|
||||
"causes" | "caused_by" => RelationType::Causes,
|
||||
"contains" | "contained_by" => RelationType::Contains,
|
||||
"references" | "referenced_by" => RelationType::References,
|
||||
"contradicts" => RelationType::Contradicts,
|
||||
"exemplifies" | "exemplified_by" => RelationType::Exemplifies,
|
||||
"activates" => RelationType::Activates,
|
||||
"temporally_precedes" | "follows" => RelationType::TemporallyPrecedes,
|
||||
_ => RelationType::References, // safe default
|
||||
"supersedes" | "superseded_by" => "supersedes",
|
||||
"causes" | "caused_by" => "causes",
|
||||
"contains" | "contained_by" => "contains",
|
||||
"references" | "referenced_by" => "references",
|
||||
"contradicts" => "contradicts",
|
||||
"exemplifies" | "exemplified_by" => "exemplifies",
|
||||
"activates" => "activates",
|
||||
"temporally_precedes" | "temporallyprecedes" | "follows" => "temporally_precedes",
|
||||
_ => "references", // safe default
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,12 +343,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn edge_type_supersedes() {
|
||||
assert_eq!(edge_type_to_relation("supersedes"), RelationType::Supersedes);
|
||||
assert_eq!(normalise_edge_type("supersedes"), "supersedes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_type_unknown_is_references() {
|
||||
assert_eq!(edge_type_to_relation("foobar"), RelationType::References);
|
||||
assert_eq!(normalise_edge_type("foobar"), "references");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6,6 +6,7 @@
|
||||
/// edges:to:{to}:{from} → reverse index (same Edge bytes)
|
||||
/// vectors:{uuid} → raw little-endian f32 bytes
|
||||
/// salience:{uuid} → 4-byte little-endian f32
|
||||
/// edge_types:{name} → bincode-encoded EdgeTypeDef (managed by edge_type.rs)
|
||||
use crate::error::{EngramError, EngramResult};
|
||||
use crate::types::{Edge, Node};
|
||||
use sled::Db;
|
||||
@@ -35,8 +36,19 @@ pub fn salience_key(id: Uuid) -> Vec<u8> {
|
||||
|
||||
// ── Node storage ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Persist a node. Returns `EngramError::NodeAlreadyExists` if a node with
|
||||
/// this ID already exists in the store.
|
||||
///
|
||||
/// Nodes are immutable and append-only. To update, create a new node and
|
||||
/// connect it to the old with a `supersedes` edge.
|
||||
pub fn write_node(db: &Db, node: &Node) -> EngramResult<()> {
|
||||
let key = node_key(node.id);
|
||||
|
||||
// Immutability guard — reject writes to existing node IDs.
|
||||
if db.contains_key(&key)? {
|
||||
return Err(EngramError::NodeAlreadyExists(node.id));
|
||||
}
|
||||
|
||||
let val = bincode::serialize(node)?;
|
||||
db.insert(key, val)?;
|
||||
|
||||
@@ -52,6 +64,22 @@ pub fn write_node(db: &Db, node: &Node) -> EngramResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Overwrite a node unconditionally. Used internally for salience/tier updates
|
||||
/// that must mutate in-place (touch, decay, consolidation).
|
||||
///
|
||||
/// Do NOT expose this in public API — callers should use `write_node` which
|
||||
/// enforces immutability.
|
||||
pub(crate) fn overwrite_node(db: &Db, node: &Node) -> EngramResult<()> {
|
||||
let key = node_key(node.id);
|
||||
let val = bincode::serialize(node)?;
|
||||
db.insert(key, val)?;
|
||||
let vkey = vector_key(node.id);
|
||||
db.insert(vkey, floats_to_bytes(&node.embedding))?;
|
||||
let skey = salience_key(node.id);
|
||||
db.insert(skey, f32_to_bytes(node.salience))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_node(db: &Db, id: Uuid) -> EngramResult<Option<Node>> {
|
||||
match db.get(node_key(id))? {
|
||||
Some(bytes) => Ok(Some(bincode::deserialize(&bytes)?)),
|
||||
@@ -1,6 +1,20 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
// ── Built-in edge type name constants ─────────────────────────────────────────
|
||||
//
|
||||
// String constants for the 8 built-in relation types. Use these on Edge.relation
|
||||
// to reference known types. Intelligence about when to create new types or how
|
||||
// to score confidence belongs in el, not here.
|
||||
pub const EDGE_SUPERSEDES: &str = "supersedes";
|
||||
pub const EDGE_CAUSES: &str = "causes";
|
||||
pub const EDGE_CONTAINS: &str = "contains";
|
||||
pub const EDGE_REFERENCES: &str = "references";
|
||||
pub const EDGE_CONTRADICTS: &str = "contradicts";
|
||||
pub const EDGE_EXEMPLIFIES: &str = "exemplifies";
|
||||
pub const EDGE_ACTIVATES: &str = "activates";
|
||||
pub const EDGE_TEMPORALLY_PRECEDES: &str = "temporally_precedes";
|
||||
|
||||
/// The functional role of a node in the memory graph.
|
||||
///
|
||||
/// Different node types participate in different retrieval patterns:
|
||||
@@ -8,6 +22,7 @@ use uuid::Uuid;
|
||||
/// - Concepts and Entities form the semantic backbone
|
||||
/// - Processes encode procedural knowledge
|
||||
/// - InternalState captures the system's own affective context
|
||||
/// - Custom(String) is an open extension point; el defines new types freely
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum NodeType {
|
||||
/// A specific remembered experience or observation
|
||||
@@ -22,6 +37,8 @@ pub enum NodeType {
|
||||
Process,
|
||||
/// An internal affective or motivational state
|
||||
InternalState,
|
||||
/// Caller-defined node type. el uses this for types Rust does not need to know about.
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// Where in the memory hierarchy a node currently lives.
|
||||
@@ -44,28 +61,35 @@ pub enum MemoryTier {
|
||||
Procedural,
|
||||
}
|
||||
|
||||
/// The typed relationship between two nodes.
|
||||
/// Metadata record for a named edge type. Dumb data container — Rust stores and
|
||||
/// returns it. All decisions about confidence thresholds, when to create new
|
||||
/// types, merge/split logic, and pattern recognition belong in el, not here.
|
||||
///
|
||||
/// Relation types encode causal, temporal, hierarchical, and logical
|
||||
/// structure into the graph itself — not just into metadata.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RelationType {
|
||||
/// This node replaces or obsoletes another
|
||||
Supersedes,
|
||||
/// This node is a causal precursor to another
|
||||
Causes,
|
||||
/// This node hierarchically contains another
|
||||
Contains,
|
||||
/// This node cites or points to another as supporting context
|
||||
References,
|
||||
/// This node is in logical tension with another
|
||||
Contradicts,
|
||||
/// This node is a concrete instance of a more abstract node
|
||||
Exemplifies,
|
||||
/// Co-activation: firing this node tends to fire the other
|
||||
Activates,
|
||||
/// Temporal ordering: this node came before the other
|
||||
TemporallyPrecedes,
|
||||
/// Fields like `deprecated`, `derived_from`, and `supersedes` are data.
|
||||
/// They are set by the caller (el) and stored verbatim. Rust never inspects
|
||||
/// or acts on them autonomously.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EdgeTypeDef {
|
||||
/// Stable unique identifier for this edge type record
|
||||
pub id: Uuid,
|
||||
/// The canonical name used on edges (e.g. `"causes"`, `"resonates_with"`)
|
||||
pub name: String,
|
||||
/// Human-readable description of what this relation means
|
||||
pub description: String,
|
||||
/// Unix milliseconds when this type was first registered
|
||||
pub first_observed: i64,
|
||||
/// How many edges currently carry this type
|
||||
pub instance_count: u64,
|
||||
/// Caller-supplied confidence, 0.0–1.0. Stored as-is; not computed here.
|
||||
pub confidence: f32,
|
||||
/// Free-text note about what observation prompted this type's creation.
|
||||
/// Set by el; stored verbatim.
|
||||
pub derived_from: Option<String>,
|
||||
/// Name of the edge type this one replaced, if any. Set by el; stored verbatim.
|
||||
pub supersedes: Option<String>,
|
||||
/// When true, this type should no longer be used for new edges.
|
||||
/// Set by el; stored verbatim.
|
||||
pub deprecated: bool,
|
||||
}
|
||||
|
||||
/// A node in the engram graph — the fundamental unit of stored memory.
|
||||
@@ -161,12 +185,17 @@ pub struct ScoredNode {
|
||||
///
|
||||
/// Edge weights strengthen with co-activation (Hebbian learning).
|
||||
/// The weight directly multiplies activation flow during spreading activation.
|
||||
///
|
||||
/// The `relation` field is a free-form string naming the edge type — look up
|
||||
/// the canonical definition in the `EdgeTypeDef` registry via `edge_type`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Edge {
|
||||
pub id: Uuid,
|
||||
pub from_id: Uuid,
|
||||
pub to_id: Uuid,
|
||||
pub relation: RelationType,
|
||||
/// The edge type name (e.g. `"causes"`, `"resonates_with"`). Matches the
|
||||
/// `name` field of the corresponding `EdgeTypeDef` in the registry.
|
||||
pub relation: String,
|
||||
/// Connection strength, 0.0–1.0. Increases when both endpoints are
|
||||
/// activated in close temporal proximity (long-term potentiation).
|
||||
pub weight: f32,
|
||||
@@ -176,13 +205,13 @@ pub struct Edge {
|
||||
}
|
||||
|
||||
impl Edge {
|
||||
pub fn new(from_id: Uuid, to_id: Uuid, relation: RelationType, weight: f32) -> Self {
|
||||
pub fn new(from_id: Uuid, to_id: Uuid, relation: impl Into<String>, weight: f32) -> Self {
|
||||
let now = now_ms();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
from_id,
|
||||
to_id,
|
||||
relation,
|
||||
relation: relation.into(),
|
||||
weight: weight.clamp(0.0, 1.0),
|
||||
created_at: now,
|
||||
last_fired: now,
|
||||
@@ -98,7 +98,7 @@ fn matches_filter(a: &ActivatedNode, filter: &NodeFilter) -> bool {
|
||||
|
||||
NodeFilter::ByType(types) => {
|
||||
let node_type_str = node_type_str(&a.node.node_type);
|
||||
types.iter().any(|t| t == node_type_str)
|
||||
types.iter().any(|t| t == &node_type_str)
|
||||
}
|
||||
|
||||
NodeFilter::ByTier(tiers) => tiers.iter().any(|t| t == &a.node.tier),
|
||||
@@ -195,14 +195,15 @@ fn traverse_json_path<'a>(doc: &'a Value, path: &str) -> Option<&'a Value> {
|
||||
|
||||
// ── String helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
fn node_type_str(t: &NodeType) -> &'static str {
|
||||
fn node_type_str(t: &NodeType) -> String {
|
||||
match t {
|
||||
NodeType::Memory => "Memory",
|
||||
NodeType::Concept => "Concept",
|
||||
NodeType::Event => "Event",
|
||||
NodeType::Entity => "Entity",
|
||||
NodeType::Process => "Process",
|
||||
NodeType::InternalState => "InternalState",
|
||||
NodeType::Memory => "Memory".to_string(),
|
||||
NodeType::Concept => "Concept".to_string(),
|
||||
NodeType::Event => "Event".to_string(),
|
||||
NodeType::Entity => "Entity".to_string(),
|
||||
NodeType::Process => "Process".to_string(),
|
||||
NodeType::InternalState => "InternalState".to_string(),
|
||||
NodeType::Custom(s) => s.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use engram_core::{EngramDb, EngramResult};
|
||||
use engram_core::types::{Node, NodeType, RelationType};
|
||||
use engram_core::types::{Node, NodeType};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::types::{
|
||||
@@ -223,7 +223,7 @@ impl ReasoningEngine {
|
||||
|
||||
/// Find causal chains: what causes a concept, or what a concept causes.
|
||||
///
|
||||
/// Traverses the graph following `RelationType::Causes` edges in the
|
||||
/// Traverses the graph following `"causes"` edges in the
|
||||
/// requested direction.
|
||||
pub fn causal_chain(
|
||||
&mut self,
|
||||
@@ -252,7 +252,7 @@ impl ReasoningEngine {
|
||||
let db = self.db.lock().map_err(|_| {
|
||||
engram_core::EngramError::InvalidParam("db lock poisoned".into())
|
||||
})?;
|
||||
db.traverse(seed.id, Some(RelationType::Causes), self.config.max_depth as u8)?
|
||||
db.traverse(seed.id, Some("causes"), self.config.max_depth as u8)?
|
||||
};
|
||||
|
||||
// Build evidence nodes — always start with the seed as the first node
|
||||
@@ -326,7 +326,7 @@ impl ReasoningEngine {
|
||||
|
||||
/// Find ordered steps for a HowTo query.
|
||||
///
|
||||
/// Traverses `RelationType::Causes` and `RelationType::Contains` edges
|
||||
/// Traverses `"causes"` and `"contains"` edges
|
||||
/// from Process/Procedural nodes matching the goal embedding.
|
||||
pub fn procedural_chain(&mut self, goal_embedding: &[f32]) -> EngramResult<Vec<String>> {
|
||||
let process_nodes: Vec<Node> = {
|
||||
@@ -363,7 +363,7 @@ impl ReasoningEngine {
|
||||
let db = self.db.lock().map_err(|_| {
|
||||
engram_core::EngramError::InvalidParam("db lock poisoned".into())
|
||||
})?;
|
||||
db.traverse(best_process.id, Some(RelationType::Causes), self.config.max_depth as u8)?
|
||||
db.traverse(best_process.id, Some("causes"), self.config.max_depth as u8)?
|
||||
};
|
||||
|
||||
let mut ordered_steps: Vec<String> = Vec::new();
|
||||
@@ -437,7 +437,7 @@ impl ReasoningEngine {
|
||||
if let Some(from_node) = db.get_node(*node_id)? {
|
||||
let edges = db.get_edges_from(*node_id)?;
|
||||
for edge in edges {
|
||||
if edge.relation == RelationType::Contradicts {
|
||||
if edge.relation == "contradicts" {
|
||||
if let Some(target) = db.get_node(edge.to_id)? {
|
||||
let sim_a = cosine_sim(topic_embedding, &from_node.embedding);
|
||||
let sim_b = cosine_sim(topic_embedding, &target.embedding);
|
||||
@@ -976,16 +976,21 @@ impl ReasoningEngine {
|
||||
|
||||
// ── Edge mapping helpers ──────────────────────────────────────────────────────
|
||||
|
||||
fn relation_to_inference_edge(relation: &RelationType) -> InferenceEdgeType {
|
||||
fn relation_to_inference_edge(relation: &str) -> InferenceEdgeType {
|
||||
match relation {
|
||||
RelationType::Causes => InferenceEdgeType::Causes,
|
||||
RelationType::Contradicts => InferenceEdgeType::Contradicts,
|
||||
RelationType::Supersedes => InferenceEdgeType::Implies,
|
||||
RelationType::Contains => InferenceEdgeType::Requires,
|
||||
RelationType::References => InferenceEdgeType::SimilarTo,
|
||||
RelationType::Exemplifies => InferenceEdgeType::InstanceOf,
|
||||
RelationType::Activates => InferenceEdgeType::Supports,
|
||||
RelationType::TemporallyPrecedes => InferenceEdgeType::Causes,
|
||||
"causes" => InferenceEdgeType::Causes,
|
||||
"contradicts" => InferenceEdgeType::Contradicts,
|
||||
"supersedes" => InferenceEdgeType::Implies,
|
||||
"contains" => InferenceEdgeType::Requires,
|
||||
"references" => InferenceEdgeType::SimilarTo,
|
||||
"exemplifies" => InferenceEdgeType::InstanceOf,
|
||||
"activates" => InferenceEdgeType::Supports,
|
||||
"temporally_precedes" => InferenceEdgeType::Causes,
|
||||
// Personhood types map to their closest structural equivalent
|
||||
"grounded_in" | "reinforced_by" | "derives_from" | "resonates_with" => InferenceEdgeType::Supports,
|
||||
"in_tension_with" | "challenged_by" => InferenceEdgeType::Contradicts,
|
||||
"expressed_through" | "shaped_by" => InferenceEdgeType::Implies,
|
||||
_ => InferenceEdgeType::SimilarTo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
/// # Quick Start
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier, RelationType};
|
||||
/// use engram_core::{EngramDb, Node, Edge, NodeType, MemoryTier};
|
||||
/// use engram_reasoning::{ReasoningEngine, Hypothesis, HypothesisType, ReasoningConfig};
|
||||
/// use std::path::Path;
|
||||
/// use std::sync::{Arc, Mutex};
|
||||
@@ -9,7 +9,7 @@ mod tests {
|
||||
use tempfile::TempDir;
|
||||
use uuid::Uuid;
|
||||
|
||||
use engram_core::{Edge, EngramDb, MemoryTier, Node, NodeType, RelationType};
|
||||
use engram_core::{Edge, EngramDb, MemoryTier, Node, NodeType};
|
||||
|
||||
use crate::{
|
||||
CausalDirection, EvidenceNode, EvidenceType, Hypothesis, HypothesisType, InferenceEdge,
|
||||
@@ -51,7 +51,7 @@ mod tests {
|
||||
id
|
||||
}
|
||||
|
||||
fn make_edge(db: &Arc<Mutex<EngramDb>>, from: Uuid, to: Uuid, relation: RelationType) {
|
||||
fn make_edge(db: &Arc<Mutex<EngramDb>>, from: Uuid, to: Uuid, relation: &str) {
|
||||
let edge = Edge::new(from, to, relation, 0.8);
|
||||
db.lock().unwrap().put_edge(edge).unwrap();
|
||||
}
|
||||
@@ -339,8 +339,8 @@ mod tests {
|
||||
let id_b = make_node(&db, "Expansion causes pressure", emb_b, NodeType::Concept, 0.8);
|
||||
let id_c = make_node(&db, "Pressure causes rupture", emb_c, NodeType::Concept, 0.7);
|
||||
|
||||
make_edge(&db, id_a, id_b, RelationType::Causes);
|
||||
make_edge(&db, id_b, id_c, RelationType::Causes);
|
||||
make_edge(&db, id_a, id_b, "causes");
|
||||
make_edge(&db, id_b, id_c, "causes");
|
||||
|
||||
let mut engine = make_engine(db.clone());
|
||||
let chains = engine.causal_chain(&emb_a, CausalDirection::Forward).unwrap();
|
||||
@@ -373,7 +373,7 @@ mod tests {
|
||||
NodeType::Memory,
|
||||
0.9,
|
||||
);
|
||||
make_edge(&db, id_a, id_b, RelationType::Contradicts);
|
||||
make_edge(&db, id_a, id_b, "contradicts");
|
||||
|
||||
let mut engine = make_engine(db.clone());
|
||||
let contradictions = engine.find_contradictions(&emb_topic).unwrap();
|
||||
@@ -482,8 +482,8 @@ mod tests {
|
||||
NodeType::Process,
|
||||
0.9,
|
||||
);
|
||||
make_edge(&db, id_1, id_2, RelationType::Causes);
|
||||
make_edge(&db, id_2, id_3, RelationType::Causes);
|
||||
make_edge(&db, id_1, id_2, "causes");
|
||||
make_edge(&db, id_2, id_3, "causes");
|
||||
|
||||
let mut engine = make_engine(db.clone());
|
||||
let steps = engine.procedural_chain(&goal_emb).unwrap();
|
||||
@@ -5,9 +5,9 @@
|
||||
/// ## Core
|
||||
/// GET /stats — node/edge counts
|
||||
/// POST /nodes — create a node
|
||||
/// GET /nodes/{id} — get a node
|
||||
/// GET /nodes/:id — get a node
|
||||
/// POST /edges — create an edge
|
||||
/// GET /nodes/{id}/edges — list edges from a node
|
||||
/// GET /nodes/:id/edges — list edges from a node
|
||||
/// POST /activate — spreading activation
|
||||
/// POST /search — embedding search
|
||||
/// POST /decay — apply salience decay
|
||||
@@ -18,7 +18,7 @@
|
||||
/// POST /sync/push — receive incoming delta
|
||||
/// POST /sync/peers — register peer
|
||||
/// GET /sync/peers — list peers
|
||||
/// DELETE /sync/peers/{id} — remove peer
|
||||
/// DELETE /sync/peers/:id — remove peer
|
||||
///
|
||||
/// ## Swarm
|
||||
/// POST /swarm/activate — distributed activation (auth required)
|
||||
@@ -188,7 +188,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
.route("/sync/push", post(routes::sync::push_delta))
|
||||
.route("/sync/peers", get(routes::sync::list_peers))
|
||||
.route("/sync/peers", post(routes::sync::register_peer))
|
||||
.route("/sync/peers/{id}", delete(routes::sync::delete_peer))
|
||||
.route("/sync/peers/:id", delete(routes::sync::delete_peer))
|
||||
.route("/swarm/activate", post(routes::swarm::swarm_activate))
|
||||
.route("/swarm/status", get(routes::swarm::swarm_status))
|
||||
.layer(middleware::from_fn_with_state(
|
||||
@@ -200,9 +200,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
let core_routes = Router::new()
|
||||
.route("/stats", get(routes::core::get_stats))
|
||||
.route("/nodes", post(routes::core::create_node))
|
||||
.route("/nodes/{id}", get(routes::core::get_node))
|
||||
.route("/nodes/list", get(routes::core::list_nodes))
|
||||
.route("/nodes/:id", get(routes::core::get_node))
|
||||
.route("/edges", post(routes::core::create_edge))
|
||||
.route("/nodes/{id}/edges", get(routes::core::get_edges_from))
|
||||
.route("/nodes/:id/edges", get(routes::core::get_edges_from))
|
||||
.route("/activate", post(routes::core::activate))
|
||||
.route("/search", post(routes::core::search_embedding))
|
||||
.route("/decay", post(routes::core::decay))
|
||||
@@ -213,20 +214,20 @@ async fn main() -> anyhow::Result<()> {
|
||||
.route("/projections", post(routes::projection::register_projection))
|
||||
.route("/projections", get(routes::projection::list_projections))
|
||||
.route(
|
||||
"/projections/{name}/schema",
|
||||
"/projections/:name/schema",
|
||||
get(routes::projection::get_projection_schema),
|
||||
)
|
||||
.route(
|
||||
"/projections/{name}/query",
|
||||
"/projections/:name/query",
|
||||
post(routes::projection::query_projection),
|
||||
);
|
||||
|
||||
// Transaction routes (no auth — add auth layer if needed)
|
||||
let tx_routes = Router::new()
|
||||
.route("/tx/apply", post(routes::tx::tx_apply))
|
||||
.route("/tx/rollback/{command_id}", post(routes::tx::tx_rollback))
|
||||
.route("/tx/rollback/:command_id", post(routes::tx::tx_rollback))
|
||||
.route("/tx/history", get(routes::tx::tx_history))
|
||||
.route("/tx/chain/{command_id}", get(routes::tx::tx_causal_chain));
|
||||
.route("/tx/chain/:command_id", get(routes::tx::tx_causal_chain));
|
||||
|
||||
// Reasoning routes (no auth — graph-native inference)
|
||||
let reasoning_routes = Router::new()
|
||||
@@ -234,18 +235,47 @@ async fn main() -> anyhow::Result<()> {
|
||||
.route("/reason/causal", post(routes::reasoning::causal))
|
||||
.route("/reason/contradictions", post(routes::reasoning::contradictions));
|
||||
|
||||
let studio_routes = Router::new()
|
||||
let app = Router::new()
|
||||
// Studio
|
||||
.route("/", get(serve_studio_index))
|
||||
.route("/studio", get(serve_studio_index))
|
||||
.route("/studio/{*path}", get(serve_studio_asset));
|
||||
|
||||
let app = Router::new()
|
||||
.merge(studio_routes)
|
||||
.merge(core_routes)
|
||||
.merge(sync_routes)
|
||||
.merge(projection_routes)
|
||||
.merge(tx_routes)
|
||||
.merge(reasoning_routes)
|
||||
// Core API
|
||||
.route("/stats", get(routes::core::get_stats))
|
||||
.route("/nodes", post(routes::core::create_node))
|
||||
.route("/nodes/list", get(routes::core::list_nodes))
|
||||
.route("/nodes/:id", get(routes::core::get_node))
|
||||
.route("/edges", post(routes::core::create_edge))
|
||||
.route("/nodes/:id/edges", get(routes::core::get_edges_from))
|
||||
.route("/activate", post(routes::core::activate))
|
||||
.route("/search", post(routes::core::search_embedding))
|
||||
.route("/decay", post(routes::core::decay))
|
||||
.route("/consolidate", post(routes::core::consolidate))
|
||||
// Projection
|
||||
.route("/projections", post(routes::projection::register_projection))
|
||||
.route("/projections", get(routes::projection::list_projections))
|
||||
.route("/projections/:name/schema", get(routes::projection::get_projection_schema))
|
||||
.route("/projections/:name/query", post(routes::projection::query_projection))
|
||||
// Transactions
|
||||
.route("/tx/apply", post(routes::tx::tx_apply))
|
||||
.route("/tx/rollback/:command_id", post(routes::tx::tx_rollback))
|
||||
.route("/tx/history", get(routes::tx::tx_history))
|
||||
.route("/tx/chain/:command_id", get(routes::tx::tx_causal_chain))
|
||||
// Reasoning
|
||||
.route("/reason", post(routes::reasoning::reason))
|
||||
.route("/reason/causal", post(routes::reasoning::causal))
|
||||
.route("/reason/contradictions", post(routes::reasoning::contradictions))
|
||||
// Sync + Swarm routes added directly (auth is in the handlers themselves for now)
|
||||
.route("/sync/delta", get(routes::sync::get_delta))
|
||||
.route("/sync/push", post(routes::sync::push_delta))
|
||||
.route("/sync/peers", get(routes::sync::list_peers))
|
||||
.route("/sync/peers", post(routes::sync::register_peer))
|
||||
.route("/sync/peers/:id", delete(routes::sync::delete_peer))
|
||||
.route("/swarm/activate", post(routes::swarm::swarm_activate))
|
||||
.route("/swarm/status", get(routes::swarm::swarm_status))
|
||||
.fallback(|uri: axum::http::Uri| async move {
|
||||
tracing::warn!("FALLBACK hit for: {}", uri);
|
||||
(axum::http::StatusCode::NOT_FOUND, format!("FALLBACK: {}", uri))
|
||||
})
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use engram_core::types::{Edge, MemoryTier, Node, NodeType, RelationType};
|
||||
use engram_core::types::{Edge, MemoryTier, Node, NodeType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
@@ -54,14 +54,41 @@ pub async fn create_node(
|
||||
Ok(Json(CreateNodeResponse { id }))
|
||||
}
|
||||
|
||||
/// List all nodes (scan-based, no embedding needed)
|
||||
pub async fn list_nodes(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<Node>>, StatusCode> {
|
||||
let db = state.db.lock().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let nodes = db.scan_nodes().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(Json(nodes))
|
||||
}
|
||||
|
||||
pub async fn get_node(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Path(id_str): Path<String>,
|
||||
) -> Result<Json<Node>, StatusCode> {
|
||||
let db = state.db.lock().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
match db.get_node(id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
|
||||
Some(node) => Ok(Json(node)),
|
||||
None => Err(StatusCode::NOT_FOUND),
|
||||
tracing::info!("get_node called with raw id={}", id_str);
|
||||
let id = id_str.parse::<Uuid>().map_err(|e| {
|
||||
tracing::warn!("get_node: invalid UUID '{}': {}", id_str, e);
|
||||
StatusCode::BAD_REQUEST
|
||||
})?;
|
||||
let db = state.db.lock().map_err(|_| {
|
||||
tracing::error!("get_node: mutex poisoned");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
match db.get_node(id) {
|
||||
Ok(Some(node)) => {
|
||||
tracing::info!("get_node: found node id={}", id);
|
||||
Ok(Json(node))
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::warn!("get_node: node not found id={}", id);
|
||||
Err(StatusCode::NOT_FOUND)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("get_node: db error: {:?}", e);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +98,8 @@ pub async fn get_node(
|
||||
pub struct CreateEdgeRequest {
|
||||
pub from_id: Uuid,
|
||||
pub to_id: Uuid,
|
||||
pub relation: RelationType,
|
||||
/// Edge type name, e.g. `"causes"`, `"resonates_with"`, or any dynamic type.
|
||||
pub relation: String,
|
||||
pub weight: f32,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/// The engine wraps an `EngramDb` and a `CommandLog`. All mutations go through
|
||||
/// `apply()`. Rollbacks create new inverse commands. Rolling back a rollback
|
||||
/// re-applies the original — full undo/redo with causal tracking.
|
||||
use engram_core::types::{Edge, Node, NodeType, RelationType};
|
||||
use engram_core::types::{Edge, Node, NodeType};
|
||||
use engram_core::EngramDb;
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
@@ -319,8 +319,7 @@ impl TransactionEngine {
|
||||
|
||||
fn exec_create_edge(&self, payload: &Value) -> TxResult<()> {
|
||||
let p: CreateEdgePayload = serde_json::from_value(payload.clone())?;
|
||||
let relation = parse_relation(&p.relation)?;
|
||||
let edge = Edge::new(p.from_id, p.to_id, relation, p.weight);
|
||||
let edge = Edge::new(p.from_id, p.to_id, p.relation, p.weight);
|
||||
let db = self.db.lock().map_err(|_| TxError::Invalid("lock poisoned".into()))?;
|
||||
db.put_edge(edge)?;
|
||||
Ok(())
|
||||
@@ -465,12 +464,11 @@ pub fn build_create_edge_cmd(
|
||||
idempotency_key: impl Into<String>,
|
||||
peer_id: Option<Uuid>,
|
||||
) -> Command {
|
||||
let relation_str = relation_to_str(&edge.relation).to_string();
|
||||
let payload = serde_json::to_value(CreateEdgePayload {
|
||||
edge_id: edge.id,
|
||||
from_id: edge.from_id,
|
||||
to_id: edge.to_id,
|
||||
relation: relation_str.clone(),
|
||||
relation: edge.relation.clone(),
|
||||
weight: edge.weight,
|
||||
})
|
||||
.unwrap_or(Value::Null);
|
||||
@@ -505,45 +503,19 @@ fn parse_node_type(s: &str) -> TxResult<NodeType> {
|
||||
"Entity" => Ok(NodeType::Entity),
|
||||
"Process" => Ok(NodeType::Process),
|
||||
"InternalState" => Ok(NodeType::InternalState),
|
||||
_ => Err(TxError::Invalid(format!("unknown node type: {}", s))),
|
||||
other => Ok(NodeType::Custom(other.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type_to_str(t: &NodeType) -> &'static str {
|
||||
fn node_type_to_str(t: &NodeType) -> String {
|
||||
match t {
|
||||
NodeType::Memory => "Memory",
|
||||
NodeType::Concept => "Concept",
|
||||
NodeType::Event => "Event",
|
||||
NodeType::Entity => "Entity",
|
||||
NodeType::Process => "Process",
|
||||
NodeType::InternalState => "InternalState",
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_relation(s: &str) -> TxResult<RelationType> {
|
||||
match s {
|
||||
"Supersedes" => Ok(RelationType::Supersedes),
|
||||
"Causes" => Ok(RelationType::Causes),
|
||||
"Contains" => Ok(RelationType::Contains),
|
||||
"References" => Ok(RelationType::References),
|
||||
"Contradicts" => Ok(RelationType::Contradicts),
|
||||
"Exemplifies" => Ok(RelationType::Exemplifies),
|
||||
"Activates" => Ok(RelationType::Activates),
|
||||
"TemporallyPrecedes" => Ok(RelationType::TemporallyPrecedes),
|
||||
_ => Err(TxError::Invalid(format!("unknown relation: {}", s))),
|
||||
}
|
||||
}
|
||||
|
||||
fn relation_to_str(r: &RelationType) -> &'static str {
|
||||
match r {
|
||||
RelationType::Supersedes => "Supersedes",
|
||||
RelationType::Causes => "Causes",
|
||||
RelationType::Contains => "Contains",
|
||||
RelationType::References => "References",
|
||||
RelationType::Contradicts => "Contradicts",
|
||||
RelationType::Exemplifies => "Exemplifies",
|
||||
RelationType::Activates => "Activates",
|
||||
RelationType::TemporallyPrecedes => "TemporallyPrecedes",
|
||||
NodeType::Memory => "Memory".to_string(),
|
||||
NodeType::Concept => "Concept".to_string(),
|
||||
NodeType::Event => "Event".to_string(),
|
||||
NodeType::Entity => "Entity".to_string(),
|
||||
NodeType::Process => "Process".to_string(),
|
||||
NodeType::InternalState => "InternalState".to_string(),
|
||||
NodeType::Custom(s) => s.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,7 +669,7 @@ mod tests {
|
||||
engine.apply(build_create_node_cmd(&n2, "edge-n2", None)).unwrap();
|
||||
|
||||
// Create edge
|
||||
let edge = Edge::new(n1.id, n2.id, RelationType::References, 0.7);
|
||||
let edge = Edge::new(n1.id, n2.id, "references", 0.7);
|
||||
let edge_cmd = build_create_edge_cmd(&edge, "edge-create-1", None);
|
||||
let result = engine.apply(edge_cmd).unwrap();
|
||||
assert_eq!(result.status, CommandStatus::Applied);
|
||||
+9
-8
@@ -7,7 +7,8 @@
|
||||
/// The nodes represent a tiny knowledge graph about the spreading activation
|
||||
/// model itself — somewhat recursive, intentionally.
|
||||
use engram_core::{
|
||||
ActivatedNode, ConsolidationConfig, Edge, EngramDb, MemoryTier, Node, NodeType, RelationType,
|
||||
ActivatedNode, ConsolidationConfig, Edge, EngramDb, MemoryTier, Node, NodeType,
|
||||
EDGE_ACTIVATES, EDGE_CAUSES, EDGE_EXEMPLIFIES, EDGE_REFERENCES, EDGE_TEMPORALLY_PRECEDES,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
@@ -98,17 +99,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// concepts reliably co-activate. Weaker weights mean looser association.
|
||||
|
||||
// Spreading activation Causes long-term potentiation (strong causal link)
|
||||
db.put_edge(Edge::new(id0, id1, RelationType::Causes, 0.9))?;
|
||||
db.put_edge(Edge::new(id0, id1, EDGE_CAUSES, 0.9))?;
|
||||
// LTP is Referenced by Hebbian learning
|
||||
db.put_edge(Edge::new(id1, id2, RelationType::References, 0.85))?;
|
||||
db.put_edge(Edge::new(id1, id2, EDGE_REFERENCES, 0.85))?;
|
||||
// Spreading activation Activates associative memory
|
||||
db.put_edge(Edge::new(id0, id3, RelationType::Activates, 0.88))?;
|
||||
db.put_edge(Edge::new(id0, id3, EDGE_ACTIVATES, 0.88))?;
|
||||
// Hebbian learning Exemplifies associative memory
|
||||
db.put_edge(Edge::new(id2, id3, RelationType::Exemplifies, 0.80))?;
|
||||
// Salience decay Supersedes naive forgetting
|
||||
db.put_edge(Edge::new(id4, id5, RelationType::TemporallyPrecedes, 0.65))?;
|
||||
db.put_edge(Edge::new(id2, id3, EDGE_EXEMPLIFIES, 0.80))?;
|
||||
// Salience decay TemporallyPrecedes naive forgetting
|
||||
db.put_edge(Edge::new(id4, id5, EDGE_TEMPORALLY_PRECEDES, 0.65))?;
|
||||
// LTP TemporallyPrecedes memory consolidation
|
||||
db.put_edge(Edge::new(id1, id5, RelationType::TemporallyPrecedes, 0.72))?;
|
||||
db.put_edge(Edge::new(id1, id5, EDGE_TEMPORALLY_PRECEDES, 0.72))?;
|
||||
|
||||
println!("Inserted {} edges", db.edge_count()?);
|
||||
println!(" node0 --[Causes]--> node1");
|
||||
|
||||
+1272
-269
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user