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",
|
"instant-distance",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sled",
|
"sled",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
|
|||||||
+12
-12
@@ -1,17 +1,17 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/engram-core",
|
"engrams/engram-core",
|
||||||
"crates/engram-ffi",
|
"engrams/engram-ffi",
|
||||||
"crates/engram-jni",
|
"engrams/engram-jni",
|
||||||
"crates/engram-migrate",
|
"engrams/engram-migrate",
|
||||||
"crates/engram-sync",
|
"engrams/engram-sync",
|
||||||
"crates/engram-server",
|
"engrams/engram-server",
|
||||||
"crates/engram-projection",
|
"engrams/engram-projection",
|
||||||
"crates/engram-tx",
|
"engrams/engram-tx",
|
||||||
"crates/engram-crypto",
|
"engrams/engram-crypto",
|
||||||
"crates/engram-reasoning",
|
"engrams/engram-reasoning",
|
||||||
# engram-wasm is in bindings/ and compiled separately via wasm-pack
|
# engram-wasm is in receptors/ and compiled separately via wasm-pack
|
||||||
# (wasm targets can't be in the same workspace build as native targets)
|
# (wasm targets can't be in the same workspace build as native targets)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -34,4 +34,4 @@ edition = "2021"
|
|||||||
migration = ["engram-core/migration"]
|
migration = ["engram-core/migration"]
|
||||||
|
|
||||||
[dependencies]
|
[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 }
|
sled = { version = "0.34", optional = true }
|
||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
bincode = "1"
|
bincode = "1"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
@@ -88,8 +88,9 @@ pub fn consolidate(db: &Db, config: &ConsolidationConfig) -> EngramResult<Consol
|
|||||||
&& node.salience >= config.salience_floor
|
&& node.salience >= config.salience_floor
|
||||||
{
|
{
|
||||||
// Promote: change tier to Semantic and persist.
|
// Promote: change tier to Semantic and persist.
|
||||||
|
// Use overwrite_node — this is an internal state update, not a new node.
|
||||||
node.tier = MemoryTier::Semantic;
|
node.tier = MemoryTier::Semantic;
|
||||||
graph::put_node(db, &node)?;
|
graph::overwrite_node(db, &node)?;
|
||||||
promoted_count += 1;
|
promoted_count += 1;
|
||||||
|
|
||||||
if promoted_count >= config.max_promotions_per_run {
|
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 {
|
if new_sal != node.salience {
|
||||||
node.salience = new_sal;
|
node.salience = new_sal;
|
||||||
storage::write_salience(db, node.id, new_sal)?;
|
storage::write_salience(db, node.id, new_sal)?;
|
||||||
graph::put_node(db, &node)?;
|
graph::overwrite_node(db, &node)?;
|
||||||
decayed_count += 1;
|
decayed_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,11 +14,12 @@
|
|||||||
mod sled_impl {
|
mod sled_impl {
|
||||||
use crate::activation;
|
use crate::activation;
|
||||||
use crate::consolidation::{self, ConsolidationConfig, ConsolidationReport};
|
use crate::consolidation::{self, ConsolidationConfig, ConsolidationReport};
|
||||||
|
use crate::edge_type;
|
||||||
use crate::error::{EngramError, EngramResult};
|
use crate::error::{EngramError, EngramResult};
|
||||||
use crate::graph;
|
use crate::graph;
|
||||||
use crate::salience;
|
use crate::salience;
|
||||||
use crate::storage;
|
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 crate::vector;
|
||||||
use sled::Db;
|
use sled::Db;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -28,16 +29,32 @@ mod sled_impl {
|
|||||||
pub(crate) db: Db,
|
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 {
|
impl EngramDb {
|
||||||
/// Open (or create) an engram database at the given path.
|
/// 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> {
|
pub fn open(path: &Path) -> EngramResult<Self> {
|
||||||
let db = sled::open(path)?;
|
let db = sled::open(path)?;
|
||||||
|
edge_type::seed_builtin_types(&db)?;
|
||||||
Ok(Self { db })
|
Ok(Self { db })
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Node operations ───────────────────────────────────────────────────
|
// ── 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> {
|
pub fn put_node(&self, node: Node) -> EngramResult<Uuid> {
|
||||||
let id = graph::put_node(&self.db, &node)?;
|
let id = graph::put_node(&self.db, &node)?;
|
||||||
// Mark HNSW index dirty so next search rebuilds it.
|
// Mark HNSW index dirty so next search rebuilds it.
|
||||||
@@ -109,15 +126,90 @@ mod sled_impl {
|
|||||||
// ── Graph traversal ───────────────────────────────────────────────────
|
// ── Graph traversal ───────────────────────────────────────────────────
|
||||||
|
|
||||||
/// BFS traversal from `from`, following edges up to `max_depth` hops.
|
/// 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(
|
pub fn traverse(
|
||||||
&self,
|
&self,
|
||||||
from: Uuid,
|
from: Uuid,
|
||||||
relation: Option<RelationType>,
|
relation: Option<&str>,
|
||||||
max_depth: u8,
|
max_depth: u8,
|
||||||
) -> EngramResult<Vec<Node>> {
|
) -> EngramResult<Vec<Node>> {
|
||||||
graph::traverse(&self.db, from, relation, max_depth)
|
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 ───────────────────────────────────────────────
|
// ── Salience management ───────────────────────────────────────────────
|
||||||
|
|
||||||
/// Mark a node as recently activated — update last_activated, increment
|
/// Mark a node as recently activated — update last_activated, increment
|
||||||
@@ -132,7 +224,8 @@ mod sled_impl {
|
|||||||
node.last_activated,
|
node.last_activated,
|
||||||
node.activation_count,
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +247,7 @@ mod sled_impl {
|
|||||||
if new_salience != node.salience {
|
if new_salience != node.salience {
|
||||||
node.salience = new_salience;
|
node.salience = new_salience;
|
||||||
storage::write_salience(&self.db, node.id, 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;
|
count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,9 +319,9 @@ mod wasm_impl {
|
|||||||
use crate::error::{EngramError, EngramResult};
|
use crate::error::{EngramError, EngramResult};
|
||||||
use crate::mem_storage::MemStore;
|
use crate::mem_storage::MemStore;
|
||||||
use crate::salience;
|
use crate::salience;
|
||||||
use crate::types::{ActivatedNode, Edge, Node, RelationType, ScoredNode};
|
use crate::types::{ActivatedNode, Edge, Node, ScoredNode};
|
||||||
use crate::vector;
|
use crate::vector;
|
||||||
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -332,7 +425,7 @@ mod wasm_impl {
|
|||||||
pub fn traverse(
|
pub fn traverse(
|
||||||
&self,
|
&self,
|
||||||
from: Uuid,
|
from: Uuid,
|
||||||
relation: Option<RelationType>,
|
relation: Option<&str>,
|
||||||
max_depth: u8,
|
max_depth: u8,
|
||||||
) -> EngramResult<Vec<Node>> {
|
) -> EngramResult<Vec<Node>> {
|
||||||
let store = self
|
let store = self
|
||||||
@@ -352,8 +445,8 @@ mod wasm_impl {
|
|||||||
}
|
}
|
||||||
let edges = store.read_edges_from(current_id)?;
|
let edges = store.read_edges_from(current_id)?;
|
||||||
for edge in edges {
|
for edge in edges {
|
||||||
if let Some(ref rel) = relation {
|
if let Some(rel) = relation {
|
||||||
if &edge.relation != rel {
|
if edge.relation != rel {
|
||||||
continue;
|
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}")]
|
#[error("Node not found: {0}")]
|
||||||
NotFound(uuid::Uuid),
|
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}")]
|
#[error("Invalid embedding: expected {expected} dimensions, got {got}")]
|
||||||
DimensionMismatch { expected: usize, got: usize },
|
DimensionMismatch { expected: usize, got: usize },
|
||||||
|
|
||||||
@@ -4,17 +4,25 @@
|
|||||||
/// in both directions so that forward and backward traversals are equally cheap.
|
/// in both directions so that forward and backward traversals are equally cheap.
|
||||||
use crate::error::EngramResult;
|
use crate::error::EngramResult;
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
use crate::types::{Edge, Node, RelationType};
|
use crate::types::{Edge, Node};
|
||||||
use sled::Db;
|
use sled::Db;
|
||||||
use std::collections::{HashSet, VecDeque};
|
use std::collections::{HashSet, VecDeque};
|
||||||
use uuid::Uuid;
|
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> {
|
pub fn put_node(db: &Db, node: &Node) -> EngramResult<Uuid> {
|
||||||
storage::write_node(db, node)?;
|
storage::write_node(db, node)?;
|
||||||
Ok(node.id)
|
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.
|
/// Retrieve a node by id. Returns None if not found.
|
||||||
pub fn get_node(db: &Db, id: Uuid) -> EngramResult<Option<Node>> {
|
pub fn get_node(db: &Db, id: Uuid) -> EngramResult<Option<Node>> {
|
||||||
storage::read_node(db, id)
|
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.
|
/// Breadth-first traversal starting from `from`, following forward edges only.
|
||||||
///
|
///
|
||||||
/// If `relation` is Some, only edges of that type are followed.
|
/// If `relation` is `Some(&str)`, only edges whose `relation` field matches
|
||||||
/// The BFS respects `max_depth` hops. The seed node itself is NOT included.
|
/// that string are followed. The BFS respects `max_depth` hops. The seed node
|
||||||
/// Visited nodes are deduplicated.
|
/// itself is NOT included. Visited nodes are deduplicated.
|
||||||
pub fn traverse(
|
pub fn traverse(
|
||||||
db: &Db,
|
db: &Db,
|
||||||
from: Uuid,
|
from: Uuid,
|
||||||
relation: Option<RelationType>,
|
relation: Option<&str>,
|
||||||
max_depth: u8,
|
max_depth: u8,
|
||||||
) -> EngramResult<Vec<Node>> {
|
) -> EngramResult<Vec<Node>> {
|
||||||
let mut visited: HashSet<Uuid> = HashSet::new();
|
let mut visited: HashSet<Uuid> = HashSet::new();
|
||||||
@@ -60,8 +68,8 @@ pub fn traverse(
|
|||||||
let edges = edges_from(db, current_id)?;
|
let edges = edges_from(db, current_id)?;
|
||||||
for edge in edges {
|
for edge in edges {
|
||||||
// Filter by relation type if specified
|
// Filter by relation type if specified
|
||||||
if let Some(ref rel) = relation {
|
if let Some(rel) = relation {
|
||||||
if &edge.relation != rel {
|
if edge.relation != rel {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
/// # Quick Start
|
/// # Quick Start
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```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;
|
/// use std::path::Path;
|
||||||
///
|
///
|
||||||
/// let db = EngramDb::open(Path::new("/tmp/my-engram")).unwrap();
|
/// let db = EngramDb::open(Path::new("/tmp/my-engram")).unwrap();
|
||||||
@@ -32,6 +32,8 @@
|
|||||||
pub mod activation;
|
pub mod activation;
|
||||||
pub mod consolidation;
|
pub mod consolidation;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
#[cfg(not(feature = "wasm"))]
|
||||||
|
pub mod edge_type;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
pub mod salience;
|
pub mod salience;
|
||||||
@@ -48,6 +50,8 @@ pub mod migration;
|
|||||||
pub use db::EngramDb;
|
pub use db::EngramDb;
|
||||||
pub use error::{EngramError, EngramResult};
|
pub use error::{EngramError, EngramResult};
|
||||||
pub use types::{
|
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};
|
pub use consolidation::{ConsolidationConfig, ConsolidationReport};
|
||||||
@@ -10,9 +10,8 @@
|
|||||||
/// | `memory_nodes` | `Node { tier: Episodic, node_type: Memory }` |
|
/// | `memory_nodes` | `Node { tier: Episodic, node_type: Memory }` |
|
||||||
/// | `knowledge_entries` | `Node { tier: Semantic, node_type: Concept }` |
|
/// | `knowledge_entries` | `Node { tier: Semantic, node_type: Concept }` |
|
||||||
///
|
///
|
||||||
/// Edges from `graph_edges` (type = "supersedes" or "Supersedes") are converted
|
/// Edges from `graph_edges` are converted using their `edge_type` string directly
|
||||||
/// to `Edge { relation: RelationType::Supersedes }`. Other edge types become
|
/// (normalised to lowercase). Unknown types map to `"references"`.
|
||||||
/// `RelationType::References`.
|
|
||||||
///
|
///
|
||||||
/// # Embeddings
|
/// # Embeddings
|
||||||
///
|
///
|
||||||
@@ -23,7 +22,7 @@
|
|||||||
/// TODO: wire in real embeddings from all-MiniLM-L6-v2 via the ONNX runtime.
|
/// TODO: wire in real embeddings from all-MiniLM-L6-v2 via the ONNX runtime.
|
||||||
|
|
||||||
use crate::error::{EngramError, EngramResult};
|
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 rusqlite::{Connection, OpenFlags};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -227,7 +226,7 @@ pub fn migrate_from_neuron(config: &MigrationConfig) -> EngramResult<MigrationRe
|
|||||||
None => continue,
|
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);
|
let edge = Edge::new(from_uuid, to_uuid, relation, weight as f32);
|
||||||
|
|
||||||
match engram_db.put_edge(edge) {
|
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.
|
/// Normalise a Neuron edge type string to a canonical Engram edge type name.
|
||||||
fn edge_type_to_relation(edge_type: &str) -> RelationType {
|
///
|
||||||
|
/// 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() {
|
match edge_type.to_lowercase().as_str() {
|
||||||
"supersedes" | "superseded_by" => RelationType::Supersedes,
|
"supersedes" | "superseded_by" => "supersedes",
|
||||||
"causes" | "caused_by" => RelationType::Causes,
|
"causes" | "caused_by" => "causes",
|
||||||
"contains" | "contained_by" => RelationType::Contains,
|
"contains" | "contained_by" => "contains",
|
||||||
"references" | "referenced_by" => RelationType::References,
|
"references" | "referenced_by" => "references",
|
||||||
"contradicts" => RelationType::Contradicts,
|
"contradicts" => "contradicts",
|
||||||
"exemplifies" | "exemplified_by" => RelationType::Exemplifies,
|
"exemplifies" | "exemplified_by" => "exemplifies",
|
||||||
"activates" => RelationType::Activates,
|
"activates" => "activates",
|
||||||
"temporally_precedes" | "follows" => RelationType::TemporallyPrecedes,
|
"temporally_precedes" | "temporallyprecedes" | "follows" => "temporally_precedes",
|
||||||
_ => RelationType::References, // safe default
|
_ => "references", // safe default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,12 +343,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn edge_type_supersedes() {
|
fn edge_type_supersedes() {
|
||||||
assert_eq!(edge_type_to_relation("supersedes"), RelationType::Supersedes);
|
assert_eq!(normalise_edge_type("supersedes"), "supersedes");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn edge_type_unknown_is_references() {
|
fn edge_type_unknown_is_references() {
|
||||||
assert_eq!(edge_type_to_relation("foobar"), RelationType::References);
|
assert_eq!(normalise_edge_type("foobar"), "references");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
/// edges:to:{to}:{from} → reverse index (same Edge bytes)
|
/// edges:to:{to}:{from} → reverse index (same Edge bytes)
|
||||||
/// vectors:{uuid} → raw little-endian f32 bytes
|
/// vectors:{uuid} → raw little-endian f32 bytes
|
||||||
/// salience:{uuid} → 4-byte little-endian f32
|
/// 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::error::{EngramError, EngramResult};
|
||||||
use crate::types::{Edge, Node};
|
use crate::types::{Edge, Node};
|
||||||
use sled::Db;
|
use sled::Db;
|
||||||
@@ -35,8 +36,19 @@ pub fn salience_key(id: Uuid) -> Vec<u8> {
|
|||||||
|
|
||||||
// ── Node storage ─────────────────────────────────────────────────────────────
|
// ── 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<()> {
|
pub fn write_node(db: &Db, node: &Node) -> EngramResult<()> {
|
||||||
let key = node_key(node.id);
|
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)?;
|
let val = bincode::serialize(node)?;
|
||||||
db.insert(key, val)?;
|
db.insert(key, val)?;
|
||||||
|
|
||||||
@@ -52,6 +64,22 @@ pub fn write_node(db: &Db, node: &Node) -> EngramResult<()> {
|
|||||||
Ok(())
|
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>> {
|
pub fn read_node(db: &Db, id: Uuid) -> EngramResult<Option<Node>> {
|
||||||
match db.get(node_key(id))? {
|
match db.get(node_key(id))? {
|
||||||
Some(bytes) => Ok(Some(bincode::deserialize(&bytes)?)),
|
Some(bytes) => Ok(Some(bincode::deserialize(&bytes)?)),
|
||||||
@@ -1,6 +1,20 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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.
|
/// The functional role of a node in the memory graph.
|
||||||
///
|
///
|
||||||
/// Different node types participate in different retrieval patterns:
|
/// Different node types participate in different retrieval patterns:
|
||||||
@@ -8,6 +22,7 @@ use uuid::Uuid;
|
|||||||
/// - Concepts and Entities form the semantic backbone
|
/// - Concepts and Entities form the semantic backbone
|
||||||
/// - Processes encode procedural knowledge
|
/// - Processes encode procedural knowledge
|
||||||
/// - InternalState captures the system's own affective context
|
/// - 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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum NodeType {
|
pub enum NodeType {
|
||||||
/// A specific remembered experience or observation
|
/// A specific remembered experience or observation
|
||||||
@@ -22,6 +37,8 @@ pub enum NodeType {
|
|||||||
Process,
|
Process,
|
||||||
/// An internal affective or motivational state
|
/// An internal affective or motivational state
|
||||||
InternalState,
|
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.
|
/// Where in the memory hierarchy a node currently lives.
|
||||||
@@ -44,28 +61,35 @@ pub enum MemoryTier {
|
|||||||
Procedural,
|
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
|
/// Fields like `deprecated`, `derived_from`, and `supersedes` are data.
|
||||||
/// structure into the graph itself — not just into metadata.
|
/// They are set by the caller (el) and stored verbatim. Rust never inspects
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
/// or acts on them autonomously.
|
||||||
pub enum RelationType {
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
/// This node replaces or obsoletes another
|
pub struct EdgeTypeDef {
|
||||||
Supersedes,
|
/// Stable unique identifier for this edge type record
|
||||||
/// This node is a causal precursor to another
|
pub id: Uuid,
|
||||||
Causes,
|
/// The canonical name used on edges (e.g. `"causes"`, `"resonates_with"`)
|
||||||
/// This node hierarchically contains another
|
pub name: String,
|
||||||
Contains,
|
/// Human-readable description of what this relation means
|
||||||
/// This node cites or points to another as supporting context
|
pub description: String,
|
||||||
References,
|
/// Unix milliseconds when this type was first registered
|
||||||
/// This node is in logical tension with another
|
pub first_observed: i64,
|
||||||
Contradicts,
|
/// How many edges currently carry this type
|
||||||
/// This node is a concrete instance of a more abstract node
|
pub instance_count: u64,
|
||||||
Exemplifies,
|
/// Caller-supplied confidence, 0.0–1.0. Stored as-is; not computed here.
|
||||||
/// Co-activation: firing this node tends to fire the other
|
pub confidence: f32,
|
||||||
Activates,
|
/// Free-text note about what observation prompted this type's creation.
|
||||||
/// Temporal ordering: this node came before the other
|
/// Set by el; stored verbatim.
|
||||||
TemporallyPrecedes,
|
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.
|
/// 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).
|
/// Edge weights strengthen with co-activation (Hebbian learning).
|
||||||
/// The weight directly multiplies activation flow during spreading activation.
|
/// 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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Edge {
|
pub struct Edge {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub from_id: Uuid,
|
pub from_id: Uuid,
|
||||||
pub to_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
|
/// Connection strength, 0.0–1.0. Increases when both endpoints are
|
||||||
/// activated in close temporal proximity (long-term potentiation).
|
/// activated in close temporal proximity (long-term potentiation).
|
||||||
pub weight: f32,
|
pub weight: f32,
|
||||||
@@ -176,13 +205,13 @@ pub struct Edge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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();
|
let now = now_ms();
|
||||||
Self {
|
Self {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
from_id,
|
from_id,
|
||||||
to_id,
|
to_id,
|
||||||
relation,
|
relation: relation.into(),
|
||||||
weight: weight.clamp(0.0, 1.0),
|
weight: weight.clamp(0.0, 1.0),
|
||||||
created_at: now,
|
created_at: now,
|
||||||
last_fired: now,
|
last_fired: now,
|
||||||
@@ -98,7 +98,7 @@ fn matches_filter(a: &ActivatedNode, filter: &NodeFilter) -> bool {
|
|||||||
|
|
||||||
NodeFilter::ByType(types) => {
|
NodeFilter::ByType(types) => {
|
||||||
let node_type_str = node_type_str(&a.node.node_type);
|
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),
|
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 ────────────────────────────────────────────────────────────
|
// ── String helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn node_type_str(t: &NodeType) -> &'static str {
|
fn node_type_str(t: &NodeType) -> String {
|
||||||
match t {
|
match t {
|
||||||
NodeType::Memory => "Memory",
|
NodeType::Memory => "Memory".to_string(),
|
||||||
NodeType::Concept => "Concept",
|
NodeType::Concept => "Concept".to_string(),
|
||||||
NodeType::Event => "Event",
|
NodeType::Event => "Event".to_string(),
|
||||||
NodeType::Entity => "Entity",
|
NodeType::Entity => "Entity".to_string(),
|
||||||
NodeType::Process => "Process",
|
NodeType::Process => "Process".to_string(),
|
||||||
NodeType::InternalState => "InternalState",
|
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 std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use engram_core::{EngramDb, EngramResult};
|
use engram_core::{EngramDb, EngramResult};
|
||||||
use engram_core::types::{Node, NodeType, RelationType};
|
use engram_core::types::{Node, NodeType};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
@@ -223,7 +223,7 @@ impl ReasoningEngine {
|
|||||||
|
|
||||||
/// Find causal chains: what causes a concept, or what a concept causes.
|
/// 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.
|
/// requested direction.
|
||||||
pub fn causal_chain(
|
pub fn causal_chain(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -252,7 +252,7 @@ impl ReasoningEngine {
|
|||||||
let db = self.db.lock().map_err(|_| {
|
let db = self.db.lock().map_err(|_| {
|
||||||
engram_core::EngramError::InvalidParam("db lock poisoned".into())
|
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
|
// 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.
|
/// 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.
|
/// from Process/Procedural nodes matching the goal embedding.
|
||||||
pub fn procedural_chain(&mut self, goal_embedding: &[f32]) -> EngramResult<Vec<String>> {
|
pub fn procedural_chain(&mut self, goal_embedding: &[f32]) -> EngramResult<Vec<String>> {
|
||||||
let process_nodes: Vec<Node> = {
|
let process_nodes: Vec<Node> = {
|
||||||
@@ -363,7 +363,7 @@ impl ReasoningEngine {
|
|||||||
let db = self.db.lock().map_err(|_| {
|
let db = self.db.lock().map_err(|_| {
|
||||||
engram_core::EngramError::InvalidParam("db lock poisoned".into())
|
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();
|
let mut ordered_steps: Vec<String> = Vec::new();
|
||||||
@@ -437,7 +437,7 @@ impl ReasoningEngine {
|
|||||||
if let Some(from_node) = db.get_node(*node_id)? {
|
if let Some(from_node) = db.get_node(*node_id)? {
|
||||||
let edges = db.get_edges_from(*node_id)?;
|
let edges = db.get_edges_from(*node_id)?;
|
||||||
for edge in edges {
|
for edge in edges {
|
||||||
if edge.relation == RelationType::Contradicts {
|
if edge.relation == "contradicts" {
|
||||||
if let Some(target) = db.get_node(edge.to_id)? {
|
if let Some(target) = db.get_node(edge.to_id)? {
|
||||||
let sim_a = cosine_sim(topic_embedding, &from_node.embedding);
|
let sim_a = cosine_sim(topic_embedding, &from_node.embedding);
|
||||||
let sim_b = cosine_sim(topic_embedding, &target.embedding);
|
let sim_b = cosine_sim(topic_embedding, &target.embedding);
|
||||||
@@ -976,16 +976,21 @@ impl ReasoningEngine {
|
|||||||
|
|
||||||
// ── Edge mapping helpers ──────────────────────────────────────────────────────
|
// ── Edge mapping helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn relation_to_inference_edge(relation: &RelationType) -> InferenceEdgeType {
|
fn relation_to_inference_edge(relation: &str) -> InferenceEdgeType {
|
||||||
match relation {
|
match relation {
|
||||||
RelationType::Causes => InferenceEdgeType::Causes,
|
"causes" => InferenceEdgeType::Causes,
|
||||||
RelationType::Contradicts => InferenceEdgeType::Contradicts,
|
"contradicts" => InferenceEdgeType::Contradicts,
|
||||||
RelationType::Supersedes => InferenceEdgeType::Implies,
|
"supersedes" => InferenceEdgeType::Implies,
|
||||||
RelationType::Contains => InferenceEdgeType::Requires,
|
"contains" => InferenceEdgeType::Requires,
|
||||||
RelationType::References => InferenceEdgeType::SimilarTo,
|
"references" => InferenceEdgeType::SimilarTo,
|
||||||
RelationType::Exemplifies => InferenceEdgeType::InstanceOf,
|
"exemplifies" => InferenceEdgeType::InstanceOf,
|
||||||
RelationType::Activates => InferenceEdgeType::Supports,
|
"activates" => InferenceEdgeType::Supports,
|
||||||
RelationType::TemporallyPrecedes => InferenceEdgeType::Causes,
|
"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
|
/// # Quick Start
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```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 engram_reasoning::{ReasoningEngine, Hypothesis, HypothesisType, ReasoningConfig};
|
||||||
/// use std::path::Path;
|
/// use std::path::Path;
|
||||||
/// use std::sync::{Arc, Mutex};
|
/// use std::sync::{Arc, Mutex};
|
||||||
@@ -9,7 +9,7 @@ mod tests {
|
|||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use engram_core::{Edge, EngramDb, MemoryTier, Node, NodeType, RelationType};
|
use engram_core::{Edge, EngramDb, MemoryTier, Node, NodeType};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CausalDirection, EvidenceNode, EvidenceType, Hypothesis, HypothesisType, InferenceEdge,
|
CausalDirection, EvidenceNode, EvidenceType, Hypothesis, HypothesisType, InferenceEdge,
|
||||||
@@ -51,7 +51,7 @@ mod tests {
|
|||||||
id
|
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);
|
let edge = Edge::new(from, to, relation, 0.8);
|
||||||
db.lock().unwrap().put_edge(edge).unwrap();
|
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_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);
|
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_a, id_b, "causes");
|
||||||
make_edge(&db, id_b, id_c, RelationType::Causes);
|
make_edge(&db, id_b, id_c, "causes");
|
||||||
|
|
||||||
let mut engine = make_engine(db.clone());
|
let mut engine = make_engine(db.clone());
|
||||||
let chains = engine.causal_chain(&emb_a, CausalDirection::Forward).unwrap();
|
let chains = engine.causal_chain(&emb_a, CausalDirection::Forward).unwrap();
|
||||||
@@ -373,7 +373,7 @@ mod tests {
|
|||||||
NodeType::Memory,
|
NodeType::Memory,
|
||||||
0.9,
|
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 mut engine = make_engine(db.clone());
|
||||||
let contradictions = engine.find_contradictions(&emb_topic).unwrap();
|
let contradictions = engine.find_contradictions(&emb_topic).unwrap();
|
||||||
@@ -482,8 +482,8 @@ mod tests {
|
|||||||
NodeType::Process,
|
NodeType::Process,
|
||||||
0.9,
|
0.9,
|
||||||
);
|
);
|
||||||
make_edge(&db, id_1, id_2, RelationType::Causes);
|
make_edge(&db, id_1, id_2, "causes");
|
||||||
make_edge(&db, id_2, id_3, RelationType::Causes);
|
make_edge(&db, id_2, id_3, "causes");
|
||||||
|
|
||||||
let mut engine = make_engine(db.clone());
|
let mut engine = make_engine(db.clone());
|
||||||
let steps = engine.procedural_chain(&goal_emb).unwrap();
|
let steps = engine.procedural_chain(&goal_emb).unwrap();
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
/// ## Core
|
/// ## Core
|
||||||
/// GET /stats — node/edge counts
|
/// GET /stats — node/edge counts
|
||||||
/// POST /nodes — create a node
|
/// POST /nodes — create a node
|
||||||
/// GET /nodes/{id} — get a node
|
/// GET /nodes/:id — get a node
|
||||||
/// POST /edges — create an edge
|
/// 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 /activate — spreading activation
|
||||||
/// POST /search — embedding search
|
/// POST /search — embedding search
|
||||||
/// POST /decay — apply salience decay
|
/// POST /decay — apply salience decay
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
/// POST /sync/push — receive incoming delta
|
/// POST /sync/push — receive incoming delta
|
||||||
/// POST /sync/peers — register peer
|
/// POST /sync/peers — register peer
|
||||||
/// GET /sync/peers — list peers
|
/// GET /sync/peers — list peers
|
||||||
/// DELETE /sync/peers/{id} — remove peer
|
/// DELETE /sync/peers/:id — remove peer
|
||||||
///
|
///
|
||||||
/// ## Swarm
|
/// ## Swarm
|
||||||
/// POST /swarm/activate — distributed activation (auth required)
|
/// 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/push", post(routes::sync::push_delta))
|
||||||
.route("/sync/peers", get(routes::sync::list_peers))
|
.route("/sync/peers", get(routes::sync::list_peers))
|
||||||
.route("/sync/peers", post(routes::sync::register_peer))
|
.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/activate", post(routes::swarm::swarm_activate))
|
||||||
.route("/swarm/status", get(routes::swarm::swarm_status))
|
.route("/swarm/status", get(routes::swarm::swarm_status))
|
||||||
.layer(middleware::from_fn_with_state(
|
.layer(middleware::from_fn_with_state(
|
||||||
@@ -200,9 +200,10 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let core_routes = Router::new()
|
let core_routes = Router::new()
|
||||||
.route("/stats", get(routes::core::get_stats))
|
.route("/stats", get(routes::core::get_stats))
|
||||||
.route("/nodes", post(routes::core::create_node))
|
.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("/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("/activate", post(routes::core::activate))
|
||||||
.route("/search", post(routes::core::search_embedding))
|
.route("/search", post(routes::core::search_embedding))
|
||||||
.route("/decay", post(routes::core::decay))
|
.route("/decay", post(routes::core::decay))
|
||||||
@@ -213,20 +214,20 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.route("/projections", post(routes::projection::register_projection))
|
.route("/projections", post(routes::projection::register_projection))
|
||||||
.route("/projections", get(routes::projection::list_projections))
|
.route("/projections", get(routes::projection::list_projections))
|
||||||
.route(
|
.route(
|
||||||
"/projections/{name}/schema",
|
"/projections/:name/schema",
|
||||||
get(routes::projection::get_projection_schema),
|
get(routes::projection::get_projection_schema),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/projections/{name}/query",
|
"/projections/:name/query",
|
||||||
post(routes::projection::query_projection),
|
post(routes::projection::query_projection),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Transaction routes (no auth — add auth layer if needed)
|
// Transaction routes (no auth — add auth layer if needed)
|
||||||
let tx_routes = Router::new()
|
let tx_routes = Router::new()
|
||||||
.route("/tx/apply", post(routes::tx::tx_apply))
|
.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/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)
|
// Reasoning routes (no auth — graph-native inference)
|
||||||
let reasoning_routes = Router::new()
|
let reasoning_routes = Router::new()
|
||||||
@@ -234,18 +235,47 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.route("/reason/causal", post(routes::reasoning::causal))
|
.route("/reason/causal", post(routes::reasoning::causal))
|
||||||
.route("/reason/contradictions", post(routes::reasoning::contradictions));
|
.route("/reason/contradictions", post(routes::reasoning::contradictions));
|
||||||
|
|
||||||
let studio_routes = Router::new()
|
let app = Router::new()
|
||||||
|
// Studio
|
||||||
.route("/", get(serve_studio_index))
|
.route("/", get(serve_studio_index))
|
||||||
.route("/studio", get(serve_studio_index))
|
.route("/studio", get(serve_studio_index))
|
||||||
.route("/studio/{*path}", get(serve_studio_asset));
|
// Core API
|
||||||
|
.route("/stats", get(routes::core::get_stats))
|
||||||
let app = Router::new()
|
.route("/nodes", post(routes::core::create_node))
|
||||||
.merge(studio_routes)
|
.route("/nodes/list", get(routes::core::list_nodes))
|
||||||
.merge(core_routes)
|
.route("/nodes/:id", get(routes::core::get_node))
|
||||||
.merge(sync_routes)
|
.route("/edges", post(routes::core::create_edge))
|
||||||
.merge(projection_routes)
|
.route("/nodes/:id/edges", get(routes::core::get_edges_from))
|
||||||
.merge(tx_routes)
|
.route("/activate", post(routes::core::activate))
|
||||||
.merge(reasoning_routes)
|
.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())
|
.layer(CorsLayer::permissive())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ use axum::{
|
|||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use engram_core::types::{Edge, MemoryTier, Node, NodeType, RelationType};
|
use engram_core::types::{Edge, MemoryTier, Node, NodeType};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -54,14 +54,41 @@ pub async fn create_node(
|
|||||||
Ok(Json(CreateNodeResponse { id }))
|
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(
|
pub async fn get_node(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<Uuid>,
|
Path(id_str): Path<String>,
|
||||||
) -> Result<Json<Node>, StatusCode> {
|
) -> Result<Json<Node>, StatusCode> {
|
||||||
let db = state.db.lock().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
tracing::info!("get_node called with raw id={}", id_str);
|
||||||
match db.get_node(id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
|
let id = id_str.parse::<Uuid>().map_err(|e| {
|
||||||
Some(node) => Ok(Json(node)),
|
tracing::warn!("get_node: invalid UUID '{}': {}", id_str, e);
|
||||||
None => Err(StatusCode::NOT_FOUND),
|
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 struct CreateEdgeRequest {
|
||||||
pub from_id: Uuid,
|
pub from_id: Uuid,
|
||||||
pub to_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,
|
pub weight: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
/// The engine wraps an `EngramDb` and a `CommandLog`. All mutations go through
|
/// The engine wraps an `EngramDb` and a `CommandLog`. All mutations go through
|
||||||
/// `apply()`. Rollbacks create new inverse commands. Rolling back a rollback
|
/// `apply()`. Rollbacks create new inverse commands. Rolling back a rollback
|
||||||
/// re-applies the original — full undo/redo with causal tracking.
|
/// 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 engram_core::EngramDb;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -319,8 +319,7 @@ impl TransactionEngine {
|
|||||||
|
|
||||||
fn exec_create_edge(&self, payload: &Value) -> TxResult<()> {
|
fn exec_create_edge(&self, payload: &Value) -> TxResult<()> {
|
||||||
let p: CreateEdgePayload = serde_json::from_value(payload.clone())?;
|
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, p.relation, p.weight);
|
||||||
let edge = Edge::new(p.from_id, p.to_id, relation, p.weight);
|
|
||||||
let db = self.db.lock().map_err(|_| TxError::Invalid("lock poisoned".into()))?;
|
let db = self.db.lock().map_err(|_| TxError::Invalid("lock poisoned".into()))?;
|
||||||
db.put_edge(edge)?;
|
db.put_edge(edge)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -465,12 +464,11 @@ pub fn build_create_edge_cmd(
|
|||||||
idempotency_key: impl Into<String>,
|
idempotency_key: impl Into<String>,
|
||||||
peer_id: Option<Uuid>,
|
peer_id: Option<Uuid>,
|
||||||
) -> Command {
|
) -> Command {
|
||||||
let relation_str = relation_to_str(&edge.relation).to_string();
|
|
||||||
let payload = serde_json::to_value(CreateEdgePayload {
|
let payload = serde_json::to_value(CreateEdgePayload {
|
||||||
edge_id: edge.id,
|
edge_id: edge.id,
|
||||||
from_id: edge.from_id,
|
from_id: edge.from_id,
|
||||||
to_id: edge.to_id,
|
to_id: edge.to_id,
|
||||||
relation: relation_str.clone(),
|
relation: edge.relation.clone(),
|
||||||
weight: edge.weight,
|
weight: edge.weight,
|
||||||
})
|
})
|
||||||
.unwrap_or(Value::Null);
|
.unwrap_or(Value::Null);
|
||||||
@@ -505,45 +503,19 @@ fn parse_node_type(s: &str) -> TxResult<NodeType> {
|
|||||||
"Entity" => Ok(NodeType::Entity),
|
"Entity" => Ok(NodeType::Entity),
|
||||||
"Process" => Ok(NodeType::Process),
|
"Process" => Ok(NodeType::Process),
|
||||||
"InternalState" => Ok(NodeType::InternalState),
|
"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 {
|
match t {
|
||||||
NodeType::Memory => "Memory",
|
NodeType::Memory => "Memory".to_string(),
|
||||||
NodeType::Concept => "Concept",
|
NodeType::Concept => "Concept".to_string(),
|
||||||
NodeType::Event => "Event",
|
NodeType::Event => "Event".to_string(),
|
||||||
NodeType::Entity => "Entity",
|
NodeType::Entity => "Entity".to_string(),
|
||||||
NodeType::Process => "Process",
|
NodeType::Process => "Process".to_string(),
|
||||||
NodeType::InternalState => "InternalState",
|
NodeType::InternalState => "InternalState".to_string(),
|
||||||
}
|
NodeType::Custom(s) => s.clone(),
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,7 +669,7 @@ mod tests {
|
|||||||
engine.apply(build_create_node_cmd(&n2, "edge-n2", None)).unwrap();
|
engine.apply(build_create_node_cmd(&n2, "edge-n2", None)).unwrap();
|
||||||
|
|
||||||
// Create edge
|
// 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 edge_cmd = build_create_edge_cmd(&edge, "edge-create-1", None);
|
||||||
let result = engine.apply(edge_cmd).unwrap();
|
let result = engine.apply(edge_cmd).unwrap();
|
||||||
assert_eq!(result.status, CommandStatus::Applied);
|
assert_eq!(result.status, CommandStatus::Applied);
|
||||||
+9
-8
@@ -7,7 +7,8 @@
|
|||||||
/// The nodes represent a tiny knowledge graph about the spreading activation
|
/// The nodes represent a tiny knowledge graph about the spreading activation
|
||||||
/// model itself — somewhat recursive, intentionally.
|
/// model itself — somewhat recursive, intentionally.
|
||||||
use engram_core::{
|
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;
|
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.
|
// concepts reliably co-activate. Weaker weights mean looser association.
|
||||||
|
|
||||||
// Spreading activation Causes long-term potentiation (strong causal link)
|
// 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
|
// 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
|
// 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
|
// Hebbian learning Exemplifies associative memory
|
||||||
db.put_edge(Edge::new(id2, id3, RelationType::Exemplifies, 0.80))?;
|
db.put_edge(Edge::new(id2, id3, EDGE_EXEMPLIFIES, 0.80))?;
|
||||||
// Salience decay Supersedes naive forgetting
|
// Salience decay TemporallyPrecedes naive forgetting
|
||||||
db.put_edge(Edge::new(id4, id5, RelationType::TemporallyPrecedes, 0.65))?;
|
db.put_edge(Edge::new(id4, id5, EDGE_TEMPORALLY_PRECEDES, 0.65))?;
|
||||||
// LTP TemporallyPrecedes memory consolidation
|
// 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!("Inserted {} edges", db.edge_count()?);
|
||||||
println!(" node0 --[Causes]--> node1");
|
println!(" node0 --[Causes]--> node1");
|
||||||
|
|||||||
+1272
-269
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user