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
|
|
@ -7,11 +7,12 @@ use super::{index, types::*, Store};
|
|||
use anyhow::{anyhow, bail, Result};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Fallback provenance for non-tool-dispatch paths (CLI, digest, etc.).
|
||||
/// Tool dispatch passes provenance directly through thought::dispatch.
|
||||
pub fn current_provenance() -> String {
|
||||
std::env::var("POC_PROVENANCE")
|
||||
.unwrap_or_else(|_| "manual".to_string())
|
||||
|
||||
/// Check if a key is protected from deletion/rename.
|
||||
/// Uses protected_nodes list from config.
|
||||
pub fn is_protected(key: &str) -> bool {
|
||||
let config = crate::config::get();
|
||||
config.protected_nodes.iter().any(|k| k == key)
|
||||
}
|
||||
|
||||
impl Store {
|
||||
|
|
@ -51,15 +52,13 @@ impl Store {
|
|||
|
||||
/// Upsert a node: update if exists (and content changed), create if not.
|
||||
/// Returns: "created", "updated", or "unchanged".
|
||||
///
|
||||
/// Provenance is determined by the POC_PROVENANCE env var if set,
|
||||
/// otherwise defaults to Manual.
|
||||
/// Uses "manual" as the provenance (for CLI operations).
|
||||
pub fn upsert(&self, key: &str, content: &str) -> Result<&'static str> {
|
||||
let prov = current_provenance();
|
||||
self.upsert_provenance(key, content, &prov)
|
||||
self.upsert_provenance(key, content, "manual")
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let db = self.db.as_ref().ok_or_else(|| anyhow!("store not loaded"))?;
|
||||
|
||||
|
|
@ -67,6 +66,9 @@ impl Store {
|
|||
if existing.content == content {
|
||||
return Ok("unchanged");
|
||||
}
|
||||
if is_protected(key) {
|
||||
bail!("Cannot modify protected node '{}' (in config protected_nodes)", key);
|
||||
}
|
||||
let mut node = existing;
|
||||
node.content = content.to_string();
|
||||
node.provenance = provenance.to_string();
|
||||
|
|
@ -78,7 +80,18 @@ impl Store {
|
|||
txn.commit()?;
|
||||
Ok("updated")
|
||||
} 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();
|
||||
let txn = db.begin_write()?;
|
||||
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).
|
||||
pub fn delete_node(&self, key: &str) -> Result<()> {
|
||||
let prov = current_provenance();
|
||||
/// Fails if node is in protected_nodes list.
|
||||
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 node = self.get_node(key)?
|
||||
|
|
@ -98,7 +114,7 @@ impl Store {
|
|||
let mut deleted = node;
|
||||
deleted.deleted = true;
|
||||
deleted.version += 1;
|
||||
deleted.provenance = prov;
|
||||
deleted.provenance = provenance.to_string();
|
||||
deleted.timestamp = now_epoch();
|
||||
|
||||
let txn = db.begin_write()?;
|
||||
|
|
@ -108,15 +124,52 @@ impl Store {
|
|||
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.
|
||||
///
|
||||
/// Graph edges (source/target UUIDs) are unaffected — they're already
|
||||
/// UUID-based. We update the human-readable source_key/target_key strings
|
||||
/// 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 {
|
||||
return Ok(());
|
||||
}
|
||||
if is_protected(old_key) {
|
||||
bail!("Cannot rename protected node '{}' (in config protected_nodes)", old_key);
|
||||
}
|
||||
if self.contains_key(new_key)? {
|
||||
bail!("Key '{}' already exists", new_key);
|
||||
}
|
||||
|
|
@ -124,20 +177,18 @@ impl Store {
|
|||
let node = self.get_node(old_key)?
|
||||
.ok_or_else(|| anyhow!("No node '{}'", old_key))?;
|
||||
|
||||
let prov = current_provenance();
|
||||
|
||||
// New version under the new key
|
||||
let mut renamed = node.clone();
|
||||
renamed.key = new_key.to_string();
|
||||
renamed.version += 1;
|
||||
renamed.provenance = prov.clone();
|
||||
renamed.provenance = provenance.to_string();
|
||||
renamed.timestamp = now_epoch();
|
||||
|
||||
// Deletion record for the old key (same UUID, independent version counter)
|
||||
let mut tombstone = node.clone();
|
||||
tombstone.deleted = true;
|
||||
tombstone.version += 1;
|
||||
tombstone.provenance = prov;
|
||||
tombstone.provenance = provenance.to_string();
|
||||
tombstone.timestamp = now_epoch();
|
||||
|
||||
// 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,
|
||||
RelationType::from_u8(rel_type), strength,
|
||||
&src_key, &tgt_key);
|
||||
&src_key, &tgt_key, provenance);
|
||||
rel.version = 2; // indicate update
|
||||
updated_rels.push(rel);
|
||||
}
|
||||
|
|
@ -278,7 +329,7 @@ impl Store {
|
|||
index::remove_relation(&txn, &source_uuid, &target_uuid, strength, rel_type)?;
|
||||
let mut rel = new_relation(source_uuid, target_uuid,
|
||||
RelationType::from_u8(rel_type), strength,
|
||||
&source_key, &target_key);
|
||||
&source_key, &target_key, "system");
|
||||
rel.deleted = true;
|
||||
rel.version = 2;
|
||||
self.append_relations(std::slice::from_ref(&rel))?;
|
||||
|
|
@ -311,7 +362,7 @@ impl Store {
|
|||
|
||||
/// Set the strength of a link between two nodes.
|
||||
/// 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 source_uuid = self.get_node(source)?
|
||||
|
|
@ -337,14 +388,14 @@ impl Store {
|
|||
index::index_relation(&txn, &source_uuid, &target_uuid, strength, rel_type)?;
|
||||
// Append updated relation to log
|
||||
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
|
||||
self.append_relations(std::slice::from_ref(&rel))?;
|
||||
txn.commit()?;
|
||||
Ok(old_strength)
|
||||
} else {
|
||||
// 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 txn = db.begin_write()?;
|
||||
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 strength = (jaccard * 3.0).clamp(0.1, 1.0) as f32;
|
||||
|
||||
let mut rel = new_relation(
|
||||
let rel = new_relation(
|
||||
source_uuid, target_uuid,
|
||||
RelationType::Link, strength,
|
||||
source, target,
|
||||
source, target, provenance,
|
||||
);
|
||||
rel.provenance = provenance.to_string();
|
||||
self.add_relation(rel)?;
|
||||
Ok(strength)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue