Add calibrate agent, link-set command, and dominating-set query stage

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"
This commit is contained in:
ProofOfConcept 2026-03-17 01:39:41 -04:00
parent 7fc1270d6f
commit 19e181665d
5 changed files with 166 additions and 1 deletions

View file

@ -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)?;

View file

@ -299,6 +299,16 @@ enum GraphCmd {
/// Optional reason
reason: Vec<String>,
},
/// Set strength of an existing link
#[command(name = "link-set")]
LinkSet {
/// Source node key
source: String,
/// Target node key
target: String,
/// Strength (0.01.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),

View file

@ -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<String> = items.iter().map(|(k, _)| k.clone()).collect();
let mut cover_count: HMap<String, usize> = items.iter().map(|(k, _)| (k.clone(), 0)).collect();
let mut selected: Vec<(String, f64)> = Vec::new();
let mut selected_set: std::collections::HashSet<String> = 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
}
}
}

View file

@ -64,6 +64,7 @@ pub enum Stage {
Select(Vec<String>),
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<String> = items.into_iter().map(|(k, _)| k).collect();
results.retain(|r| keep.contains(&r.key));
}
}
}