use std::collections::HashSet;
use serde::Serialize;
use crate::store::{Store, StoreError};
use crate::{Edge, EdgeKind, Node};
const DIM: usize = 256;
pub const EMBED_REF: &str = "embedding:hash/v1";
#[derive(Debug, Clone, Copy)]
pub struct InferenceConfig {
pub min_confidence: f64,
pub top_k: usize,
}
impl Default for InferenceConfig {
fn default() -> Self {
Self {
min_confidence: 0.4,
top_k: 5,
}
}
}
type Embedding = [f32; DIM];
fn fnv1a(s: &str) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for b in s.bytes() {
hash ^= u64::from(b);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
fn tokens(text: &str) -> Vec<String> {
let lower = text.to_lowercase();
let mut out = Vec::new();
for word in lower
.split(|c: char| !c.is_alphanumeric())
.filter(|w| !w.is_empty())
{
out.push(word.to_owned());
let chars: Vec<char> = word.chars().collect();
if chars.len() >= 3 {
for w in chars.windows(3) {
out.push(w.iter().collect());
}
}
}
out
}
#[must_use]
pub fn embed(text: &str) -> Embedding {
let mut v = [0f32; DIM];
for tok in tokens(text) {
let h = fnv1a(&tok);
let idx = usize::try_from(h % DIM as u64).unwrap_or(0);
let sign = if (h >> 63) & 1 == 1 { -1.0 } else { 1.0 };
v[idx] += sign;
}
let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for x in &mut v {
*x /= norm;
}
}
v
}
#[must_use]
pub fn similarity(a: &[f32], b: &[f32]) -> f64 {
if a.len() != b.len() {
return 0.0;
}
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
f64::from(dot).clamp(0.0, 1.0)
}
pub trait Embedder {
fn embed(&self, text: &str) -> Vec<f32>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct HashEmbedder;
impl Embedder for HashEmbedder {
fn embed(&self, text: &str) -> Vec<f32> {
embed(text).to_vec()
}
}
fn node_text(node: &Node) -> String {
let mut text = node.name.clone();
if let Some(path) = &node.path
&& let Some(stem) = std::path::Path::new(path)
.file_stem()
.and_then(|s| s.to_str())
{
text.push(' ');
text.push_str(stem);
}
if let Some(content) = node.meta.get("content").and_then(|v| v.as_str()) {
text.push(' ');
text.push_str(content);
}
text
}
pub fn infer_edges(store: &Store, config: InferenceConfig) -> Result<Vec<Edge>, StoreError> {
infer_edges_with(store, config, &HashEmbedder)
}
pub fn infer_edges_with(
store: &Store,
config: InferenceConfig,
embedder: &dyn Embedder,
) -> Result<Vec<Edge>, StoreError> {
let keys = store.all_keys()?;
let mut nodes: Vec<(String, Vec<f32>)> = Vec::with_capacity(keys.len());
for key in &keys {
if let Some(node) = store.get_node(key)? {
nodes.push((node.key.clone(), embedder.embed(&node_text(&node))));
}
}
let mut existing_owned: Vec<(String, String)> = Vec::new();
for (key, _) in &nodes {
for edge in store.edges_from(key)? {
existing_owned.push((edge.src, edge.dst));
}
}
let existing: HashSet<(&str, &str)> = existing_owned
.iter()
.map(|(s, d)| (s.as_str(), d.as_str()))
.collect();
let connected = |a: &str, b: &str| existing.contains(&(a, b)) || existing.contains(&(b, a));
let mut edges = Vec::new();
for (i, (src, src_vec)) in nodes.iter().enumerate() {
let mut candidates: Vec<(f64, &str)> = Vec::new();
for (j, (dst, dst_vec)) in nodes.iter().enumerate() {
if i == j || connected(src, dst) {
continue;
}
let sim = similarity(src_vec, dst_vec);
if sim >= config.min_confidence {
candidates.push((sim, dst.as_str()));
}
}
candidates.sort_by(|a, b| {
b.0.partial_cmp(&a.0)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.1.cmp(b.1))
});
for (sim, dst) in candidates.into_iter().take(config.top_k) {
let mut edge = Edge::inferred(src.clone(), dst.to_owned(), EdgeKind::Related, sim);
edge.src_ref = Some(EMBED_REF.to_owned());
edges.push(edge);
}
}
Ok(edges)
}
#[derive(Debug, Clone, Copy)]
pub struct DuplicateConfig {
pub min_similarity: f64,
pub limit: usize,
}
impl Default for DuplicateConfig {
fn default() -> Self {
Self {
min_similarity: 0.9,
limit: 50,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DuplicatePair {
pub a: String,
pub b: String,
pub similarity: f64,
pub exact: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DuplicateReport {
pub schema: &'static str,
pub total: usize,
pub pairs: Vec<DuplicatePair>,
}
fn better_first(p: &DuplicatePair, q: &DuplicatePair) -> std::cmp::Ordering {
q.exact
.cmp(&p.exact)
.then_with(|| {
q.similarity
.partial_cmp(&p.similarity)
.unwrap_or(std::cmp::Ordering::Equal)
})
.then_with(|| (&p.a, &p.b).cmp(&(&q.a, &q.b)))
}
struct ByRank(DuplicatePair);
impl PartialEq for ByRank {
fn eq(&self, other: &Self) -> bool {
better_first(&self.0, &other.0) == std::cmp::Ordering::Equal
}
}
impl Eq for ByRank {}
impl PartialOrd for ByRank {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ByRank {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
better_first(&self.0, &other.0)
}
}
pub fn duplicates(store: &Store, config: DuplicateConfig) -> Result<DuplicateReport, StoreError> {
duplicates_with(store, config, &HashEmbedder)
}
pub fn duplicates_with(
store: &Store,
config: DuplicateConfig,
embedder: &dyn Embedder,
) -> Result<DuplicateReport, StoreError> {
struct Cand {
key: String,
is_file: bool,
blob: Option<String>,
has_content: bool,
vec: Vec<f32>,
}
let mut cands: Vec<Cand> = Vec::new();
for key in store.all_keys()? {
let Some(node) = store.get_node(&key)? else {
continue;
};
let is_file = node.kind == crate::NodeKind::File;
let has_content = node
.meta
.get("content")
.and_then(serde_json::Value::as_str)
.is_some_and(|s| !s.is_empty());
let file_with_blob = is_file && node.blob_hash.is_some();
if !file_with_blob && !has_content {
continue;
}
cands.push(Cand {
key: node.key.clone(),
is_file,
blob: node.blob_hash.clone(),
has_content,
vec: embedder.embed(&node_text(&node)),
});
}
cands.sort_by(|x, y| x.key.cmp(&y.key));
let mut total = 0usize;
let mut heap: std::collections::BinaryHeap<ByRank> = std::collections::BinaryHeap::new();
for (i, a) in cands.iter().enumerate() {
for b in cands.iter().skip(i + 1) {
let exact = a.is_file
&& b.is_file
&& matches!((&a.blob, &b.blob), (Some(x), Some(y)) if x == y);
let both_content = a.has_content && b.has_content;
if !exact && !both_content {
continue;
}
let sim = similarity(&a.vec, &b.vec);
if !exact && sim < config.min_similarity {
continue;
}
total += 1;
if config.limit == 0 {
continue;
}
heap.push(ByRank(DuplicatePair {
a: a.key.clone(),
b: b.key.clone(),
similarity: sim,
exact,
}));
if heap.len() > config.limit {
heap.pop(); }
}
}
let mut pairs: Vec<DuplicatePair> = heap.into_iter().map(|r| r.0).collect();
pairs.sort_by(better_first);
Ok(DuplicateReport {
schema: crate::query::SCHEMA,
total,
pairs,
})
}
#[cfg(test)]
mod tests {
use super::{
DuplicateConfig, InferenceConfig, duplicates, embed, infer_edges, node_text, similarity,
};
use crate::{EdgeKind, FactSet, Node, NodeKind, Provenance, Store};
#[test]
fn node_text_includes_captured_content() {
let mut n = Node::new("file:docs/auth.md", NodeKind::Doc, "auth.md");
n.path = Some("docs/auth.md".to_owned());
n.meta = serde_json::json!({ "content": "token validation and OAuth flow" });
let text = node_text(&n);
assert!(text.contains("auth.md")); assert!(text.contains("auth")); assert!(text.contains("token validation and OAuth flow")); let plain = Node::new("sym:rust:a.rs#foo", NodeKind::Fn, "foo");
assert_eq!(node_text(&plain), "foo");
}
#[test]
fn embedding_is_deterministic_and_unit_length() {
let a = embed("Store::apply_factset");
let b = embed("Store::apply_factset");
let bits = |v: &[f32; super::DIM]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
assert_eq!(bits(&a), bits(&b), "embedding must be deterministic");
let norm: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 1e-5, "unit length, got {norm}");
}
#[test]
fn similar_names_score_higher_than_unrelated() {
let from = embed("edges_from");
let to = embed("edges_to");
let far = embed("cloudflare deployment pipeline");
let near = similarity(&from, &to);
let distant = similarity(&from, &far);
assert!(
near > distant,
"near {near} should exceed distant {distant}"
);
assert!(near > 0.3, "related names should share features: {near}");
}
#[test]
fn similarity_is_in_range() {
let a = embed("anything at all");
assert!((0.0..=1.0).contains(&similarity(&a, &a)));
assert!((0.0..=1.0).contains(&similarity(&a, &embed(""))));
}
#[test]
fn infers_confident_related_edges_and_skips_known_facts() {
let mut store = Store::open_in_memory().expect("store");
let facts = FactSet::new()
.with_node(Node::new(
"sym:rust:a.rs#edges_from",
NodeKind::Fn,
"edges_from",
))
.with_node(Node::new(
"sym:rust:a.rs#edges_to",
NodeKind::Fn,
"edges_to",
))
.with_node(Node::new(
"sym:rust:a.rs#edges_by_provenance",
NodeKind::Fn,
"edges_by_provenance",
))
.with_node(Node::new("sym:rust:a.rs#unrelated", NodeKind::Fn, "quokka"))
.with_edge(crate::Edge::derived(
"sym:rust:a.rs#edges_from",
"sym:rust:a.rs#edges_to",
EdgeKind::Calls,
));
store.apply_factset(&facts).expect("apply");
let inferred = infer_edges(&store, InferenceConfig::default()).expect("infer");
assert!(
!inferred.is_empty(),
"should infer at least one edge among the unconnected similar fns",
);
for e in &inferred {
assert_eq!(e.provenance, Provenance::Inferred);
assert_eq!(e.kind, EdgeKind::Related);
let c = e.confidence.expect("confidence present");
assert!((0.0..=1.0).contains(&c));
assert!(e.is_valid());
assert!(
!(e.src == "sym:rust:a.rs#edges_from" && e.dst == "sym:rust:a.rs#edges_to"),
"must not re-suggest an existing edge",
);
}
assert!(
inferred
.iter()
.all(|e| e.dst != "sym:rust:a.rs#unrelated" && e.src != "sym:rust:a.rs#unrelated"),
"unrelated node must not be inferred-linked",
);
let mut s2 = store;
s2.apply_factset(&FactSet {
nodes: vec![],
edges: inferred,
})
.expect("inferred edges satisfy store invariants");
assert!(
!s2.edges_by_provenance(Provenance::Inferred)
.expect("q")
.is_empty()
);
}
#[test]
fn re_inferring_is_authoritative_after_clearing() {
let mut store = Store::open_in_memory().expect("store");
let facts = FactSet::new()
.with_node(Node::new(
"sym:rust:a.rs#handle_read",
NodeKind::Fn,
"handle_read",
))
.with_node(Node::new(
"sym:rust:a.rs#handle_write",
NodeKind::Fn,
"handle_write",
))
.with_node(Node::new(
"sym:rust:a.rs#handler_pool",
NodeKind::Fn,
"handler_pool",
));
store.apply_factset(&facts).expect("apply");
let apply = |store: &mut Store, min: f64| {
let edges = infer_edges(
store,
InferenceConfig {
min_confidence: min,
top_k: 5,
},
)
.expect("infer");
store
.apply_factset(&FactSet {
nodes: vec![],
edges,
})
.expect("apply inferred");
};
apply(&mut store, 0.3);
let loose = store
.edges_by_provenance(Provenance::Inferred)
.expect("q")
.len();
assert!(loose > 0);
let removed = store
.delete_edges_by_provenance(Provenance::Inferred)
.expect("delete");
assert_eq!(
usize::try_from(removed).unwrap(),
loose,
"delete removes exactly the inferred edges"
);
assert!(
store
.edges_by_provenance(Provenance::Inferred)
.expect("q")
.is_empty()
);
apply(&mut store, 0.9);
let strict = store
.edges_by_provenance(Provenance::Inferred)
.expect("q")
.len();
assert!(
strict <= loose,
"stricter re-run must not accumulate: {strict} vs {loose}"
);
}
#[test]
fn duplicates_reports_exact_and_semantic_pairs() {
let mut store = Store::open_in_memory().expect("store");
let mut fa = Node::new("file:a.rs", NodeKind::File, "a.rs");
fa.blob_hash = Some("OID1".to_owned());
let mut fb = Node::new("file:copy/a.rs", NodeKind::File, "a.rs");
fb.blob_hash = Some("OID1".to_owned());
let body = "token validation and oauth login flow session refresh handling";
let mut da = Node::new("file:docs/x.md", NodeKind::Doc, "x.md");
da.path = Some("docs/x.md".to_owned());
da.meta = serde_json::json!({ "content": body });
let mut db = Node::new("file:docs/y.md", NodeKind::Doc, "y.md");
db.path = Some("docs/y.md".to_owned());
db.meta = serde_json::json!({ "content": body });
let mut solo = Node::new("file:docs/z.md", NodeKind::Doc, "z.md");
solo.path = Some("docs/z.md".to_owned());
solo.meta = serde_json::json!({ "content": "quokkas graze on rottnest island" });
store
.apply_factset(
&FactSet::new()
.with_node(fa)
.with_node(fb)
.with_node(da)
.with_node(db)
.with_node(solo),
)
.expect("apply");
let report = duplicates(&store, DuplicateConfig::default()).expect("dup");
assert_eq!(report.total, report.pairs.len(), "no truncation expected");
for p in &report.pairs {
assert!(p.a < p.b, "pair not canonically ordered: {p:?}");
assert!((0.0..=1.0).contains(&p.similarity));
}
let exacts: Vec<_> = report.pairs.iter().filter(|p| p.exact).collect();
assert_eq!(exacts.len(), 1, "one exact pair");
assert_eq!(
(exacts[0].a.as_str(), exacts[0].b.as_str()),
("file:a.rs", "file:copy/a.rs")
);
assert!(report.pairs[0].exact, "exact pairs sort first");
assert!(
report.pairs.iter().any(|p| p.a == "file:docs/x.md"
&& p.b == "file:docs/y.md"
&& !p.exact
&& p.similarity >= 0.9),
"semantic doc duplicate missing: {:?}",
report.pairs
);
assert!(
report
.pairs
.iter()
.all(|p| p.a != "file:docs/z.md" && p.b != "file:docs/z.md"),
"unrelated node must not be a duplicate",
);
}
#[test]
fn duplicates_bounds_memory_by_limit_and_counts_total() {
let mut store = Store::open_in_memory().expect("store");
let mut facts = FactSet::new();
for i in 0..6 {
let mut n = Node::new(
format!("file:docs/d{i}.md"),
NodeKind::Doc,
format!("d{i}.md"),
);
n.path = Some(format!("docs/d{i}.md"));
n.meta = serde_json::json!({ "content": "identical shared documentation body text" });
facts = facts.with_node(n);
}
store.apply_factset(&facts).expect("apply");
let report = duplicates(
&store,
DuplicateConfig {
min_similarity: 0.0,
limit: 2,
},
)
.expect("dup");
assert_eq!(report.total, 15, "all C(6,2) pairs counted");
assert_eq!(report.pairs.len(), 2, "output bounded by limit");
for p in &report.pairs {
assert!(p.a < p.b);
assert!((0.0..=1.0).contains(&p.similarity));
}
}
#[test]
fn top_k_bounds_edges_per_source() {
let mut store = Store::open_in_memory().expect("store");
let mut facts = FactSet::new();
for i in 0..10 {
facts = facts.with_node(Node::new(
format!("sym:rust:a.rs#handler{i}"),
NodeKind::Fn,
format!("handler{i}"),
));
}
store.apply_factset(&facts).expect("apply");
let cfg = InferenceConfig {
min_confidence: 0.3,
top_k: 2,
};
let inferred = infer_edges(&store, cfg).expect("infer");
for key in store.all_keys().expect("keys") {
let from_key = inferred.iter().filter(|e| e.src == key).count();
assert!(
from_key <= 2,
"top_k=2 bound exceeded for {key}: {from_key}"
);
}
}
}