forked from kent/consciousness
store: protected nodes, explicit provenance in mutations
- Add protected_nodes config list - blocks delete/rename of core nodes - Remove current_provenance() env var lookup, pass provenance explicitly - delete_node, rename_node, set_link_strength now take provenance param - Fix new_relation calls in admin.rs to pass "system" provenance Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
cc29cd2225
commit
6ec7fcb777
4 changed files with 159 additions and 38 deletions
|
|
@ -56,8 +56,30 @@ pub async fn cmd_init() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cmd_fsck() -> Result<()> {
|
pub async fn cmd_fsck() -> Result<()> {
|
||||||
// Check/repair capnp log integrity first
|
// Full fsck: verify capnp logs and compare index with rebuilt
|
||||||
store::fsck()?;
|
let report = store::fsck_full()?;
|
||||||
|
|
||||||
|
if report.capnp_repaired {
|
||||||
|
eprintln!("capnp log was repaired (corrupt messages truncated)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !report.zombies.is_empty() {
|
||||||
|
eprintln!("\nZOMBIE entries (in index but not in log):");
|
||||||
|
for key in &report.zombies {
|
||||||
|
eprintln!(" {}", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !report.missing.is_empty() {
|
||||||
|
eprintln!("\nMISSING entries (in log but not in index):");
|
||||||
|
for key in &report.missing {
|
||||||
|
eprintln!(" {}", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !report.is_clean() {
|
||||||
|
eprintln!("\nTo repair: poc-memory admin repair-index");
|
||||||
|
}
|
||||||
|
|
||||||
let store = memory::access_local()?;
|
let store = memory::access_local()?;
|
||||||
|
|
||||||
|
|
@ -110,6 +132,12 @@ pub async fn cmd_fsck() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn cmd_repair_index() -> Result<()> {
|
||||||
|
store::repair_index()?;
|
||||||
|
println!("Index repaired successfully.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn cmd_dedup(apply: bool) -> Result<()> {
|
pub async fn cmd_dedup(apply: bool) -> Result<()> {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
@ -255,6 +283,7 @@ pub async fn cmd_dedup(apply: bool) -> Result<()> {
|
||||||
store::RelationType::from_u8(rel_type), strength,
|
store::RelationType::from_u8(rel_type), strength,
|
||||||
&uuid_to_key.get(&old_src).cloned().unwrap_or_default(),
|
&uuid_to_key.get(&old_src).cloned().unwrap_or_default(),
|
||||||
&uuid_to_key.get(&old_tgt).cloned().unwrap_or_default(),
|
&uuid_to_key.get(&old_tgt).cloned().unwrap_or_default(),
|
||||||
|
"system",
|
||||||
);
|
);
|
||||||
tombstone.deleted = true;
|
tombstone.deleted = true;
|
||||||
tombstone.version = 2;
|
tombstone.version = 2;
|
||||||
|
|
@ -263,6 +292,7 @@ pub async fn cmd_dedup(apply: bool) -> Result<()> {
|
||||||
new_src, new_tgt,
|
new_src, new_tgt,
|
||||||
store::RelationType::from_u8(rel_type), strength,
|
store::RelationType::from_u8(rel_type), strength,
|
||||||
&src_key, &tgt_key,
|
&src_key, &tgt_key,
|
||||||
|
"system",
|
||||||
);
|
);
|
||||||
redirected.version = 2;
|
redirected.version = 2;
|
||||||
|
|
||||||
|
|
@ -299,6 +329,7 @@ pub async fn cmd_dedup(apply: bool) -> Result<()> {
|
||||||
src, tgt,
|
src, tgt,
|
||||||
store::RelationType::from_u8(rel_type), strength,
|
store::RelationType::from_u8(rel_type), strength,
|
||||||
&src_key, &tgt_key,
|
&src_key, &tgt_key,
|
||||||
|
"system",
|
||||||
);
|
);
|
||||||
tombstone.deleted = true;
|
tombstone.deleted = true;
|
||||||
tombstone.version = 2;
|
tombstone.version = 2;
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,9 @@ pub struct Config {
|
||||||
#[serde(deserialize_with = "deserialize_path")]
|
#[serde(deserialize_with = "deserialize_path")]
|
||||||
pub projects_dir: PathBuf,
|
pub projects_dir: PathBuf,
|
||||||
pub core_nodes: Vec<String>,
|
pub core_nodes: Vec<String>,
|
||||||
|
/// Nodes that cannot be deleted or renamed without --force
|
||||||
|
#[serde(default)]
|
||||||
|
pub protected_nodes: Vec<String>,
|
||||||
pub journal_days: u32,
|
pub journal_days: u32,
|
||||||
pub journal_max: usize,
|
pub journal_max: usize,
|
||||||
pub context_groups: Vec<ContextGroup>,
|
pub context_groups: Vec<ContextGroup>,
|
||||||
|
|
@ -146,6 +149,7 @@ impl Default for Config {
|
||||||
identity_dir: home.join(".consciousness/identity"),
|
identity_dir: home.join(".consciousness/identity"),
|
||||||
projects_dir: home.join(".claude/projects"),
|
projects_dir: home.join(".claude/projects"),
|
||||||
core_nodes: vec!["identity".to_string(), "core-practices".to_string()],
|
core_nodes: vec!["identity".to_string(), "core-practices".to_string()],
|
||||||
|
protected_nodes: Vec::new(),
|
||||||
journal_days: 7,
|
journal_days: 7,
|
||||||
journal_max: 20,
|
journal_max: 20,
|
||||||
context_groups: vec![
|
context_groups: vec![
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,10 @@ pub fn memory_links(store: &Store, _provenance: &str, key: &str) -> Result<Vec<L
|
||||||
Ok(links)
|
Ok(links)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn memory_link_set(store: &Store, _provenance: &str, source: &str, target: &str, strength: f32) -> Result<String> {
|
pub fn memory_link_set(store: &Store, provenance: &str, source: &str, target: &str, strength: f32) -> Result<String> {
|
||||||
let s = store.resolve_key(source).map_err(|e| anyhow::anyhow!("{}", e))?;
|
let s = store.resolve_key(source).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
let t = store.resolve_key(target).map_err(|e| anyhow::anyhow!("{}", e))?;
|
let t = store.resolve_key(target).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
let old = store.set_link_strength(&s, &t, strength).map_err(|e| anyhow::anyhow!("{}", e))?;
|
let old = store.set_link_strength(&s, &t, strength, provenance).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
Ok(format!("{} ↔ {} strength {:.2} → {:.2}", s, t, old, strength))
|
Ok(format!("{} ↔ {} strength {:.2} → {:.2}", s, t, old, strength))
|
||||||
}
|
}
|
||||||
|
|
@ -107,13 +107,19 @@ pub fn memory_link_add(store: &Store, provenance: &str, source: &str, target: &s
|
||||||
Ok(format!("linked {} → {} (strength={:.2})", s, t, strength))
|
Ok(format!("linked {} → {} (strength={:.2})", s, t, strength))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn memory_delete(store: &Store, _provenance: &str, key: &str) -> Result<String> {
|
pub fn memory_delete(store: &Store, provenance: &str, key: &str) -> Result<String> {
|
||||||
let resolved = store.resolve_key(key).map_err(|e| anyhow::anyhow!("{}", e))?;
|
let resolved = store.resolve_key(key).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
store.delete_node(&resolved).map_err(|e| anyhow::anyhow!("{}", e))?;
|
store.delete_node(&resolved, provenance).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
Ok(format!("deleted {}", resolved))
|
Ok(format!("deleted {}", resolved))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn memory_restore(store: &Store, provenance: &str, key: &str) -> Result<String> {
|
||||||
|
let result = store.restore_node(key, provenance).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
|
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn memory_history(store: &Store, _provenance: &str, key: &str, full: Option<bool>) -> Result<String> {
|
pub fn memory_history(store: &Store, _provenance: &str, key: &str, full: Option<bool>) -> Result<String> {
|
||||||
let key = store.resolve_key(key).unwrap_or_else(|_| key.to_string());
|
let key = store.resolve_key(key).unwrap_or_else(|_| key.to_string());
|
||||||
let full = full.unwrap_or(false);
|
let full = full.unwrap_or(false);
|
||||||
|
|
@ -171,9 +177,9 @@ pub fn memory_weight_set(store: &Store, _provenance: &str, key: &str, weight: f3
|
||||||
Ok(format!("weight {} {:.2} → {:.2}", resolved, old, new))
|
Ok(format!("weight {} {:.2} → {:.2}", resolved, old, new))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn memory_rename(store: &Store, _provenance: &str, old_key: &str, new_key: &str) -> Result<String> {
|
pub fn memory_rename(store: &Store, provenance: &str, old_key: &str, new_key: &str) -> Result<String> {
|
||||||
let resolved = store.resolve_key(old_key).map_err(|e| anyhow::anyhow!("{}", e))?;
|
let resolved = store.resolve_key(old_key).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
store.rename_node(&resolved, new_key).map_err(|e| anyhow::anyhow!("{}", e))?;
|
store.rename_node(&resolved, new_key, provenance).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
Ok(format!("Renamed '{}' → '{}'", resolved, new_key))
|
Ok(format!("Renamed '{}' → '{}'", resolved, new_key))
|
||||||
}
|
}
|
||||||
|
|
@ -184,14 +190,44 @@ pub fn memory_supersede(store: &Store, provenance: &str, old_key: &str, new_key:
|
||||||
.map_err(|e| anyhow::anyhow!("{}", e))?
|
.map_err(|e| anyhow::anyhow!("{}", e))?
|
||||||
.map(|n| n.content)
|
.map(|n| n.content)
|
||||||
.ok_or_else(|| anyhow::anyhow!("node not found: {}", old_key))?;
|
.ok_or_else(|| anyhow::anyhow!("node not found: {}", old_key))?;
|
||||||
|
|
||||||
|
// Transfer links from old node to new node (if new_key exists)
|
||||||
|
let mut links_transferred = 0;
|
||||||
|
if store.contains_key(new_key).unwrap_or(false) {
|
||||||
|
// Get old node's neighbors
|
||||||
|
let old_neighbors = store.neighbors(old_key).unwrap_or_default();
|
||||||
|
// Get new node's existing neighbors (to avoid weakening existing links)
|
||||||
|
let new_neighbors: std::collections::HashMap<String, f32> = store.neighbors(new_key)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (neighbor_key, old_strength) in old_neighbors {
|
||||||
|
// Skip self-links
|
||||||
|
if neighbor_key == new_key { continue; }
|
||||||
|
// Only add/strengthen link if new node doesn't have a stronger one
|
||||||
|
let current = new_neighbors.get(&neighbor_key).copied().unwrap_or(0.0);
|
||||||
|
if old_strength > current {
|
||||||
|
if store.set_link_strength(new_key, &neighbor_key, old_strength, provenance).is_ok() {
|
||||||
|
links_transferred += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let notice = format!("**SUPERSEDED** by `{}` — {}\n\n---\n\n{}",
|
let notice = format!("**SUPERSEDED** by `{}` — {}\n\n---\n\n{}",
|
||||||
new_key, reason, content.trim());
|
new_key, reason, content.trim());
|
||||||
store.upsert_provenance(old_key, ¬ice, provenance)
|
store.upsert_provenance(old_key, ¬ice, provenance)
|
||||||
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
store.set_weight(old_key, 0.01).map_err(|e| anyhow::anyhow!("{}", e))?;
|
store.set_weight(old_key, 0.01).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
store.save().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
|
|
||||||
|
if links_transferred > 0 {
|
||||||
|
Ok(format!("superseded {} → {} ({}), transferred {} links", old_key, new_key, reason, links_transferred))
|
||||||
|
} else {
|
||||||
Ok(format!("superseded {} → {} ({})", old_key, new_key, reason))
|
Ok(format!("superseded {} → {} ({})", old_key, new_key, reason))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert a list of keys to ReplayItems with priority and graph metrics.
|
/// Convert a list of keys to ReplayItems with priority and graph metrics.
|
||||||
pub fn keys_to_replay_items(
|
pub fn keys_to_replay_items(
|
||||||
|
|
@ -396,7 +432,7 @@ pub fn graph_communities(store: &Store, _provenance: &str, top_n: Option<usize>,
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn graph_normalize_strengths(store: &Store, _provenance: &str, apply: Option<bool>) -> Result<String> {
|
pub fn graph_normalize_strengths(store: &Store, provenance: &str, apply: Option<bool>) -> Result<String> {
|
||||||
use crate::store::{StoreView, RelationType};
|
use crate::store::{StoreView, RelationType};
|
||||||
|
|
||||||
let apply = apply.unwrap_or(false);
|
let apply = apply.unwrap_or(false);
|
||||||
|
|
@ -459,7 +495,7 @@ pub fn graph_normalize_strengths(store: &Store, _provenance: &str, apply: Option
|
||||||
|
|
||||||
if apply {
|
if apply {
|
||||||
for (source, target, new_strength) in to_update {
|
for (source, target, new_strength) in to_update {
|
||||||
store.set_link_strength(&source, &target, new_strength)?;
|
store.set_link_strength(&source, &target, new_strength, provenance)?;
|
||||||
}
|
}
|
||||||
writeln!(out, "\nApplied {} strength updates.", changed).ok();
|
writeln!(out, "\nApplied {} strength updates.", changed).ok();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,12 @@ use super::{index, types::*, Store};
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
/// Fallback provenance for non-tool-dispatch paths (CLI, digest, etc.).
|
|
||||||
/// Tool dispatch passes provenance directly through thought::dispatch.
|
/// Check if a key is protected from deletion/rename.
|
||||||
pub fn current_provenance() -> String {
|
/// Uses protected_nodes list from config.
|
||||||
std::env::var("POC_PROVENANCE")
|
pub fn is_protected(key: &str) -> bool {
|
||||||
.unwrap_or_else(|_| "manual".to_string())
|
let config = crate::config::get();
|
||||||
|
config.protected_nodes.iter().any(|k| k == key)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Store {
|
impl Store {
|
||||||
|
|
@ -51,15 +52,13 @@ impl Store {
|
||||||
|
|
||||||
/// Upsert a node: update if exists (and content changed), create if not.
|
/// Upsert a node: update if exists (and content changed), create if not.
|
||||||
/// Returns: "created", "updated", or "unchanged".
|
/// Returns: "created", "updated", or "unchanged".
|
||||||
///
|
/// Uses "manual" as the provenance (for CLI operations).
|
||||||
/// Provenance is determined by the POC_PROVENANCE env var if set,
|
|
||||||
/// otherwise defaults to Manual.
|
|
||||||
pub fn upsert(&self, key: &str, content: &str) -> Result<&'static str> {
|
pub fn upsert(&self, key: &str, content: &str) -> Result<&'static str> {
|
||||||
let prov = current_provenance();
|
self.upsert_provenance(key, content, "manual")
|
||||||
self.upsert_provenance(key, content, &prov)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upsert with explicit provenance (for agent-created nodes).
|
/// Upsert with explicit provenance (for agent-created nodes).
|
||||||
|
/// Updates to protected nodes are blocked.
|
||||||
pub fn upsert_provenance(&self, key: &str, content: &str, provenance: &str) -> Result<&'static str> {
|
pub fn upsert_provenance(&self, key: &str, content: &str, provenance: &str) -> Result<&'static str> {
|
||||||
let db = self.db.as_ref().ok_or_else(|| anyhow!("store not loaded"))?;
|
let db = self.db.as_ref().ok_or_else(|| anyhow!("store not loaded"))?;
|
||||||
|
|
||||||
|
|
@ -67,6 +66,9 @@ impl Store {
|
||||||
if existing.content == content {
|
if existing.content == content {
|
||||||
return Ok("unchanged");
|
return Ok("unchanged");
|
||||||
}
|
}
|
||||||
|
if is_protected(key) {
|
||||||
|
bail!("Cannot modify protected node '{}' (in config protected_nodes)", key);
|
||||||
|
}
|
||||||
let mut node = existing;
|
let mut node = existing;
|
||||||
node.content = content.to_string();
|
node.content = content.to_string();
|
||||||
node.provenance = provenance.to_string();
|
node.provenance = provenance.to_string();
|
||||||
|
|
@ -78,7 +80,18 @@ impl Store {
|
||||||
txn.commit()?;
|
txn.commit()?;
|
||||||
Ok("updated")
|
Ok("updated")
|
||||||
} else {
|
} else {
|
||||||
let mut node = new_node(key, content);
|
// Check if there's a previous (possibly deleted) version to continue from
|
||||||
|
let mut node = if let Some(prev) = self.find_latest_by_key(key)? {
|
||||||
|
// Continue from previous version (maintains UUID and version continuity)
|
||||||
|
let mut n = prev;
|
||||||
|
n.content = content.to_string();
|
||||||
|
n.deleted = false;
|
||||||
|
n.timestamp = now_epoch();
|
||||||
|
n.version += 1;
|
||||||
|
n
|
||||||
|
} else {
|
||||||
|
new_node(key, content)
|
||||||
|
};
|
||||||
node.provenance = provenance.to_string();
|
node.provenance = provenance.to_string();
|
||||||
let txn = db.begin_write()?;
|
let txn = db.begin_write()?;
|
||||||
let offset = self.append_nodes(std::slice::from_ref(&node))?;
|
let offset = self.append_nodes(std::slice::from_ref(&node))?;
|
||||||
|
|
@ -89,8 +102,11 @@ impl Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Soft-delete a node (appends deleted version, removes from index).
|
/// Soft-delete a node (appends deleted version, removes from index).
|
||||||
pub fn delete_node(&self, key: &str) -> Result<()> {
|
/// Fails if node is in protected_nodes list.
|
||||||
let prov = current_provenance();
|
pub fn delete_node(&self, key: &str, provenance: &str) -> Result<()> {
|
||||||
|
if is_protected(key) {
|
||||||
|
bail!("Cannot delete protected node '{}' (in config protected_nodes)", key);
|
||||||
|
}
|
||||||
let db = self.db.as_ref().ok_or_else(|| anyhow!("store not loaded"))?;
|
let db = self.db.as_ref().ok_or_else(|| anyhow!("store not loaded"))?;
|
||||||
|
|
||||||
let node = self.get_node(key)?
|
let node = self.get_node(key)?
|
||||||
|
|
@ -98,7 +114,7 @@ impl Store {
|
||||||
let mut deleted = node;
|
let mut deleted = node;
|
||||||
deleted.deleted = true;
|
deleted.deleted = true;
|
||||||
deleted.version += 1;
|
deleted.version += 1;
|
||||||
deleted.provenance = prov;
|
deleted.provenance = provenance.to_string();
|
||||||
deleted.timestamp = now_epoch();
|
deleted.timestamp = now_epoch();
|
||||||
|
|
||||||
let txn = db.begin_write()?;
|
let txn = db.begin_write()?;
|
||||||
|
|
@ -108,15 +124,52 @@ impl Store {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restore a deleted node to its last non-deleted state.
|
||||||
|
/// Returns the restored node's content preview.
|
||||||
|
pub fn restore_node(&self, key: &str, provenance: &str) -> Result<String> {
|
||||||
|
let db = self.db.as_ref().ok_or_else(|| anyhow!("store not loaded"))?;
|
||||||
|
|
||||||
|
// Check if node already exists (not deleted)
|
||||||
|
if self.contains_key(key)? {
|
||||||
|
bail!("Node '{}' is not deleted", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the last non-deleted version (for content)
|
||||||
|
let last_live = self.find_last_live_version(key)?
|
||||||
|
.ok_or_else(|| anyhow!("No previous version of '{}' found", key))?;
|
||||||
|
|
||||||
|
// Find the absolute latest version (for version number continuity)
|
||||||
|
let latest = self.find_latest_by_key(key)?
|
||||||
|
.ok_or_else(|| anyhow!("No previous version of '{}' found", key))?;
|
||||||
|
|
||||||
|
// Create restored version: content from last_live, version from latest + 1
|
||||||
|
let mut restored = last_live.clone();
|
||||||
|
restored.deleted = false;
|
||||||
|
restored.version = latest.version + 1;
|
||||||
|
restored.timestamp = now_epoch();
|
||||||
|
restored.provenance = provenance.to_string();
|
||||||
|
|
||||||
|
let txn = db.begin_write()?;
|
||||||
|
let offset = self.append_nodes(std::slice::from_ref(&restored))?;
|
||||||
|
index::index_node(&txn, &restored.key, offset, &restored.uuid, restored.node_type as u8, restored.timestamp, &restored.provenance)?;
|
||||||
|
txn.commit()?;
|
||||||
|
|
||||||
|
let preview: String = restored.content.chars().take(100).collect();
|
||||||
|
Ok(format!("Restored '{}' (v{}): {}...", key, restored.version, preview))
|
||||||
|
}
|
||||||
|
|
||||||
/// Rename a node: change its key, update debug strings on all edges.
|
/// Rename a node: change its key, update debug strings on all edges.
|
||||||
///
|
///
|
||||||
/// Graph edges (source/target UUIDs) are unaffected — they're already
|
/// Graph edges (source/target UUIDs) are unaffected — they're already
|
||||||
/// UUID-based. We update the human-readable source_key/target_key strings
|
/// UUID-based. We update the human-readable source_key/target_key strings
|
||||||
/// on relations, and created_at is preserved untouched.
|
/// on relations, and created_at is preserved untouched.
|
||||||
pub fn rename_node(&self, old_key: &str, new_key: &str) -> Result<()> {
|
pub fn rename_node(&self, old_key: &str, new_key: &str, provenance: &str) -> Result<()> {
|
||||||
if old_key == new_key {
|
if old_key == new_key {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
if is_protected(old_key) {
|
||||||
|
bail!("Cannot rename protected node '{}' (in config protected_nodes)", old_key);
|
||||||
|
}
|
||||||
if self.contains_key(new_key)? {
|
if self.contains_key(new_key)? {
|
||||||
bail!("Key '{}' already exists", new_key);
|
bail!("Key '{}' already exists", new_key);
|
||||||
}
|
}
|
||||||
|
|
@ -124,20 +177,18 @@ impl Store {
|
||||||
let node = self.get_node(old_key)?
|
let node = self.get_node(old_key)?
|
||||||
.ok_or_else(|| anyhow!("No node '{}'", old_key))?;
|
.ok_or_else(|| anyhow!("No node '{}'", old_key))?;
|
||||||
|
|
||||||
let prov = current_provenance();
|
|
||||||
|
|
||||||
// New version under the new key
|
// New version under the new key
|
||||||
let mut renamed = node.clone();
|
let mut renamed = node.clone();
|
||||||
renamed.key = new_key.to_string();
|
renamed.key = new_key.to_string();
|
||||||
renamed.version += 1;
|
renamed.version += 1;
|
||||||
renamed.provenance = prov.clone();
|
renamed.provenance = provenance.to_string();
|
||||||
renamed.timestamp = now_epoch();
|
renamed.timestamp = now_epoch();
|
||||||
|
|
||||||
// Deletion record for the old key (same UUID, independent version counter)
|
// Deletion record for the old key (same UUID, independent version counter)
|
||||||
let mut tombstone = node.clone();
|
let mut tombstone = node.clone();
|
||||||
tombstone.deleted = true;
|
tombstone.deleted = true;
|
||||||
tombstone.version += 1;
|
tombstone.version += 1;
|
||||||
tombstone.provenance = prov;
|
tombstone.provenance = provenance.to_string();
|
||||||
tombstone.timestamp = now_epoch();
|
tombstone.timestamp = now_epoch();
|
||||||
|
|
||||||
// Find relations touching this node's UUID (read before txn)
|
// Find relations touching this node's UUID (read before txn)
|
||||||
|
|
@ -164,7 +215,7 @@ impl Store {
|
||||||
};
|
};
|
||||||
let mut rel = new_relation(src_uuid, tgt_uuid,
|
let mut rel = new_relation(src_uuid, tgt_uuid,
|
||||||
RelationType::from_u8(rel_type), strength,
|
RelationType::from_u8(rel_type), strength,
|
||||||
&src_key, &tgt_key);
|
&src_key, &tgt_key, provenance);
|
||||||
rel.version = 2; // indicate update
|
rel.version = 2; // indicate update
|
||||||
updated_rels.push(rel);
|
updated_rels.push(rel);
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +329,7 @@ impl Store {
|
||||||
index::remove_relation(&txn, &source_uuid, &target_uuid, strength, rel_type)?;
|
index::remove_relation(&txn, &source_uuid, &target_uuid, strength, rel_type)?;
|
||||||
let mut rel = new_relation(source_uuid, target_uuid,
|
let mut rel = new_relation(source_uuid, target_uuid,
|
||||||
RelationType::from_u8(rel_type), strength,
|
RelationType::from_u8(rel_type), strength,
|
||||||
&source_key, &target_key);
|
&source_key, &target_key, "system");
|
||||||
rel.deleted = true;
|
rel.deleted = true;
|
||||||
rel.version = 2;
|
rel.version = 2;
|
||||||
self.append_relations(std::slice::from_ref(&rel))?;
|
self.append_relations(std::slice::from_ref(&rel))?;
|
||||||
|
|
@ -311,7 +362,7 @@ impl Store {
|
||||||
|
|
||||||
/// Set the strength of a link between two nodes.
|
/// Set the strength of a link between two nodes.
|
||||||
/// Returns the old strength. Creates link if it doesn't exist.
|
/// Returns the old strength. Creates link if it doesn't exist.
|
||||||
pub fn set_link_strength(&self, source: &str, target: &str, strength: f32) -> Result<f32> {
|
pub fn set_link_strength(&self, source: &str, target: &str, strength: f32, provenance: &str) -> Result<f32> {
|
||||||
let strength = strength.clamp(0.01, 1.0);
|
let strength = strength.clamp(0.01, 1.0);
|
||||||
|
|
||||||
let source_uuid = self.get_node(source)?
|
let source_uuid = self.get_node(source)?
|
||||||
|
|
@ -337,14 +388,14 @@ impl Store {
|
||||||
index::index_relation(&txn, &source_uuid, &target_uuid, strength, rel_type)?;
|
index::index_relation(&txn, &source_uuid, &target_uuid, strength, rel_type)?;
|
||||||
// Append updated relation to log
|
// Append updated relation to log
|
||||||
let mut rel = new_relation(source_uuid, target_uuid,
|
let mut rel = new_relation(source_uuid, target_uuid,
|
||||||
RelationType::from_u8(rel_type), strength, source, target);
|
RelationType::from_u8(rel_type), strength, source, target, provenance);
|
||||||
rel.version = 2; // indicate update
|
rel.version = 2; // indicate update
|
||||||
self.append_relations(std::slice::from_ref(&rel))?;
|
self.append_relations(std::slice::from_ref(&rel))?;
|
||||||
txn.commit()?;
|
txn.commit()?;
|
||||||
Ok(old_strength)
|
Ok(old_strength)
|
||||||
} else {
|
} else {
|
||||||
// Create new link then update its strength
|
// Create new link then update its strength
|
||||||
self.add_link(source, target, "link_set")?;
|
self.add_link(source, target, provenance)?;
|
||||||
let db = self.db.as_ref().ok_or_else(|| anyhow!("store not loaded"))?;
|
let db = self.db.as_ref().ok_or_else(|| anyhow!("store not loaded"))?;
|
||||||
let txn = db.begin_write()?;
|
let txn = db.begin_write()?;
|
||||||
index::remove_relation(&txn, &source_uuid, &target_uuid, 0.1, RelationType::Link as u8)?;
|
index::remove_relation(&txn, &source_uuid, &target_uuid, 0.1, RelationType::Link as u8)?;
|
||||||
|
|
@ -377,12 +428,11 @@ impl Store {
|
||||||
let jaccard = graph.jaccard(source, target);
|
let jaccard = graph.jaccard(source, target);
|
||||||
let strength = (jaccard * 3.0).clamp(0.1, 1.0) as f32;
|
let strength = (jaccard * 3.0).clamp(0.1, 1.0) as f32;
|
||||||
|
|
||||||
let mut rel = new_relation(
|
let rel = new_relation(
|
||||||
source_uuid, target_uuid,
|
source_uuid, target_uuid,
|
||||||
RelationType::Link, strength,
|
RelationType::Link, strength,
|
||||||
source, target,
|
source, target, provenance,
|
||||||
);
|
);
|
||||||
rel.provenance = provenance.to_string();
|
|
||||||
self.add_relation(rel)?;
|
self.add_relation(rel)?;
|
||||||
Ok(strength)
|
Ok(strength)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue