use okf_core::{Bundle, ConceptId, ResourceKind};
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NodeKind {
Concept,
Computation,
Phantom,
Source,
}
#[derive(Clone, Debug)]
pub struct GraphNode {
pub key: String,
pub label: String,
pub kind: NodeKind,
pub id: Option<ConceptId>,
pub degree: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EdgeKind {
Link,
Derivation,
Broken,
Source,
}
#[derive(Clone, Copy, Debug)]
pub struct GraphEdge {
pub from: usize,
pub to: usize,
pub kind: EdgeKind,
}
#[derive(Clone, Debug, Default)]
pub struct GraphModel {
pub nodes: Vec<GraphNode>,
pub edges: Vec<GraphEdge>,
}
impl GraphModel {
#[must_use]
pub fn build(bundle: &Bundle) -> Self {
let mut nodes: Vec<GraphNode> = Vec::new();
let mut index: HashMap<String, usize> = HashMap::new();
for concept in bundle.concepts() {
let key = concept.id.to_string();
let kind = if concept.attested_computation().is_some() {
NodeKind::Computation
} else {
NodeKind::Concept
};
index.insert(key.clone(), nodes.len());
nodes.push(GraphNode {
label: key.clone(),
key,
kind,
id: Some(concept.id.clone()),
degree: 0,
});
}
let mut edges: Vec<GraphEdge> = Vec::new();
for concept in bundle.concepts() {
let from = index[&concept.id.to_string()];
for link in bundle.links_from(&concept.id) {
if link.exists {
if let Some(&to) = index.get(&link.target.to_string()) {
edges.push(GraphEdge {
from,
to,
kind: EdgeKind::Link,
});
}
} else {
let key = format!("✗{}", link.target);
let to = *index.entry(key.clone()).or_insert_with(|| {
nodes.push(GraphNode {
label: link.target.to_string(),
key,
kind: NodeKind::Phantom,
id: None,
degree: 0,
});
nodes.len() - 1
});
edges.push(GraphEdge {
from,
to,
kind: EdgeKind::Broken,
});
}
}
for source in bundle.sources_of(&concept.id) {
if let Some(target) = &source.concept {
if let Some(&to) = index.get(&target.to_string()) {
edges.push(GraphEdge {
from,
to,
kind: EdgeKind::Derivation,
});
}
} else if matches!(
source.source.resource_kind(),
ResourceKind::Url | ResourceKind::Scope | ResourceKind::Path
) {
let label = source.source.label().to_string();
let key = format!("src:{label}");
let to = *index.entry(key.clone()).or_insert_with(|| {
nodes.push(GraphNode {
label,
key,
kind: NodeKind::Source,
id: None,
degree: 0,
});
nodes.len() - 1
});
edges.push(GraphEdge {
from,
to,
kind: EdgeKind::Source,
});
}
}
}
for edge in &edges {
nodes[edge.from].degree += 1;
nodes[edge.to].degree += 1;
}
Self { nodes, edges }
}
#[must_use]
pub fn node_of(&self, id: &ConceptId) -> Option<usize> {
self.nodes.iter().position(|n| n.id.as_ref() == Some(id))
}
#[must_use]
pub fn neighborhood(&self, center: usize, k: usize) -> Vec<bool> {
let mut included = vec![false; self.nodes.len()];
if center >= self.nodes.len() {
return included;
}
included[center] = true;
let mut frontier = vec![center];
for _ in 0..k {
let mut next = Vec::new();
for edge in &self.edges {
for (a, b) in [(edge.from, edge.to), (edge.to, edge.from)] {
if frontier.contains(&a) && !included[b] {
included[b] = true;
next.push(b);
}
}
}
frontier = next;
}
included
}
}