use serde::{Deserialize, Serialize};
use crate::core::call_graph::ScopeMatch;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceGrade {
HeuristicStructural,
ResolvedStructural,
VerifiedSemantic,
}
impl EvidenceGrade {
pub fn from_scope(via: ScopeMatch) -> Self {
match via {
ScopeMatch::SameFile | ScopeMatch::UniqueImport | ScopeMatch::ModulePath => {
Self::ResolvedStructural
}
ScopeMatch::UniqueInProject => Self::HeuristicStructural,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::HeuristicStructural => "heuristic",
Self::ResolvedStructural => "resolved",
Self::VerifiedSemantic => "verified",
}
}
pub fn weight_factor(self) -> f64 {
match self {
Self::HeuristicStructural => 0.5,
Self::ResolvedStructural | Self::VerifiedSemantic => 1.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceOrigin {
#[default]
Enrichment,
ImpactIndex,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Contribution {
pub origin: EvidenceOrigin,
pub grade: EvidenceGrade,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backend: Option<String>,
pub sites: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EdgeEvidence {
pub v: u8,
pub grade: EvidenceGrade,
pub by: Vec<Contribution>,
}
impl EdgeEvidence {
pub const VERSION: u8 = 1;
pub fn new(
grade: EvidenceGrade,
origin: EvidenceOrigin,
backend: Option<String>,
sites: u32,
) -> Self {
Self {
v: Self::VERSION,
grade,
by: vec![Contribution {
origin,
grade,
backend,
sites,
}],
}
}
fn from_contributions(mut by: Vec<Contribution>) -> Option<Self> {
by.sort_by_key(|c| c.origin);
let grade = by.iter().map(|c| c.grade).max()?;
Some(Self {
v: Self::VERSION,
grade,
by,
})
}
pub fn primary(&self) -> Option<&Contribution> {
self.by
.iter()
.max_by(|a, b| a.grade.cmp(&b.grade).then(b.origin.cmp(&a.origin)))
}
pub fn has(&self, origin: EvidenceOrigin) -> bool {
self.by.iter().any(|c| c.origin == origin)
}
pub fn merge(existing: Option<Self>, incoming: Self) -> Self {
let mut by: Vec<Contribution> = existing
.map(|e| e.by)
.unwrap_or_default()
.into_iter()
.filter(|c| !incoming.has(c.origin))
.collect();
by.extend(incoming.by.iter().cloned());
Self::from_contributions(by).unwrap_or(incoming)
}
pub fn without(self, origin: EvidenceOrigin) -> Option<Self> {
Self::from_contributions(self.by.into_iter().filter(|c| c.origin != origin).collect())
}
pub fn to_metadata(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
}
pub fn from_metadata(metadata: Option<&str>) -> Option<Self> {
serde_json::from_str::<Self>(metadata?)
.ok()
.filter(|e| e.v == Self::VERSION)
}
pub fn weight_factor_of(metadata: Option<&str>) -> f64 {
Self::from_metadata(metadata).map_or(1.0, |e| e.grade.weight_factor())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn contributions_are_owned_per_producer() {
use EvidenceGrade::{ResolvedStructural, VerifiedSemantic};
use EvidenceOrigin::{Enrichment, ImpactIndex};
let verified = EdgeEvidence::new(VerifiedSemantic, Enrichment, Some("lsp:ra@1".into()), 1);
let structural = |origin| EdgeEvidence::new(ResolvedStructural, origin, None, 1);
let both = EdgeEvidence::merge(Some(verified.clone()), structural(ImpactIndex));
assert_eq!(both.grade, VerifiedSemantic, "no cross-producer downgrade");
assert_eq!(
both.primary().and_then(|c| c.backend.as_deref()),
Some("lsp:ra@1")
);
let recomputed = EdgeEvidence::merge(Some(both.clone()), structural(Enrichment));
assert_eq!(
recomputed.grade, ResolvedStructural,
"owner may weaken its own part"
);
assert!(recomputed.has(ImpactIndex));
let kept = both.clone().without(Enrichment).unwrap();
assert_eq!(
kept.grade, ResolvedStructural,
"impact still derives the edge"
);
assert!(
kept.without(ImpactIndex).is_none(),
"nobody derives it → delete"
);
assert_eq!(
EdgeEvidence::from_metadata(Some(&both.to_metadata())),
Some(both),
"deterministic round trip of the stored form"
);
}
#[test]
fn foreign_or_legacy_metadata_keeps_full_weight() {
let heuristic = EdgeEvidence::new(
EvidenceGrade::HeuristicStructural,
EvidenceOrigin::Enrichment,
None,
1,
);
assert_eq!(
EdgeEvidence::weight_factor_of(Some(&heuristic.to_metadata())),
0.5
);
assert_eq!(EdgeEvidence::weight_factor_of(None), 1.0);
assert_eq!(
EdgeEvidence::weight_factor_of(Some("kind=fn;exported")),
1.0
);
assert_eq!(
EdgeEvidence::weight_factor_of(Some(
r#"{"v":9,"grade":"verified_semantic","sites":1}"#
)),
1.0,
"unknown schema versions are ignored, not misread"
);
}
}