use std::collections::BTreeMap;
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
use crate::fusion::sort_and_truncate;
pub(crate) const SUPPORT_FULL: u32 = 3;
pub(crate) const USAGE_WEIGHT: f32 = 0.5;
pub(crate) const TAU_COSINE: f32 = 0.70;
pub(crate) const TAU_LEXICAL: f32 = 0.5;
const MAX_TERMS: usize = 5;
const MS_PER_DAY: f64 = 86_400_000.0;
const RECENCY_GRACE_DAYS: f64 = 90.0;
const RECENCY_HALF_LIFE_DAYS: f64 = 90.0;
const EVICTION_FLOOR: f32 = 0.01;
const MEMBER_CAP: usize = 50;
fn recency_factor(now_ts: u64, last_ts: u64) -> f32 {
let dt_days = now_ts.saturating_sub(last_ts) as f64 / MS_PER_DAY;
if dt_days <= RECENCY_GRACE_DAYS {
return 1.0;
}
2f64.powf(-(dt_days - RECENCY_GRACE_DAYS) / RECENCY_HALF_LIFE_DAYS) as f32
}
pub(crate) fn usage_weight(support: u32) -> f32 {
let ramp = (support as f32 / SUPPORT_FULL as f32).min(1.0);
USAGE_WEIGHT * ramp
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Capability {
Tool,
Skill,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct UsageArm {
pub intent_id: String,
pub similarity: f32,
pub support: u32,
pub weight: f32,
pub ids: Vec<String>,
}
impl UsageArm {
pub(crate) fn weight(&self) -> f32 {
self.weight
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IntentGraphError {
Malformed(String),
UnsupportedVersion(u32),
}
impl std::fmt::Display for IntentGraphError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IntentGraphError::Malformed(e) => write!(f, "malformed intent graph: {e}"),
IntentGraphError::UnsupportedVersion(v) => {
write!(
f,
"unsupported intent graph version {v} (this build reads 1)"
)
}
}
}
}
impl std::error::Error for IntentGraphError {}
const GRAPH_VERSION: u32 = 1;
#[derive(Debug, Default)]
struct PendingQuery(Mutex<Option<(String, Vec<f32>, String)>>);
impl Clone for PendingQuery {
fn clone(&self) -> Self {
Self::default()
}
}
impl PartialEq for PendingQuery {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl PendingQuery {
fn set(&self, query: &str, vector: &[f32], fingerprint: &str) {
if let Ok(mut slot) = self.0.lock() {
*slot = Some((query.to_string(), vector.to_vec(), fingerprint.to_string()));
}
}
fn vector_for(&self, query: &str) -> Option<(Vec<f32>, String)> {
let slot = self.0.lock().ok()?;
match slot.as_ref() {
Some((q, v, fp)) if q == query => Some((v.clone(), fp.clone())),
_ => None,
}
}
}
#[derive(Debug, Default)]
struct CreditSlot(Mutex<Option<(String, bool)>>);
impl Clone for CreditSlot {
fn clone(&self) -> Self {
Self::default()
}
}
impl PartialEq for CreditSlot {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl CreditSlot {
fn arm(&self, query: &str) {
if let Ok(mut slot) = self.0.lock() {
*slot = Some((query.to_string(), false));
}
}
fn claim(&self, query: &str) -> bool {
let Ok(mut slot) = self.0.lock() else {
return false;
};
match slot.as_mut() {
Some((q, credited)) if q == query && !*credited => {
*credited = true;
true
}
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Intent {
pub id: String,
pub label: String,
#[serde(default)]
pub terms: Vec<String>,
pub members: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub centroid: Option<Vec<f32>>,
pub support: u32,
#[serde(default)]
pub last_ts: u64,
#[serde(default)]
pub tools: BTreeMap<String, f32>,
#[serde(default)]
pub skills: BTreeMap<String, f32>,
#[serde(skip)]
member_bags: Vec<std::collections::HashSet<String>>,
#[serde(skip)]
bag: std::collections::HashSet<String>,
#[serde(skip)]
vector_n: u32,
}
impl PartialEq for Intent {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.members == other.members
&& self.centroid == other.centroid
&& self.support == other.support
&& self.tools == other.tools
&& self.skills == other.skills
}
}
impl Intent {
fn edges(&self, kind: Capability) -> &BTreeMap<String, f32> {
match kind {
Capability::Tool => &self.tools,
Capability::Skill => &self.skills,
}
}
fn ranked(&self, kind: Capability, known: &dyn Fn(&str) -> bool) -> Vec<String> {
let mut ranked: Vec<(String, f32)> = self
.edges(kind)
.iter()
.filter(|(id, _)| known(id.as_str()))
.map(|(id, w)| (id.clone(), *w))
.collect();
let len = ranked.len();
sort_and_truncate(&mut ranked, len);
ranked.into_iter().map(|(id, _)| id).collect()
}
fn absorb_vector(&mut self, vector: &[f32]) {
let merged: Vec<f32> = match self.centroid.as_deref() {
Some(c) if c.len() == vector.len() => {
let k = self.vector_n.max(1) as f32;
c.iter().zip(vector).map(|(c, v)| c * k + v).collect()
}
_ => {
self.vector_n = 0;
vector.to_vec()
}
};
self.vector_n = self.vector_n.saturating_add(1);
self.centroid = Some(normalize(merged));
}
fn cap_members(&mut self) {
while self.members.len() > MEMBER_CAP {
self.members.remove(0);
self.member_bags.remove(0);
}
self.bag = self.member_bags.iter().flatten().cloned().collect();
}
fn absorb_tokens(&mut self, member: &str) {
let tokens: std::collections::HashSet<String> = tokenize(member).into_iter().collect();
self.bag.extend(tokens.iter().cloned());
self.member_bags.push(tokens);
}
fn rebuild_bag(&mut self) {
self.member_bags = self
.members
.iter()
.map(|m| tokenize(m).into_iter().collect())
.collect();
self.bag = self.members.iter().flat_map(|m| tokenize(m)).collect();
}
fn lexical_score(&self, q: &std::collections::HashSet<String>) -> f32 {
let needed = (TAU_LEXICAL * (q.len() as f32 + 1.0) / (1.0 + TAU_LEXICAL)).ceil() as usize;
if q.iter().filter(|t| self.bag.contains(*t)).count() < needed {
return 0.0;
}
self.member_bags
.iter()
.map(|m| {
let (lo, hi) = if q.len() < m.len() {
(q.len(), m.len())
} else {
(m.len(), q.len())
};
if hi == 0 || (lo as f32 / hi as f32) < TAU_LEXICAL {
return 0.0;
}
let inter = q.intersection(m).count() as f32;
let union = (q.len() + m.len()) as f32 - inter;
if union == 0.0 { 0.0 } else { inter / union }
})
.fold(0.0f32, f32::max)
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct IntentGraph {
pub v: u32,
pub built_from_ts: u64,
#[serde(default)]
pub rev: u64,
pub intents: Vec<Intent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip)]
pending: PendingQuery,
#[serde(skip)]
credit: CreditSlot,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum GraphModelStatus {
Ok,
DimMismatch { built: usize, active: usize },
ModelMismatch { built: String, active: String },
}
impl GraphModelStatus {
pub(crate) fn describe(&self) -> Option<(String, String, bool)> {
match self {
GraphModelStatus::Ok => None,
GraphModelStatus::DimMismatch { built, active } => {
Some((built.to_string(), active.to_string(), true))
}
GraphModelStatus::ModelMismatch { built, active } => {
Some((built.clone(), active.clone(), false))
}
}
}
}
impl Serialize for IntentGraph {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let len = 4 + usize::from(self.model.is_some());
let mut out = serializer.serialize_struct("IntentGraph", len)?;
out.serialize_field("v", &self.v)?;
out.serialize_field("built_from_ts", &self.built_from_ts)?;
out.serialize_field("rev", &self.rev)?;
if let Some(model) = &self.model {
out.serialize_field("model", model)?;
}
out.serialize_field("intents", &self.labeled())?;
out.end()
}
}
impl Default for IntentGraph {
fn default() -> Self {
Self::empty()
}
}
impl IntentGraph {
pub fn from_json(json: &str) -> Result<Self, IntentGraphError> {
let mut graph: IntentGraph =
serde_json::from_str(json).map_err(|e| IntentGraphError::Malformed(e.to_string()))?;
if graph.v != GRAPH_VERSION {
return Err(IntentGraphError::UnsupportedVersion(graph.v));
}
graph.validate()?;
let anchor = graph.built_from_ts;
for it in &mut graph.intents {
if it.last_ts == 0 {
it.last_ts = anchor;
}
}
graph.rebuild_caches();
Ok(graph)
}
fn validate(&self) -> Result<(), IntentGraphError> {
let mut seen = std::collections::HashSet::with_capacity(self.intents.len());
for it in &self.intents {
if !seen.insert(it.id.as_str()) {
return Err(IntentGraphError::Malformed(format!(
"duplicate intent id {:?}",
it.id
)));
}
if it.members.is_empty() {
return Err(IntentGraphError::Malformed(format!(
"intent {:?} has no members",
it.id
)));
}
if it.support < 1 {
return Err(IntentGraphError::Malformed(format!(
"intent {:?} has support 0 (a confirmed cluster is at least 1)",
it.id
)));
}
if it.centroid.as_ref().is_some_and(|c| c.is_empty()) {
return Err(IntentGraphError::Malformed(format!(
"intent {:?} has an empty centroid",
it.id
)));
}
if it
.tools
.values()
.chain(it.skills.values())
.any(|w| *w <= 0.0)
{
return Err(IntentGraphError::Malformed(format!(
"intent {:?} has a non-positive edge weight",
it.id
)));
}
}
Ok(())
}
fn rebuild_caches(&mut self) {
for it in &mut self.intents {
it.rebuild_bag();
}
}
pub fn empty() -> Self {
Self {
v: GRAPH_VERSION,
built_from_ts: 0,
rev: 0,
intents: Vec::new(),
model: None,
pending: PendingQuery::default(),
credit: CreditSlot::default(),
}
}
pub fn len(&self) -> usize {
self.intents.len()
}
pub fn is_empty(&self) -> bool {
self.intents.is_empty()
}
pub(crate) fn note_query_vector(&self, query: &str, vector: &[f32], fingerprint: &str) {
self.pending.set(query, vector, fingerprint);
}
pub(crate) fn arm_credit(&self, query: &str) {
self.credit.arm(query);
}
pub(crate) fn claim_credit(&self, query: &str) -> bool {
self.credit.claim(query)
}
pub(crate) fn observe(
&mut self,
query: &str,
kind: Capability,
capability_id: &str,
ts_ms: u64,
first_confirmation: bool,
) {
let stashed = self.pending.vector_for(query);
if stashed.is_none() && tokenize(query).is_empty() {
return; }
self.built_from_ts = self.built_from_ts.max(ts_ms);
let usable = match (&self.model, &stashed) {
(Some(m), Some((_, fp))) => m == fp,
_ => true,
};
let vector: Option<Vec<f32>> = if usable {
stashed.as_ref().map(|(v, _)| v.clone())
} else {
None
};
let fingerprint: Option<String> = stashed.as_ref().map(|(_, fp)| fp.clone());
let idx = match self.best_match(query, vector.as_deref()) {
Some(i) => i,
None => {
let id = format!("intent_{}", self.next_intent_seq());
self.intents.push(Intent {
id,
label: String::new(),
terms: Vec::new(),
members: Vec::new(),
centroid: None,
support: 0,
last_ts: 0,
tools: BTreeMap::new(),
skills: BTreeMap::new(),
bag: std::collections::HashSet::new(),
member_bags: Vec::new(),
vector_n: 0,
});
self.intents.len() - 1
}
};
{
let it = &mut self.intents[idx];
if !it.members.iter().any(|m| m == query) {
it.members.push(query.to_string());
it.absorb_tokens(query);
if let Some(v) = vector.as_deref() {
it.absorb_vector(v);
}
it.cap_members();
}
if first_confirmation || it.support == 0 {
it.support = it.support.saturating_add(1);
}
it.last_ts = it.last_ts.max(ts_ms);
let edges = match kind {
Capability::Tool => &mut it.tools,
Capability::Skill => &mut it.skills,
};
*edges.entry(capability_id.to_string()).or_insert(0.0) += 1.0;
}
if self.model.is_none() && self.intents[idx].centroid.is_some() {
self.model = fingerprint;
}
let now = self.built_from_ts;
self.intents
.retain(|it| recency_factor(now, it.last_ts) >= EVICTION_FLOOR);
self.rev += 1;
}
pub fn rev(&self) -> u64 {
self.rev
}
pub(crate) fn model_status(
&self,
active_fingerprint: &str,
query_dim: usize,
) -> GraphModelStatus {
let Some(built_dim) = self
.intents
.iter()
.find_map(|i| i.centroid.as_ref().map(Vec::len))
else {
return GraphModelStatus::Ok; };
if built_dim != query_dim {
return GraphModelStatus::DimMismatch {
built: built_dim,
active: query_dim,
};
}
match &self.model {
Some(built) if built != active_fingerprint => GraphModelStatus::ModelMismatch {
built: built.clone(),
active: active_fingerprint.to_string(),
},
_ => GraphModelStatus::Ok,
}
}
pub(crate) fn rebuild_centroids(
&mut self,
per_cluster: Vec<(String, Vec<Vec<f32>>)>,
fingerprint: String,
) {
let index: std::collections::HashMap<String, usize> = self
.intents
.iter()
.enumerate()
.map(|(i, it)| (it.id.clone(), i))
.collect();
for (id, vectors) in per_cluster {
if vectors.is_empty() {
continue;
}
let Some(&i) = index.get(&id) else {
continue; };
let dim = vectors[0].len();
let mut sum = vec![0.0f32; dim];
for v in &vectors {
for (s, x) in sum.iter_mut().zip(v) {
*s += x;
}
}
self.intents[i].centroid = Some(normalize(sum));
}
self.model = Some(fingerprint);
self.rev += 1;
}
fn best_match(&self, query: &str, vector: Option<&[f32]>) -> Option<usize> {
if let Some(v) = vector
&& let Some(i) = self.best_dense_match(v)
{
return Some(i);
}
self.best_lexical_match(query)
}
fn best_dense_match(&self, vector: &[f32]) -> Option<usize> {
self.intents
.iter()
.enumerate()
.filter_map(|(i, it)| {
let c = it.centroid.as_deref()?;
if c.len() != vector.len() {
return None; }
Some((i, cosine(vector, c)))
})
.filter(|(_, sim)| *sim >= TAU_COSINE)
.max_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| self.intents[b.0].id.cmp(&self.intents[a.0].id))
})
.map(|(i, _)| i)
}
fn best_lexical_match(&self, query: &str) -> Option<usize> {
let q: std::collections::HashSet<String> = tokenize(query).into_iter().collect();
if q.is_empty() {
return None;
}
self.intents
.iter()
.enumerate()
.map(|(i, it)| (i, it.lexical_score(&q)))
.filter(|(_, score)| *score >= TAU_LEXICAL)
.max_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| self.intents[b.0].id.cmp(&self.intents[a.0].id))
})
.map(|(i, _)| i)
}
fn next_intent_seq(&self) -> usize {
self.intents
.iter()
.filter_map(|i| i.id.strip_prefix("intent_")?.parse::<usize>().ok())
.max()
.map_or(0, |m| m + 1)
}
fn medoid(&self, idx: usize) -> String {
let it = &self.intents[idx];
let tokenized: Vec<(&String, Vec<String>)> =
it.members.iter().map(|m| (m, tokenize(m))).collect();
tokenized
.iter()
.enumerate()
.map(|(i, (m, t))| {
let shared = t
.iter()
.filter(|tok| {
tokenized
.iter()
.enumerate()
.any(|(j, (_, other))| j != i && other.contains(*tok))
})
.count();
let score = if t.is_empty() {
0.0
} else {
shared as f32 / t.len() as f32
};
(*m, score)
})
.max_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| b.0.cmp(a.0))
})
.map(|(m, _)| m.clone())
.unwrap_or_default()
}
fn c_tf_idf_terms(
cluster_tokens: &[String],
total: usize,
avg: f32,
global: &std::collections::HashMap<&str, usize>,
) -> Vec<String> {
use std::collections::HashMap;
if total == 0 || cluster_tokens.is_empty() {
return Vec::new();
}
let mut local: HashMap<&str, usize> = HashMap::new();
for t in cluster_tokens {
*local.entry(t.as_str()).or_insert(0) += 1;
}
let len = cluster_tokens.len() as f32;
let mut scored: Vec<(String, f32)> = local
.into_iter()
.map(|(t, count)| {
let f = global[t] as f32;
(t.to_string(), (count as f32 / len) * (1.0 + avg / f).ln())
})
.collect();
sort_and_truncate(&mut scored, MAX_TERMS);
scored.into_iter().map(|(t, _)| t).collect()
}
pub fn labeled(&self) -> Vec<Intent> {
use std::collections::HashMap;
let per_cluster: Vec<Vec<String>> = self
.intents
.iter()
.map(|it| it.members.iter().flat_map(|m| tokenize(m)).collect())
.collect();
let total: usize = per_cluster.iter().map(|c| c.len()).sum();
let avg = if per_cluster.is_empty() {
0.0
} else {
total as f32 / per_cluster.len() as f32
};
let mut global: HashMap<&str, usize> = HashMap::new();
for c in &per_cluster {
for t in c {
*global.entry(t.as_str()).or_insert(0) += 1;
}
}
self.intents
.iter()
.enumerate()
.map(|(i, it)| Intent {
label: self.medoid(i),
terms: Self::c_tf_idf_terms(&per_cluster[i], total, avg, &global),
..it.clone()
})
.collect()
}
pub(crate) fn arm(
&self,
query: &str,
query_vec: Option<&[f32]>,
kind: Capability,
known: &dyn Fn(&str) -> bool,
) -> Option<UsageArm> {
match query_vec {
Some(v) if self.has_centroids() => self
.arm_dense(v, kind, known)
.or_else(|| self.arm_lexical_matching(query, kind, known, true)),
_ => self.arm_lexical(query, kind, known),
}
}
fn has_centroids(&self) -> bool {
self.intents.iter().any(|i| i.centroid.is_some())
}
pub(crate) fn arm_dense(
&self,
query_vec: &[f32],
kind: Capability,
known: &dyn Fn(&str) -> bool,
) -> Option<UsageArm> {
let best = self
.intents
.iter()
.filter_map(|it| {
let c = it.centroid.as_deref()?;
if c.len() != query_vec.len() {
return None; }
Some((it, cosine(query_vec, c)))
})
.filter(|(_, sim)| *sim >= TAU_COSINE)
.max_by(pick_best)?;
arm_from(best.0, best.1, self.built_from_ts, kind, known)
}
pub(crate) fn arm_lexical(
&self,
query: &str,
kind: Capability,
known: &dyn Fn(&str) -> bool,
) -> Option<UsageArm> {
self.arm_lexical_matching(query, kind, known, false)
}
fn arm_lexical_matching(
&self,
query: &str,
kind: Capability,
known: &dyn Fn(&str) -> bool,
centroidless_only: bool,
) -> Option<UsageArm> {
let q: std::collections::HashSet<String> = tokenize(query).into_iter().collect();
if q.is_empty() {
return None;
}
let best = self
.intents
.iter()
.filter(|it| !centroidless_only || it.centroid.is_none())
.map(|it| (it, it.lexical_score(&q)))
.filter(|(_, score)| *score >= TAU_LEXICAL)
.max_by(pick_best)?;
arm_from(best.0, best.1, self.built_from_ts, kind, known)
}
}
fn pick_best(a: &(&Intent, f32), b: &(&Intent, f32)) -> std::cmp::Ordering {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| b.0.id.cmp(&a.0.id))
}
fn arm_from(
intent: &Intent,
similarity: f32,
now_ts: u64,
kind: Capability,
known: &dyn Fn(&str) -> bool,
) -> Option<UsageArm> {
let ids = intent.ranked(kind, known);
if ids.is_empty() {
return None; }
let weight = usage_weight(intent.support) * recency_factor(now_ts, intent.last_ts);
Some(UsageArm {
intent_id: intent.id.clone(),
similarity,
support: intent.support,
weight,
ids,
})
}
fn normalize(mut v: Vec<f32>) -> Vec<f32> {
let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for x in &mut v {
*x /= norm;
}
}
v
}
fn cosine(a: &[f32], b: &[f32]) -> f32 {
let mut dot = 0.0;
let mut na = 0.0;
let mut nb = 0.0;
for (x, y) in a.iter().zip(b.iter()) {
dot += x * y;
na += x * x;
nb += y * y;
}
if na == 0.0 || nb == 0.0 {
return 0.0;
}
dot / (na.sqrt() * nb.sqrt())
}
fn tokenize(text: &str) -> Vec<String> {
text.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_lowercase())
.filter(|t| !STOPWORDS.contains(&t.as_str()))
.collect()
}
const STOPWORDS: &[&str] = &[
"a", "an", "and", "are", "as", "at", "be", "but", "by", "did", "do", "does", "for", "from",
"how", "i", "if", "in", "is", "it", "my", "of", "on", "or", "that", "the", "this", "to", "was",
"what", "when", "where", "which", "why", "with", "you", "your",
];
#[cfg(test)]
mod tests {
use super::*;
fn intent(id: &str, members: &[&str], tools: &[(&str, f32)]) -> Intent {
let mut it = Intent {
id: id.into(),
label: members.first().copied().unwrap_or_default().into(),
terms: Vec::new(),
members: members.iter().map(|m| m.to_string()).collect(),
centroid: None,
support: 5,
last_ts: 0,
tools: tools.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
skills: BTreeMap::new(),
bag: std::collections::HashSet::new(),
member_bags: Vec::new(),
vector_n: 0,
};
it.rebuild_bag(); it
}
fn graph(intents: Vec<Intent>) -> IntentGraph {
IntentGraph {
v: 1,
built_from_ts: 1_753_000_000_000,
rev: 0,
intents,
model: None,
pending: PendingQuery::default(),
credit: CreditSlot::default(),
}
}
fn all_known(_: &str) -> bool {
true
}
#[test]
fn a_vector_query_boosts_a_centroidless_cluster_when_dense_misses() {
let mut dense = intent(
"dense",
&["why is the build broken"],
&[("gh_run_list", 1.0)],
);
dense.centroid = Some(normalize(vec![1.0, 0.0, 0.0]));
let lexical = intent(
"lexical",
&["deploy the app to prod"],
&[("deploy_tool", 1.0)],
);
let g = graph(vec![dense, lexical]);
let arm = g.arm(
"deploy the app to prod",
Some(&[0.0, 1.0, 0.0]),
Capability::Tool,
&all_known,
);
let arm = arm.expect("word-only cluster must still boost on a dense miss");
assert_eq!(arm.intent_id, "lexical");
assert_eq!(arm.ids, vec!["deploy_tool".to_string()]);
}
#[test]
fn a_dense_rejected_cluster_is_not_rescued_by_word_overlap() {
let mut dense = intent(
"dense",
&["deploy the app to prod"],
&[("gh_run_list", 1.0)],
);
dense.centroid = Some(normalize(vec![1.0, 0.0, 0.0]));
let g = graph(vec![dense]);
let arm = g.arm(
"deploy the app to prod",
Some(&[0.0, 1.0, 0.0]),
Capability::Tool,
&all_known,
);
assert!(
arm.is_none(),
"a fingerprinted cluster dense rejected must not match lexically, got {arm:?}"
);
}
#[test]
fn absorb_vector_weights_by_vectors_folded_not_member_count() {
let mut it = intent("i0", &["a", "b", "c", "d"], &[("t", 1.0)]);
assert!(
it.centroid.is_none(),
"four lexical members, no centroid yet"
);
it.absorb_vector(&[1.0, 0.0, 0.0]); it.absorb_vector(&[0.0, 1.0, 0.0]);
let c = it.centroid.as_ref().unwrap();
assert!(
(c[0] - c[1]).abs() < 1e-6,
"equal-weight vectors → symmetric centroid, got {c:?}"
);
assert!(
(c[0] - std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-6,
"expected ~0.707 per axis, got {c:?}"
);
}
#[test]
fn medoid_labels_the_most_central_member_not_the_alphabetical_first() {
let g = graph(vec![intent(
"i0",
&[
"a lonely unique phrase",
"build broken ci",
"build broken pipeline",
],
&[("t", 1.0)],
)]);
assert_eq!(g.medoid(0), "build broken ci");
}
#[test]
fn support_ramps_the_arm_weight_then_caps() {
assert!((usage_weight(1) - USAGE_WEIGHT / 3.0).abs() < 1e-6);
assert!((usage_weight(2) - USAGE_WEIGHT * 2.0 / 3.0).abs() < 1e-6);
assert!((usage_weight(3) - USAGE_WEIGHT).abs() < 1e-6);
assert!((usage_weight(900) - USAGE_WEIGHT).abs() < 1e-6);
}
#[test]
fn a_single_observation_is_weaker_than_a_confirmed_cluster() {
assert!(usage_weight(1) < usage_weight(3));
}
#[test]
fn parses_a_graph_without_a_centroid() {
let json = r#"{"v":1,"built_from_ts":1,
"intents":[{"id":"i0","label":"l","members":["q"],"support":2,
"tools":{"t":1.0},"skills":{}}]}"#;
let g = IntentGraph::from_json(json).expect("valid graph");
assert_eq!(g.len(), 1);
assert!(g.intents[0].centroid.is_none());
}
#[test]
fn rejects_an_unknown_version_instead_of_degrading() {
let json = r#"{"v":2,"built_from_ts":1,"intents":[]}"#;
assert_eq!(
IntentGraph::from_json(json),
Err(IntentGraphError::UnsupportedVersion(2))
);
}
#[test]
fn rejects_malformed_bytes() {
assert!(matches!(
IntentGraph::from_json("not json"),
Err(IntentGraphError::Malformed(_))
));
}
#[test]
fn observe_bumps_rev_once_per_mutation() {
let mut g = IntentGraph::empty();
assert_eq!(g.rev(), 0, "an empty graph has written nothing");
g.observe("build broken", Capability::Tool, "a", T0, true);
assert_eq!(g.rev(), 1);
g.observe("build broken", Capability::Tool, "b", T0, false);
assert_eq!(g.rev(), 2);
}
#[test]
fn a_no_op_observe_does_not_bump_rev() {
let mut g = IntentGraph::empty();
g.observe(" ", Capability::Tool, "a", T0, true);
assert_eq!(g.len(), 0);
assert_eq!(g.rev(), 0);
}
#[test]
fn rev_survives_a_round_trip() {
let mut g = IntentGraph::empty();
g.observe("build broken", Capability::Tool, "a", T0, true);
g.observe("rotate the signing key", Capability::Tool, "b", T0, true);
let before = g.rev();
assert_eq!(before, 2);
let back = IntentGraph::from_json(&serde_json::to_string(&g).unwrap()).unwrap();
assert_eq!(back.rev(), before, "rev must persist across the wire form");
}
#[test]
fn a_graph_without_rev_loads_as_zero_then_continues() {
let json = r#"{"v":1,"built_from_ts":1,
"intents":[{"id":"i0","label":"l","members":["q"],"support":2,
"tools":{"t":1.0},"skills":{}}]}"#;
let mut g = IntentGraph::from_json(json).expect("valid graph");
assert_eq!(g.rev(), 0);
g.observe("something new", Capability::Tool, "t", T0, true);
assert_eq!(g.rev(), 1);
}
#[test]
fn an_unknown_field_is_ignored_on_load() {
let json = r#"{"v":1,"built_from_ts":1,"future_top_level":42,
"intents":[{"id":"i0","label":"l","members":["q"],"support":2,
"tools":{"t":1.0},"skills":{},"future_intent_field":"x"}]}"#;
let g = IntentGraph::from_json(json).expect("unknown fields must be ignored");
assert_eq!(g.len(), 1);
assert_eq!(g.intents[0].support, 2);
}
#[test]
fn an_empty_graph_contributes_no_arm() {
let g = IntentGraph::empty();
assert!(g.is_empty());
assert_eq!(
g.arm_lexical("anything", Capability::Tool, &all_known),
None
);
assert_eq!(g.arm_dense(&[1.0], Capability::Tool, &all_known), None);
}
#[test]
fn dense_match_returns_edges_best_first() {
let mut it = intent("i0", &["why is the build broken"], &[]);
it.centroid = Some(vec![1.0, 0.0, 0.0]);
it.tools = [
("gh_run_view".to_string(), 0.2),
("gh_run_list".to_string(), 0.8),
]
.into_iter()
.collect();
let g = graph(vec![it]);
let arm = g
.arm_dense(&[1.0, 0.0, 0.0], Capability::Tool, &all_known)
.expect("exact match");
assert_eq!(arm.ids, vec!["gh_run_list", "gh_run_view"]);
assert_eq!(arm.intent_id, "i0");
}
#[test]
fn dense_match_below_tau_yields_no_arm() {
let mut it = intent("i0", &["q"], &[("t", 1.0)]);
it.centroid = Some(vec![1.0, 0.0]);
let g = graph(vec![it]);
assert_eq!(g.arm_dense(&[0.0, 1.0], Capability::Tool, &all_known), None);
}
#[test]
fn dense_match_skips_centroids_of_a_different_dimension() {
let mut it = intent("i0", &["q"], &[("t", 1.0)]);
it.centroid = Some(vec![1.0, 0.0, 0.0]);
let g = graph(vec![it]);
assert_eq!(g.arm_dense(&[1.0, 0.0], Capability::Tool, &all_known), None);
}
#[test]
fn dense_match_picks_the_closest_of_several_clusters() {
let mut a = intent("a", &["q"], &[("ta", 1.0)]);
a.centroid = Some(vec![1.0, 0.0]);
let mut b = intent("b", &["q"], &[("tb", 1.0)]);
b.centroid = Some(vec![0.8, 0.6]);
let g = graph(vec![a, b]);
let arm = g
.arm_dense(&[0.8, 0.6], Capability::Tool, &all_known)
.expect("match");
assert_eq!(arm.intent_id, "b");
}
#[test]
fn lexical_match_finds_a_repeat_phrasing() {
let g = graph(vec![intent(
"i0",
&["why is the build broken", "is the build green"],
&[("gh_run_list", 1.0)],
)]);
let arm = g
.arm_lexical("is the build broken", Capability::Tool, &all_known)
.expect("shares 'build' and 'broken'");
assert_eq!(arm.ids, vec!["gh_run_list"]);
}
#[test]
fn lexical_match_cannot_bridge_disjoint_vocabulary() {
let g = graph(vec![intent(
"i0",
&["why is the build broken"],
&[("gh_run_list", 1.0)],
)]);
assert_eq!(
g.arm_lexical("did CI pass", Capability::Tool, &all_known),
None
);
}
#[test]
fn lexical_match_ignores_stopwords_only_queries() {
let g = graph(vec![intent("i0", &["build"], &[("t", 1.0)])]);
assert_eq!(g.arm_lexical("is the", Capability::Tool, &all_known), None);
}
#[test]
fn edges_naming_capabilities_the_registry_lacks_are_dropped() {
let g = graph(vec![intent(
"i0",
&["build broken"],
&[("gh_run_list", 0.8), ("since_deleted", 0.9)],
)]);
let arm = g
.arm_lexical("build broken", Capability::Tool, &|id| {
id != "since_deleted"
})
.expect("match");
assert_eq!(arm.ids, vec!["gh_run_list"]);
}
#[test]
fn a_match_whose_every_edge_is_gone_yields_no_arm() {
let g = graph(vec![intent("i0", &["build broken"], &[("gone", 1.0)])]);
assert_eq!(
g.arm_lexical("build broken", Capability::Tool, &|_| false),
None
);
}
#[test]
fn tool_and_skill_edges_are_ranked_independently() {
let mut it = intent("i0", &["build broken"], &[("a_tool", 1.0)]);
it.skills = [("a_skill".to_string(), 1.0)].into_iter().collect();
let g = graph(vec![it]);
assert_eq!(
g.arm_lexical("build broken", Capability::Tool, &all_known)
.unwrap()
.ids,
vec!["a_tool"]
);
assert_eq!(
g.arm_lexical("build broken", Capability::Skill, &all_known)
.unwrap()
.ids,
vec!["a_skill"]
);
}
#[test]
fn edges_rank_by_weight_not_by_id() {
let g = graph(vec![intent(
"i0",
&["build broken"],
&[("alpha", 0.1), ("mike", 0.5), ("zulu", 0.9)],
)]);
let arm = g
.arm_lexical("build broken", Capability::Tool, &all_known)
.unwrap();
assert_eq!(arm.ids, vec!["zulu", "mike", "alpha"]);
}
#[test]
fn tied_edge_weights_break_by_id_ascending() {
let g = graph(vec![intent(
"i0",
&["build broken"],
&[("zeta", 1.0), ("alpha", 1.0), ("mid", 1.0)],
)]);
let arm = g
.arm_lexical("build broken", Capability::Tool, &all_known)
.unwrap();
assert_eq!(arm.ids, vec!["alpha", "mid", "zeta"]);
}
#[test]
fn round_trips_through_json() {
let mut it = intent("i0", &["q"], &[("t", 1.0)]);
it.centroid = Some(vec![0.8, 0.6]);
let g = graph(vec![it]);
let back = IntentGraph::from_json(&serde_json::to_string(&g).unwrap()).unwrap();
assert_eq!(g, back);
}
const T0: u64 = 1_753_000_000_000;
#[test]
fn the_first_observation_seeds_a_cluster() {
let mut g = IntentGraph::empty();
g.observe(
"why is the build broken",
Capability::Tool,
"gh_run_list",
T0,
true,
);
assert_eq!(g.len(), 1);
assert_eq!(g.intents[0].support, 1);
assert_eq!(g.intents[0].members, vec!["why is the build broken"]);
assert_eq!(g.intents[0].tools.get("gh_run_list"), Some(&1.0));
assert!(g.intents[0].centroid.is_none());
}
#[test]
fn a_similar_query_joins_the_existing_cluster() {
let mut g = IntentGraph::empty();
g.observe(
"why is the build broken",
Capability::Tool,
"gh_run_list",
T0,
true,
);
g.observe(
"is the build broken now",
Capability::Tool,
"gh_run_list",
T0,
true,
);
assert_eq!(g.len(), 1, "should not have seeded a second cluster");
assert_eq!(g.intents[0].support, 2);
assert_eq!(g.intents[0].tools.get("gh_run_list"), Some(&2.0));
}
#[test]
fn a_dissimilar_query_seeds_its_own_cluster() {
let mut g = IntentGraph::empty();
g.observe(
"why is the build broken",
Capability::Tool,
"gh_run_list",
T0,
true,
);
g.observe(
"rotate the signing key",
Capability::Tool,
"vault_rotate",
T0,
true,
);
assert_eq!(g.len(), 2);
let ids: Vec<&str> = g.intents.iter().map(|i| i.id.as_str()).collect();
assert_eq!(ids, vec!["intent_0", "intent_1"]);
}
#[test]
fn a_repeated_phrasing_is_not_duplicated_in_members() {
let mut g = IntentGraph::empty();
for _ in 0..3 {
g.observe(
"why is the build broken",
Capability::Tool,
"gh_run_list",
T0,
true,
);
}
assert_eq!(g.intents[0].members.len(), 1);
assert_eq!(
g.intents[0].support, 3,
"support still counts every observation"
);
}
#[test]
fn learning_then_searching_closes_the_loop() {
let mut g = IntentGraph::empty();
g.observe(
"why is the build broken",
Capability::Tool,
"gh_run_list",
T0,
true,
);
g.observe(
"is the build broken again",
Capability::Tool,
"gh_run_list",
T0,
true,
);
let arm = g
.arm(
"the build broken on main",
None,
Capability::Tool,
&all_known,
)
.expect("a near-repeat of a member");
assert_eq!(arm.ids, vec!["gh_run_list"]);
assert_eq!(arm.support, 2);
}
#[test]
fn the_lexical_tier_does_not_reach_distant_wording() {
let mut g = IntentGraph::empty();
g.observe(
"why is the build broken",
Capability::Tool,
"gh_run_list",
T0,
true,
);
assert_eq!(
g.arm("is the build ok", None, Capability::Tool, &all_known),
None
);
}
#[test]
fn a_lexically_grown_graph_is_matchable_even_when_a_query_vector_is_offered() {
let mut g = IntentGraph::empty();
g.observe(
"why is the build broken",
Capability::Tool,
"gh_run_list",
T0,
true,
);
let arm = g.arm(
"why is the build broken",
Some(&[0.1, 0.2, 0.3]),
Capability::Tool,
&all_known,
);
assert!(arm.is_some(), "must not be invisible to a semantic catalog");
}
#[test]
fn edges_rank_by_how_often_a_capability_was_chosen() {
let mut g = IntentGraph::empty();
for _ in 0..3 {
g.observe(
"why is the build broken",
Capability::Tool,
"chosen_often",
T0,
true,
);
}
g.observe(
"why is the build broken",
Capability::Tool,
"chosen_once",
T0,
true,
);
let arm = g
.arm(
"why is the build broken",
None,
Capability::Tool,
&all_known,
)
.unwrap();
assert_eq!(arm.ids, vec!["chosen_often", "chosen_once"]);
}
#[test]
fn built_from_ts_tracks_the_newest_event_and_never_rewinds() {
let mut g = IntentGraph::empty();
g.observe("build broken", Capability::Tool, "a", T0 + 10, true);
g.observe("build broken", Capability::Tool, "b", T0, true);
assert_eq!(g.built_from_ts, T0 + 10);
}
#[test]
fn the_token_cache_stays_in_step_with_members() {
let mut g = IntentGraph::empty();
g.observe("why is the build broken", Capability::Tool, "t", T0, true);
g.observe("the pipeline is broken", Capability::Tool, "t", T0, true);
let it = &g.intents[0];
let fresh: std::collections::HashSet<String> =
it.members.iter().flat_map(|m| tokenize(m)).collect();
assert_eq!(&it.bag, &fresh, "union cache drifted from members");
assert_eq!(it.member_bags.len(), it.members.len(), "one set per member");
for (m, bag) in it.members.iter().zip(&it.member_bags) {
let fresh: std::collections::HashSet<String> = tokenize(m).into_iter().collect();
assert_eq!(bag, &fresh, "member set drifted for {m:?}");
}
}
#[test]
fn a_deserialized_graph_can_still_match_lexically() {
let mut g = IntentGraph::empty();
g.observe(
"why is the build broken",
Capability::Tool,
"gh_run_list",
T0,
true,
);
let back = IntentGraph::from_json(&serde_json::to_string(&g).unwrap()).unwrap();
assert!(
back.arm(
"why is the build broken",
None,
Capability::Tool,
&all_known
)
.is_some(),
"a reloaded graph must still match"
);
}
#[test]
fn the_centroid_is_folded_once_per_distinct_member() {
let v1 = [1.0f32, 0.0, 0.0];
let v2 = [0.0f32, 1.0, 0.0];
let build = |extra: usize| {
let mut g = IntentGraph::empty();
g.note_query_vector("build broken", &v1, "m");
g.observe("build broken", Capability::Tool, "a", T0, true);
g.note_query_vector("build broken again", &v2, "m");
g.observe("build broken again", Capability::Tool, "b", T0, true);
for i in 0..extra {
g.observe(
"build broken again",
Capability::Tool,
&format!("x{i}"),
T0,
false,
);
}
g.intents[0].centroid.clone().unwrap()
};
let once = build(0);
let with_extra_invokes = build(3);
for (a, b) in once.iter().zip(&with_extra_invokes) {
assert!(
(a - b).abs() < 1e-6,
"extra invokes moved the centroid: {once:?} vs {with_extra_invokes:?}"
);
}
}
#[test]
fn a_later_invoke_landing_elsewhere_still_has_support() {
let mut g = IntentGraph::empty();
g.observe("why is the build broken", Capability::Tool, "a", T0, false);
assert_eq!(g.intents[0].support, 1);
}
#[test]
fn one_shared_word_does_not_merge_distinct_intents() {
let mut g = IntentGraph::empty();
g.observe("deploy0 rollback3", Capability::Tool, "a", T0, true);
g.observe("deploy0 migrate5", Capability::Tool, "b", T0, true);
assert_eq!(g.len(), 2, "one shared word is not the same question");
}
#[test]
fn a_large_cluster_does_not_absorb_an_unrelated_query() {
let mut g = IntentGraph::empty();
for i in 0..30 {
g.observe(
&format!("build broken variant{i}"),
Capability::Tool,
"gh_run_list",
T0,
true,
);
}
assert_eq!(g.len(), 1, "those really are one ask");
g.observe("variant7 variant12", Capability::Tool, "vault", T0, true);
assert_eq!(
g.len(),
2,
"a big cluster must not absorb by sheer vocabulary"
);
}
#[test]
fn distinct_topics_do_not_collapse_at_scale() {
const WORDS: [&str; 20] = [
"deploy", "rollback", "migrate", "schema", "invoice", "refund", "tenant", "webhook",
"cursor", "throttle", "quota", "shard", "replica", "index", "vault", "rotate", "lease",
"beacon", "harvest", "prune",
];
let mut g = IntentGraph::empty();
for topic in 0..40 {
for phrasing in 0..10 {
let q = format!(
"{}{topic} {}{phrasing}",
WORDS[topic % 20],
WORDS[(topic + phrasing) % 20]
);
g.observe(&q, Capability::Tool, &format!("t{topic}"), T0, true);
}
}
assert!(
g.len() >= 35,
"40 distinct topics collapsed into {} clusters",
g.len()
);
}
#[test]
fn near_repeats_still_merge() {
let mut g = IntentGraph::empty();
for q in [
"why is the build broken",
"is the build broken again",
"the build broken on main",
] {
g.observe(q, Capability::Tool, "gh_run_list", T0, true);
}
for q in ["rotate the signing key", "rotate the signing key now"] {
g.observe(q, Capability::Tool, "vault_rotate", T0, true);
}
assert_eq!(g.len(), 2, "two asks, however phrased");
assert_eq!(g.intents[0].members.len(), 3);
assert_eq!(g.intents[1].members.len(), 2);
}
#[test]
fn the_label_is_always_one_of_the_members() {
let mut g = IntentGraph::empty();
g.observe("why is the build broken", Capability::Tool, "t", T0, true);
g.observe("is the build broken now", Capability::Tool, "t", T0, true);
let it = &g.labeled()[0];
assert!(
it.members.contains(&it.label),
"label {:?} not a member",
it.label
);
}
#[test]
fn terms_distinguish_a_cluster_from_its_neighbours() {
let mut g = IntentGraph::empty();
g.observe("why is the build broken", Capability::Tool, "t", T0, true);
g.observe("the build is broken again", Capability::Tool, "t", T0, true);
g.observe("rotate the signing key", Capability::Tool, "v", T0, true);
let build = &g.labeled()[0];
assert!(
build.terms.contains(&"build".to_string()),
"got {:?}",
build.terms
);
assert!(!build.terms.contains(&"rotate".to_string()));
}
#[test]
fn terms_are_scored_against_the_graph_as_it_is_now() {
let mut g = IntentGraph::empty();
g.observe("why is the build broken", Capability::Tool, "t", T0, true);
g.observe("the build broken again", Capability::Tool, "t", T0, true);
let alone = g.labeled()[0].terms.clone();
g.observe(
"tail the service log again",
Capability::Tool,
"u",
T0,
true,
);
let with_neighbour = g.labeled()[0].terms.clone();
let rank = |terms: &[String], t: &str| terms.iter().position(|x| x == t);
assert!(
rank(&with_neighbour, "again") >= rank(&alone, "again"),
"\"again\" should not gain rank once a neighbour shares it: {alone:?} -> {with_neighbour:?}"
);
}
#[test]
fn the_label_is_derived_not_stored() {
let mut g = IntentGraph::empty();
g.observe("why is the build broken", Capability::Tool, "t", T0, true);
assert!(
g.intents[0].label.is_empty(),
"not stored on the write path"
);
assert!(!g.labeled()[0].label.is_empty(), "materialized on read");
}
#[test]
fn a_stopword_only_query_teaches_nothing() {
let mut g = IntentGraph::empty();
g.observe("is the", Capability::Tool, "t", T0, true);
assert!(g.is_empty());
}
#[test]
fn tool_and_skill_observations_land_on_separate_edge_maps() {
let mut g = IntentGraph::empty();
g.observe(
"why is the build broken",
Capability::Tool,
"gh_run_list",
T0,
true,
);
g.observe(
"why is the build broken",
Capability::Skill,
"ci-triage",
T0,
true,
);
assert_eq!(g.len(), 1);
assert_eq!(g.intents[0].tools.len(), 1);
assert_eq!(g.intents[0].skills.len(), 1);
}
#[test]
fn a_learned_graph_round_trips_through_the_wire_form() {
let mut g = IntentGraph::empty();
g.observe(
"why is the build broken",
Capability::Tool,
"gh_run_list",
T0,
true,
);
let back = IntentGraph::from_json(&serde_json::to_string(&g).unwrap()).unwrap();
assert_eq!(g, back);
}
fn dense_intent(id: &str, centroid: Vec<f32>) -> Intent {
let mut it = intent(id, &["why is the build broken"], &[("gh_run_list", 1.0)]);
it.centroid = Some(normalize(centroid));
it
}
#[test]
fn model_status_ok_for_a_lexical_graph() {
let g = graph(vec![intent("i0", &["build broken"], &[("t", 1.0)])]);
assert_eq!(g.model_status("any-model", 384), GraphModelStatus::Ok);
}
#[test]
fn model_status_flags_a_dimension_change() {
let mut g = graph(vec![dense_intent("i0", vec![1.0, 0.0, 0.0])]);
g.model = Some("bge-small".into());
assert_eq!(
g.model_status("bge-base", 768),
GraphModelStatus::DimMismatch {
built: 3,
active: 768
}
);
}
#[test]
fn model_status_flags_a_same_dim_model_change() {
let mut g = graph(vec![dense_intent("i0", vec![1.0, 0.0, 0.0])]);
g.model = Some("model-a".into());
assert_eq!(
g.model_status("model-b", 3),
GraphModelStatus::ModelMismatch {
built: "model-a".into(),
active: "model-b".into()
}
);
}
#[test]
fn model_status_ok_when_the_model_matches() {
let mut g = graph(vec![dense_intent("i0", vec![1.0, 0.0, 0.0])]);
g.model = Some("model-a".into());
assert_eq!(g.model_status("model-a", 3), GraphModelStatus::Ok);
}
#[test]
fn observe_stamps_the_model_on_the_first_centroid() {
let mut g = IntentGraph::empty();
g.note_query_vector("build broken", &[1.0, 0.0, 0.0], "model-a");
g.observe("build broken", Capability::Tool, "t", T0, true);
assert_eq!(g.model.as_deref(), Some("model-a"));
}
#[test]
fn observe_freezes_the_centroid_on_a_model_change() {
let mut g = IntentGraph::empty();
g.note_query_vector("build broken", &[1.0, 0.0, 0.0], "model-a");
g.observe("build broken", Capability::Tool, "t", T0, true);
let frozen = g.intents[0].centroid.clone();
g.note_query_vector("build broken again", &[0.0, 1.0, 0.0], "model-b");
g.observe("build broken again", Capability::Tool, "t", T0, true);
assert_eq!(
g.intents[0].centroid, frozen,
"centroid must not blend models"
);
assert_eq!(g.intents[0].members.len(), 2, "member still recorded");
assert_eq!(g.intents[0].support, 2, "support still counts");
assert_eq!(g.model.as_deref(), Some("model-a"), "model unchanged");
}
#[test]
fn rebuild_centroids_re_embeds_members_and_restamps() {
let mut g = IntentGraph::empty();
g.note_query_vector("build broken", &[1.0, 0.0, 0.0], "model-a");
g.observe("build broken", Capability::Tool, "gh_run_list", T0, true);
let rev_before = g.rev();
let id = g.intents[0].id.clone();
g.rebuild_centroids(vec![(id, vec![vec![0.0, 1.0, 0.0]])], "model-b".into());
assert_eq!(g.rev(), rev_before + 1);
assert_eq!(g.model.as_deref(), Some("model-b"));
assert_eq!(g.model_status("model-b", 3), GraphModelStatus::Ok);
assert_eq!(g.intents[0].support, 1);
assert_eq!(g.intents[0].tools.get("gh_run_list"), Some(&1.0));
let c = g.intents[0].centroid.as_ref().unwrap();
assert!((c[1] - 1.0).abs() < 1e-6);
}
#[test]
fn rebuild_centroids_follows_ids_when_a_cluster_is_evicted_mid_rebuild() {
let mut g = IntentGraph::empty();
g.observe("alpha query", Capability::Tool, "a", T0, true);
g.observe("bravo query", Capability::Tool, "b", T0, true);
g.observe("charlie query", Capability::Tool, "c", T0, true);
assert_eq!(g.len(), 3, "three disjoint queries → three clusters");
let id_a = g.intents[0].id.clone();
let id_b = g.intents[1].id.clone();
let id_c = g.intents[2].id.clone();
let per_cluster = vec![
(id_a.clone(), vec![vec![1.0, 0.0, 0.0]]),
(id_b.clone(), vec![vec![0.0, 1.0, 0.0]]),
(id_c.clone(), vec![vec![0.0, 0.0, 1.0]]),
];
g.intents.retain(|it| it.id != id_a);
assert_eq!(g.len(), 2);
g.rebuild_centroids(per_cluster, "model-b".into());
let b = g.intents.iter().find(|it| it.id == id_b).unwrap();
assert!(
(b.centroid.as_ref().unwrap()[1] - 1.0).abs() < 1e-6,
"cluster b must keep its own centroid, got {:?}",
b.centroid
);
let c = g.intents.iter().find(|it| it.id == id_c).unwrap();
assert!(
(c.centroid.as_ref().unwrap()[2] - 1.0).abs() < 1e-6,
"cluster c must keep its own centroid, got {:?}",
c.centroid
);
}
#[test]
fn the_model_field_round_trips_through_json() {
let mut g = graph(vec![dense_intent("i0", vec![0.6, 0.8, 0.0])]);
g.model = Some("bge-small".into());
let back = IntentGraph::from_json(&serde_json::to_string(&g).unwrap()).unwrap();
assert_eq!(back.model.as_deref(), Some("bge-small"));
}
#[test]
fn from_json_matches_the_conformance_valid_and_invalid_sets() {
let vectors: serde_json::Value = serde_json::from_str(include_str!(
"../../../protocol/v1/conformance/vectors.json"
))
.expect("conformance vectors parse");
let graph = &vectors["graph"];
for case in graph["invalid"].as_array().unwrap() {
let name = case["name"].as_str().unwrap();
let doc = serde_json::to_string(&case["doc"]).unwrap();
assert!(
IntentGraph::from_json(&doc).is_err(),
"invalid vector `{name}` must be rejected"
);
}
for case in graph["valid"].as_array().unwrap() {
let name = case["name"].as_str().unwrap();
let doc = serde_json::to_string(&case["doc"]).unwrap();
assert!(
IntentGraph::from_json(&doc).is_ok(),
"valid vector `{name}` must be accepted"
);
}
}
#[test]
fn a_lexical_graph_omits_the_model_on_the_wire() {
let g = graph(vec![intent("i0", &["build broken"], &[("t", 1.0)])]);
let json = serde_json::to_string(&g).unwrap();
assert!(
!json.contains("model"),
"no model field for a centroid-less graph"
);
}
const DAY: u64 = 86_400_000;
#[test]
fn a_recent_cluster_keeps_full_weight_within_the_grace() {
let mut g = IntentGraph::empty();
for _ in 0..3 {
g.observe("why is the build broken", Capability::Tool, "t", T0, true);
}
let arm = g
.arm(
"why is the build broken",
None,
Capability::Tool,
&all_known,
)
.unwrap();
assert!((arm.weight() - USAGE_WEIGHT).abs() < 1e-6);
}
#[test]
fn a_stale_cluster_decays_after_the_grace() {
let mut g = IntentGraph::empty();
for _ in 0..3 {
g.observe("why is the build broken", Capability::Tool, "t", T0, true);
}
g.observe(
"rotate the signing key",
Capability::Tool,
"v",
T0 + 200 * DAY,
true,
);
let arm = g
.arm(
"why is the build broken",
None,
Capability::Tool,
&all_known,
)
.unwrap();
let expected = USAGE_WEIGHT * 2f32.powf(-((200.0 - 90.0) / 90.0));
assert!(
(arm.weight() - expected).abs() < 1e-3,
"got {} expected {expected}",
arm.weight()
);
assert!(
arm.weight() < USAGE_WEIGHT,
"a cold cluster must weigh less"
);
}
#[test]
fn a_long_idle_cluster_is_evicted() {
let mut g = IntentGraph::empty();
for _ in 0..3 {
g.observe("why is the build broken", Capability::Tool, "t", T0, true);
}
assert_eq!(g.len(), 1);
g.observe(
"rotate the signing key",
Capability::Tool,
"v",
T0 + 700 * DAY,
true,
);
assert_eq!(
g.len(),
1,
"the stale cluster was evicted, the fresh one stays"
);
assert!(
g.arm(
"why is the build broken",
None,
Capability::Tool,
&all_known
)
.is_none(),
"an evicted cluster contributes no arm"
);
}
#[test]
fn members_are_capped_per_cluster() {
let mut g = IntentGraph::empty();
for i in 0..MEMBER_CAP + 20 {
g.observe(
&format!("build broken variant{i}"),
Capability::Tool,
"t",
T0,
true,
);
}
assert_eq!(g.len(), 1, "near-repeats form one cluster");
assert!(
g.intents[0].members.len() <= MEMBER_CAP,
"members capped, got {}",
g.intents[0].members.len()
);
let fresh: std::collections::HashSet<String> = g.intents[0]
.members
.iter()
.flat_map(|m| tokenize(m))
.collect();
assert_eq!(&g.intents[0].bag, &fresh);
}
}