From 19e181665d901aa1c5fe739b7d0bace3117a5f88 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Tue, 17 Mar 2026 01:39:41 -0400 Subject: [PATCH] Add calibrate agent, link-set command, and dominating-set query stage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit calibrate.agent: Haiku-based agent that reads a node and all its neighbors, then assigns appropriate link strengths relative to each other. Designed for high-volume runs across the whole graph. graph link-set: Set strength of an existing link (0.0-1.0). dominating-set query stage: Greedy 3-covering dominating set — finds the minimum set of nodes such that every node in the input is within 1 hop of at least 3 selected nodes. Use with calibrate agent to ensure every link gets assessed from multiple perspectives. Usage: poc-memory query "content ~ 'bcachefs' | dominating-set" --- poc-memory/agents/calibrate.agent | 56 +++++++++++++++++++++++++++++ poc-memory/src/cli/graph.rs | 29 +++++++++++++++ poc-memory/src/main.rs | 12 +++++++ poc-memory/src/query/engine.rs | 59 ++++++++++++++++++++++++++++++- poc-memory/src/query/parser.rs | 11 ++++++ 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 poc-memory/agents/calibrate.agent diff --git a/poc-memory/agents/calibrate.agent b/poc-memory/agents/calibrate.agent new file mode 100644 index 0000000..055bc92 --- /dev/null +++ b/poc-memory/agents/calibrate.agent @@ -0,0 +1,56 @@ +{"agent":"calibrate","query":"all | not-visited:calibrate,7d | sort:degree desc | limit:1","model":"haiku","schedule":"daily","tools":["Bash(poc-memory:*)"]} + +# Calibrate Agent — Link Strength Assessment + +{{node:core-personality}} + +{{node:memory-instructions-core}} + +You calibrate link strengths in the knowledge graph. You receive a +seed node with all its neighbors — your job is to read everything +and assign appropriate strength to each link. + +## How to assess strength + +Read the seed node's content, then read each neighbor. For each link, +judge how strongly related they actually are: + +- **0.8–1.0** — core relationship. One defines or is essential to the other. + Parent-child, same concept different depth, direct dependency. +- **0.5–0.7** — strong relationship. Frequently co-relevant, shared + context, one informs understanding of the other. +- **0.2–0.4** — moderate relationship. Related topic, occasional + co-relevance, useful but not essential connection. +- **0.05–0.15** — weak relationship. Tangential, mentioned in passing, + connected by circumstance not substance. + +## How to work + +For the seed node, read it and all its neighbors. Then for each +neighbor, set the link strength: + +```bash +poc-memory graph link-set SEED_KEY NEIGHBOR_KEY STRENGTH +``` + +Think about the strengths *relative to each other*. If node A has +10 neighbors, they can't all be 0.8 — rank them and spread the +strengths accordingly. + +## Guidelines + +- **Read before judging.** Don't guess from key names alone. +- **Calibrate relatively.** The strongest link from this node should + be stronger than the weakest. Use the full range. +- **Journal→topic links are usually weak (0.1–0.3).** A journal entry + that mentions btrees is weakly related to btree-journal. +- **Topic→subtopic links are strong (0.6–0.9).** btree-journal and + btree-journal-txn-restart are tightly related. +- **Hub→leaf links vary.** bcachefs→kernel-patterns is moderate (0.4), + bcachefs→some-random-journal is weak (0.1). +- **Don't remove links.** Only adjust strength. If a link shouldn't + exist at all, set it to 0.05. + +## Seed node + +{{organize}} diff --git a/poc-memory/src/cli/graph.rs b/poc-memory/src/cli/graph.rs index dba8dce..17190fa 100644 --- a/poc-memory/src/cli/graph.rs +++ b/poc-memory/src/cli/graph.rs @@ -178,6 +178,35 @@ pub fn cmd_link_add(source: &str, target: &str, reason: &[String]) -> Result<(), Ok(()) } +pub fn cmd_link_set(source: &str, target: &str, strength: f32) -> Result<(), String> { + super::check_dry_run(); + let mut store = store::Store::load()?; + let source = store.resolve_key(source)?; + let target = store.resolve_key(target)?; + let strength = strength.clamp(0.01, 1.0); + + let mut found = false; + for rel in &mut store.relations { + if rel.deleted { continue; } + if (rel.source_key == source && rel.target_key == target) + || (rel.source_key == target && rel.target_key == source) + { + let old = rel.strength; + rel.strength = strength; + println!("Set: {} ↔ {} strength {:.2} → {:.2}", source, target, old, strength); + found = true; + break; + } + } + + if !found { + return Err(format!("No link found between {} and {}", source, target)); + } + + store.save()?; + Ok(()) +} + pub fn cmd_link_impact(source: &str, target: &str) -> Result<(), String> { let store = store::Store::load()?; let source = store.resolve_key(source)?; diff --git a/poc-memory/src/main.rs b/poc-memory/src/main.rs index eccf3c4..c2f4202 100644 --- a/poc-memory/src/main.rs +++ b/poc-memory/src/main.rs @@ -299,6 +299,16 @@ enum GraphCmd { /// Optional reason reason: Vec, }, + /// Set strength of an existing link + #[command(name = "link-set")] + LinkSet { + /// Source node key + source: String, + /// Target node key + target: String, + /// Strength (0.0–1.0) + strength: f32, + }, /// Simulate adding an edge, report topology impact #[command(name = "link-impact")] LinkImpact { @@ -775,6 +785,8 @@ fn main() { GraphCmd::Link { key } => cli::graph::cmd_link(&key), GraphCmd::LinkAdd { source, target, reason } => cli::graph::cmd_link_add(&source, &target, &reason), + GraphCmd::LinkSet { source, target, strength } + => cli::graph::cmd_link_set(&source, &target, strength), GraphCmd::LinkImpact { source, target } => cli::graph::cmd_link_impact(&source, &target), GraphCmd::LinkAudit { apply } => cli::graph::cmd_link_audit(apply), diff --git a/poc-memory/src/query/engine.rs b/poc-memory/src/query/engine.rs index 5f3f498..f70564b 100644 --- a/poc-memory/src/query/engine.rs +++ b/poc-memory/src/query/engine.rs @@ -157,6 +157,7 @@ pub enum Filter { pub enum Transform { Sort(SortField), Limit(usize), + DominatingSet, } #[derive(Clone, Debug)] @@ -257,6 +258,11 @@ impl Stage { return Ok(Stage::Generator(Generator::All)); } + // Transform: "dominating-set" + if s == "dominating-set" { + return Ok(Stage::Transform(Transform::DominatingSet)); + } + // Try algorithm parse first (bare words, no colon) if !s.contains(':') { if let Ok(algo) = AlgoStage::parse(s) { @@ -348,6 +354,7 @@ impl fmt::Display for Stage { Stage::Filter(filt) => write!(f, "{}", filt), Stage::Transform(Transform::Sort(field)) => write!(f, "sort:{:?}", field), Stage::Transform(Transform::Limit(n)) => write!(f, "limit:{}", n), + Stage::Transform(Transform::DominatingSet) => write!(f, "dominating-set"), Stage::Algorithm(a) => write!(f, "{}", a.algo), } } @@ -508,7 +515,7 @@ fn eval_filter(filt: &Filter, key: &str, store: &Store, now: i64) -> bool { } } -fn run_transform( +pub fn run_transform( xform: &Transform, mut items: Vec<(String, f64)>, store: &Store, @@ -564,6 +571,56 @@ fn run_transform( items.truncate(*n); items } + Transform::DominatingSet => { + // Greedy 3-covering dominating set: pick the node that covers + // the most under-covered neighbors, repeat until every node + // has been covered 3 times (by 3 different selected seeds). + use std::collections::HashMap as HMap; + let input_keys: std::collections::HashSet = items.iter().map(|(k, _)| k.clone()).collect(); + let mut cover_count: HMap = items.iter().map(|(k, _)| (k.clone(), 0)).collect(); + let mut selected: Vec<(String, f64)> = Vec::new(); + let mut selected_set: std::collections::HashSet = std::collections::HashSet::new(); + const REQUIRED_COVERAGE: usize = 3; + + loop { + // Find the unselected node that covers the most under-covered nodes + let best = items.iter() + .filter(|(k, _)| !selected_set.contains(k.as_str())) + .map(|(k, _)| { + let mut value = 0usize; + // Count self if under-covered + if cover_count.get(k).copied().unwrap_or(0) < REQUIRED_COVERAGE { + value += 1; + } + for (nbr, _) in graph.neighbors(k) { + if input_keys.contains(nbr.as_str()) { + if cover_count.get(nbr.as_str()).copied().unwrap_or(0) < REQUIRED_COVERAGE { + value += 1; + } + } + } + (k.clone(), value) + }) + .max_by_key(|(_, v)| *v); + + let Some((key, value)) = best else { break }; + if value == 0 { break; } // everything covered 3x + + // Mark coverage + *cover_count.entry(key.clone()).or_default() += 1; + for (nbr, _) in graph.neighbors(&key) { + if let Some(c) = cover_count.get_mut(nbr.as_str()) { + *c += 1; + } + } + + let score = items.iter().find(|(k, _)| k == &key).map(|(_, s)| *s).unwrap_or(1.0); + selected.push((key.clone(), score)); + selected_set.insert(key); + } + + selected + } } } diff --git a/poc-memory/src/query/parser.rs b/poc-memory/src/query/parser.rs index 5c5c112..a07adb8 100644 --- a/poc-memory/src/query/parser.rs +++ b/poc-memory/src/query/parser.rs @@ -64,6 +64,7 @@ pub enum Stage { Select(Vec), Count, Connectivity, + DominatingSet, } #[derive(Debug, Clone)] @@ -90,6 +91,7 @@ peg::parser! { / "select" _ f:field_list() { Stage::Select(f) } / "count" { Stage::Count } / "connectivity" { Stage::Connectivity } + / "dominating-set" { Stage::DominatingSet } rule asc_desc() -> bool = "asc" { true } @@ -425,6 +427,15 @@ fn execute_parsed( } Stage::Connectivity => {} // handled in output Stage::Select(_) | Stage::Count => {} // handled in output + Stage::DominatingSet => { + let mut items: Vec<(String, f64)> = results.iter() + .map(|r| (r.key.clone(), graph.degree(&r.key) as f64)) + .collect(); + let xform = super::engine::Transform::DominatingSet; + items = super::engine::run_transform(&xform, items, store, &graph); + let keep: std::collections::HashSet = items.into_iter().map(|(k, _)| k).collect(); + results.retain(|r| keep.contains(&r.key)); + } } }