use crate::impact::ImpactReport;
use serde::{Deserialize, Serialize};
use ucm_graph_core::edge::ConfidenceTier;
use ucm_graph_core::graph::UcmGraph;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AmbiguityReport {
pub flags: Vec<AmbiguityFlag>,
pub total_low_confidence_edges: usize,
pub total_stale_edges: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AmbiguityFlag {
pub flag_type: AmbiguityType,
pub entity_id: Option<String>,
pub description: String,
pub evidence: Vec<String>,
pub recommendation: String,
pub severity: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AmbiguityType {
SourceConflict,
RequirementDrift,
MissingData,
LowConfidence,
StaleData,
}
pub fn detect_ambiguities(graph: &UcmGraph, confidence_threshold: f64) -> AmbiguityReport {
let mut flags = Vec::new();
let mut low_confidence_count = 0;
let mut stale_count = 0;
let entities = graph.all_entities();
for entity in &entities {
if let Ok(deps) = graph.dependencies(&entity.id) {
for (dep, edge) in &deps {
let tier = ConfidenceTier::from_score(edge.confidence);
if edge.confidence < confidence_threshold {
low_confidence_count += 1;
flags.push(AmbiguityFlag {
flag_type: AmbiguityType::LowConfidence,
entity_id: Some(entity.id.as_str().to_string()),
description: format!(
"Low confidence ({:.0}%) on relationship: {} → {}",
edge.confidence * 100.0,
entity.name,
dep.name
),
evidence: edge.evidence.iter()
.map(|e| format!("{}: {:.0}% ({})", e.description, e.confidence * 100.0, e.observed_at))
.collect(),
recommendation: format!(
"Verify the relationship between {} and {} — consider running tests or re-analyzing",
entity.name, dep.name
),
severity: tier.emoji().to_string(),
});
}
let decayed = edge.decayed_confidence();
if decayed < edge.confidence * 0.8 {
stale_count += 1;
flags.push(AmbiguityFlag {
flag_type: AmbiguityType::StaleData,
entity_id: Some(entity.id.as_str().to_string()),
description: format!(
"Stale relationship: {} → {} (base {:.0}%, decayed to {:.0}%)",
entity.name,
dep.name,
edge.confidence * 100.0,
decayed * 100.0
),
evidence: vec![format!(
"Last verified: {:?}, decay rate: {}",
edge.verified_at, edge.decay_rate
)],
recommendation: "Re-verify this relationship with fresh analysis".into(),
severity: ConfidenceTier::from_score(decayed).emoji().to_string(),
});
}
}
}
}
AmbiguityReport {
flags,
total_low_confidence_edges: low_confidence_count,
total_stale_edges: stale_count,
}
}
pub fn enrich_with_ambiguities(
report: &mut ImpactReport,
graph: &UcmGraph,
confidence_threshold: f64,
) {
let ambiguity_report = detect_ambiguities(graph, confidence_threshold);
for flag in ambiguity_report.flags {
report.ambiguities.push(crate::impact::AmbiguityEntry {
entity_id: flag.entity_id,
ambiguity_type: format!("{:?}", flag.flag_type),
description: flag.description,
sources: flag.evidence,
recommendation: flag.recommendation,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use ucm_graph_core::edge::*;
use ucm_graph_core::entity::*;
#[test]
fn test_detect_low_confidence() {
let mut graph = UcmGraph::new();
graph
.add_entity(UcmEntity::new(
EntityId::local("src/a.ts", "fnA"),
EntityKind::Function {
is_async: false,
parameter_count: 0,
return_type: None,
},
"fnA",
"src/a.ts",
"typescript",
DiscoverySource::StaticAnalysis,
))
.unwrap();
graph
.add_entity(UcmEntity::new(
EntityId::local("src/b.ts", "fnB"),
EntityKind::Function {
is_async: false,
parameter_count: 0,
return_type: None,
},
"fnB",
"src/b.ts",
"typescript",
DiscoverySource::StaticAnalysis,
))
.unwrap();
graph
.add_relationship(
&EntityId::local("src/a.ts", "fnA"),
&EntityId::local("src/b.ts", "fnB"),
UcmEdge::new(
RelationType::DependsOn,
DiscoverySource::HistoricalContext,
0.40,
"weak heuristic",
),
)
.unwrap();
let report = detect_ambiguities(&graph, 0.60);
assert!(report.total_low_confidence_edges > 0);
assert!(!report.flags.is_empty());
}
}