use std::collections::{HashMap, HashSet};
use chrono::Utc;
use codemem_core::{Edge, GraphNode, MemoryNode, MemoryType, NodeKind, RelationshipType};
use codemem_core::ScipConfig;
use super::{
is_import_ref, is_read_ref, is_write_ref, ScipDefinition, ScipReadResult, ROLE_IMPORT,
ROLE_READ_ACCESS, ROLE_WRITE_ACCESS,
};
#[derive(Debug, Clone, Default)]
pub struct ScipBuildResult {
pub nodes: Vec<GraphNode>,
pub edges: Vec<Edge>,
pub memories: Vec<(MemoryNode, String)>, pub ext_nodes_created: usize,
pub files_covered: HashSet<String>,
pub doc_memories_created: usize,
}
pub fn build_graph(
scip: &ScipReadResult,
namespace: Option<&str>,
config: &ScipConfig,
) -> ScipBuildResult {
let now = Utc::now();
let ns = namespace.map(|s| s.to_string());
let mut nodes = Vec::new();
let mut edges = Vec::new();
let mut memories: Vec<(MemoryNode, String)> = Vec::new();
let mut ext_nodes_created = 0;
let mut doc_memories_created = 0;
let path_filtered: Vec<&ScipDefinition> = scip
.definitions
.iter()
.filter(|d| is_source_path(&d.file_path) && !is_wildcard_module(&d.qualified_name))
.collect();
let mut source_defs: Vec<&ScipDefinition> = Vec::with_capacity(path_filtered.len());
let mut parsed_symbols: Vec<scip::types::Symbol> = Vec::with_capacity(path_filtered.len());
for def in &path_filtered {
let parsed = match scip::symbol::parse_symbol(&def.scip_symbol) {
Ok(p) => p,
Err(_) => {
source_defs.push(def);
parsed_symbols.push(scip::types::Symbol::default());
continue;
}
};
if is_noise_symbol(def, &parsed) {
continue;
}
source_defs.push(def);
parsed_symbols.push(parsed);
}
let mut symbol_to_qname: HashMap<&str, &str> = HashMap::new();
for def in &source_defs {
symbol_to_qname.insert(&def.scip_symbol, &def.qualified_name);
}
let mut created_node_ids: HashSet<String> = HashSet::new();
let mut created_edge_ids: HashSet<String> = HashSet::new();
let mut folded_to_parent: HashMap<String, String> = HashMap::new();
let mut folded_children: HashMap<String, Vec<(String, &'static str)>> = HashMap::new();
let def_chains: Vec<Vec<(String, NodeKind)>> = parsed_symbols
.iter()
.map(extract_containment_chain_from_parsed)
.collect();
for (def_idx, def) in source_defs.iter().enumerate() {
let kind = if def.is_test {
NodeKind::Test
} else {
def.kind
};
let tier3_category = match kind {
NodeKind::Field | NodeKind::Property => Some("fields"),
NodeKind::TypeParameter => Some("type_params"),
NodeKind::EnumVariant => Some("variants"),
_ => None,
};
if let Some(category) = tier3_category {
let chain = &def_chains[def_idx];
if chain.len() >= 2 {
let parent_qname = &chain[chain.len() - 2].0;
let leaf_name = def
.qualified_name
.rsplit([':', '.'])
.next()
.unwrap_or(&def.qualified_name);
folded_children
.entry(parent_qname.clone())
.or_default()
.push((leaf_name.to_string(), category));
folded_to_parent.insert(def.qualified_name.clone(), format!("sym:{parent_qname}"));
symbol_to_qname.insert(&def.scip_symbol, &def.qualified_name);
continue; }
}
let node_id = format!("sym:{}", def.qualified_name);
let mut payload = HashMap::new();
payload.insert(
"scip_symbol".to_string(),
serde_json::Value::String(def.scip_symbol.clone()),
);
payload.insert("line_start".to_string(), serde_json::json!(def.line_start));
payload.insert("line_end".to_string(), serde_json::json!(def.line_end));
payload.insert(
"file_path".to_string(),
serde_json::Value::String(def.file_path.clone()),
);
if def.is_test {
payload.insert("is_test".to_string(), serde_json::json!(true));
}
if def.is_generated {
payload.insert("is_generated".to_string(), serde_json::json!(true));
}
if let Some(type_sig) = def.documentation.first() {
payload.insert(
"type_signature".to_string(),
serde_json::Value::String(type_sig.clone()),
);
}
payload.insert(
"source".to_string(),
serde_json::Value::String("scip".to_string()),
);
created_node_ids.insert(node_id.clone());
nodes.push(GraphNode {
id: node_id.clone(),
kind,
label: def.qualified_name.clone(),
payload,
centrality: 0.0,
memory_id: None,
namespace: ns.clone(),
valid_from: None,
valid_to: None,
});
if config.hierarchical_containment {
let chain = &def_chains[def_idx];
let file_node_id = format!("file:{}", def.file_path);
if chain.len() <= 1 {
let edge_id = format!("contains:{file_node_id}->{node_id}");
if created_edge_ids.insert(edge_id.clone()) {
edges.push(Edge {
id: edge_id,
src: file_node_id,
dst: node_id.clone(),
relationship: RelationshipType::Contains,
weight: 0.1,
properties: scip_edge_properties(),
created_at: now,
valid_from: Some(now),
valid_to: None,
});
}
} else {
for (i, (seg_qname, seg_kind)) in chain.iter().enumerate() {
let seg_node_id = format!("sym:{seg_qname}");
if seg_qname != &def.qualified_name
&& created_node_ids.insert(seg_node_id.clone())
{
let mut syn_payload = HashMap::new();
syn_payload.insert(
"source".to_string(),
serde_json::Value::String("scip-synthetic".to_string()),
);
syn_payload.insert(
"file_path".to_string(),
serde_json::Value::String(def.file_path.clone()),
);
nodes.push(GraphNode {
id: seg_node_id.clone(),
kind: *seg_kind,
label: seg_qname.clone(),
payload: syn_payload,
centrality: 0.0,
memory_id: None,
namespace: ns.clone(),
valid_from: None,
valid_to: None,
});
}
let parent_id = if i == 0 {
file_node_id.clone()
} else {
format!("sym:{}", chain[i - 1].0)
};
let edge_id = format!("contains:{parent_id}->{seg_node_id}");
if created_edge_ids.insert(edge_id.clone()) {
edges.push(Edge {
id: edge_id,
src: parent_id,
dst: seg_node_id,
relationship: RelationshipType::Contains,
weight: 0.1,
properties: scip_edge_properties(),
created_at: now,
valid_from: Some(now),
valid_to: None,
});
}
}
}
} else {
let file_node_id = format!("file:{}", def.file_path);
edges.push(Edge {
id: format!("contains:{file_node_id}->{node_id}"),
src: file_node_id,
dst: node_id.clone(),
relationship: RelationshipType::Contains,
weight: 0.1,
properties: scip_edge_properties(),
created_at: now,
valid_from: Some(now),
valid_to: None,
});
}
if config.store_docs_as_memories && !def.documentation.is_empty() {
let doc_text = def.documentation.join("\n");
let mem_id = format!("scip-doc:{}", def.qualified_name);
let memory = MemoryNode {
id: mem_id,
content: doc_text,
memory_type: MemoryType::Context,
importance: 0.4,
confidence: 1.0,
access_count: 0,
content_hash: String::new(), tags: vec!["scip-doc".to_string(), "auto-generated".to_string()],
metadata: HashMap::new(),
namespace: ns.clone(),
session_id: None,
repo: None,
git_ref: None,
expires_at: None,
created_at: now,
updated_at: now,
last_accessed_at: now,
};
memories.push((memory, node_id.clone()));
doc_memories_created += 1;
}
for rel in &def.relationships {
if rel.target_symbol.is_empty() {
continue;
}
let target_node_id =
if let Some(qname) = symbol_to_qname.get(rel.target_symbol.as_str()) {
format!("sym:{qname}")
} else {
match parse_external_node_id(&rel.target_symbol) {
Some(ext_id) => ext_id,
None => continue,
}
};
if rel.is_implementation {
edges.push(Edge {
id: format!("implements:{node_id}->{target_node_id}"),
src: node_id.clone(),
dst: target_node_id.clone(),
relationship: RelationshipType::Implements,
weight: 0.8,
properties: scip_edge_properties(),
created_at: now,
valid_from: Some(now),
valid_to: None,
});
if def.kind == NodeKind::Method {
edges.push(Edge {
id: format!("overrides:{node_id}->{target_node_id}"),
src: node_id.clone(),
dst: target_node_id.clone(),
relationship: RelationshipType::Overrides,
weight: 0.8,
properties: scip_edge_properties(),
created_at: now,
valid_from: Some(now),
valid_to: None,
});
}
}
if rel.is_type_definition {
edges.push(Edge {
id: format!("typedef:{node_id}->{target_node_id}"),
src: node_id.clone(),
dst: target_node_id.clone(),
relationship: RelationshipType::TypeDefinition,
weight: 0.6,
properties: scip_edge_properties(),
created_at: now,
valid_from: Some(now),
valid_to: None,
});
}
if rel.is_reference && !rel.is_implementation {
edges.push(Edge {
id: format!("inherits:{node_id}->{target_node_id}"),
src: node_id.clone(),
dst: target_node_id,
relationship: RelationshipType::Inherits,
weight: 0.8,
properties: scip_edge_properties(),
created_at: now,
valid_from: Some(now),
valid_to: None,
});
}
}
}
for node in &mut nodes {
let qname = node.label.as_str();
if let Some(children) = folded_children.get(qname) {
let mut fields = Vec::new();
let mut type_params = Vec::new();
let mut variants = Vec::new();
for (name, category) in children {
match *category {
"fields" => fields.push(serde_json::Value::String(name.clone())),
"type_params" => type_params.push(serde_json::Value::String(name.clone())),
"variants" => variants.push(serde_json::Value::String(name.clone())),
_ => {}
}
}
if !fields.is_empty() {
node.payload
.insert("fields".to_string(), serde_json::Value::Array(fields));
}
if !type_params.is_empty() {
node.payload.insert(
"type_params".to_string(),
serde_json::Value::Array(type_params),
);
}
if !variants.is_empty() {
node.payload
.insert("variants".to_string(), serde_json::Value::Array(variants));
}
}
}
if config.create_external_nodes {
let mut pkg_nodes_created: HashSet<String> = HashSet::new();
for ext in &scip.externals {
if ext.package_manager.is_empty() || ext.package_name.is_empty() {
continue;
}
let node_id = format!("pkg:{}:{}", ext.package_manager, ext.package_name);
if !pkg_nodes_created.insert(node_id.clone()) {
continue; }
let mut payload = HashMap::new();
payload.insert(
"package_manager".to_string(),
serde_json::Value::String(ext.package_manager.clone()),
);
payload.insert(
"package_name".to_string(),
serde_json::Value::String(ext.package_name.clone()),
);
payload.insert(
"package_version".to_string(),
serde_json::Value::String(ext.package_version.clone()),
);
payload.insert(
"source".to_string(),
serde_json::Value::String("scip".to_string()),
);
nodes.push(GraphNode {
id: node_id,
kind: NodeKind::External,
label: ext.package_name.clone(),
payload,
centrality: 0.0,
memory_id: None,
namespace: ns.clone(),
valid_from: None,
valid_to: None,
});
ext_nodes_created += 1;
}
}
let mut defs_by_file: HashMap<&str, Vec<&ScipDefinition>> = HashMap::new();
for def in &source_defs {
if folded_to_parent.contains_key(&def.qualified_name) {
continue;
}
defs_by_file
.entry(def.file_path.as_str())
.or_default()
.push(def);
}
let source_refs: Vec<&super::ScipReference> = scip
.references
.iter()
.filter(|r| is_source_path(&r.file_path))
.collect();
let mut ref_counts: HashMap<(&str, &str), usize> = HashMap::new();
for r in &source_refs {
*ref_counts
.entry((&r.scip_symbol, &r.file_path))
.or_insert(0) += 1;
}
let symbol_to_kind: HashMap<&str, NodeKind> = source_defs
.iter()
.map(|d| (d.scip_symbol.as_str(), d.kind))
.collect();
for r in &source_refs {
let count = ref_counts
.get(&(r.scip_symbol.as_str(), r.file_path.as_str()))
.copied()
.unwrap_or(0);
let target_kind = symbol_to_kind.get(r.scip_symbol.as_str()).copied();
let limit = match target_kind {
Some(NodeKind::Module) => config.fan_out_limits.module,
Some(NodeKind::Function) => config.fan_out_limits.function,
Some(NodeKind::Method) => config.fan_out_limits.method,
Some(NodeKind::Class | NodeKind::Trait | NodeKind::Interface) => {
config.fan_out_limits.class
}
_ => config.max_references_per_symbol,
};
if count > limit {
continue;
}
if crate::index::blocklist::is_blocked_call_scip(&r.scip_symbol) {
continue;
}
let mut target_node_id = if let Some(qname) = symbol_to_qname.get(r.scip_symbol.as_str()) {
format!("sym:{qname}")
} else {
match parse_external_node_id(&r.scip_symbol) {
Some(ext_id) => ext_id,
None => continue,
}
};
if let Some(qname) = symbol_to_qname.get(r.scip_symbol.as_str()) {
if let Some(parent_id) = folded_to_parent.get(*qname) {
target_node_id = parent_id.clone();
}
}
let mut source_node_id = find_enclosing_def_indexed(&defs_by_file, &r.file_path, r.line)
.map(|def| format!("sym:{}", def.qualified_name))
.unwrap_or_else(|| format!("file:{}", r.file_path));
if let Some(parent_id) = source_node_id
.strip_prefix("sym:")
.and_then(|qn| folded_to_parent.get(qn))
{
source_node_id = parent_id.clone();
}
if source_node_id == target_node_id {
continue;
}
let semantic_mask = ROLE_IMPORT | ROLE_WRITE_ACCESS | ROLE_READ_ACCESS;
let is_scip_go_generic = r.role_bitmask & semantic_mask == ROLE_READ_ACCESS;
let (rel, weight) = if is_import_ref(r.role_bitmask) {
(RelationshipType::Imports, 0.5)
} else if is_write_ref(r.role_bitmask) {
(RelationshipType::Writes, 0.4)
} else if is_read_ref(r.role_bitmask) && !is_scip_go_generic {
(RelationshipType::Reads, 0.3)
} else {
match target_kind {
Some(NodeKind::Class | NodeKind::Interface | NodeKind::Trait | NodeKind::Type) => {
(RelationshipType::DependsOn, 0.3)
}
Some(NodeKind::Module | NodeKind::Package) => (RelationshipType::Imports, 0.5),
Some(NodeKind::Constant) => (RelationshipType::Reads, 0.3),
_ => (RelationshipType::Calls, 1.0),
}
};
let edge_prefix = rel.to_string().to_lowercase();
edges.push(Edge {
id: format!(
"{edge_prefix}:{source_node_id}->{target_node_id}:{}:{}",
r.file_path, r.line
),
src: source_node_id.clone(),
dst: target_node_id.clone(),
relationship: rel,
weight,
properties: scip_edge_properties(),
created_at: now,
valid_from: Some(now),
valid_to: None,
});
if !is_import_ref(r.role_bitmask) {
let is_type_target = matches!(
target_kind,
Some(
NodeKind::Class
| NodeKind::Trait
| NodeKind::Interface
| NodeKind::Type
| NodeKind::Enum
)
);
if is_type_target {
edges.push(Edge {
id: format!(
"depends:{source_node_id}->{target_node_id}:{}:{}",
r.file_path, r.line
),
src: source_node_id,
dst: target_node_id,
relationship: RelationshipType::DependsOn,
weight: 0.7,
properties: scip_edge_properties(),
created_at: now,
valid_from: Some(now),
valid_to: None,
});
}
}
}
let mut seen_edge_ids = HashSet::new();
edges.retain(|e| seen_edge_ids.insert(e.id.clone()));
if config.collapse_intra_class_edges && config.hierarchical_containment {
let mut child_to_parent: HashMap<&str, &str> = HashMap::new();
for edge in &edges {
if edge.relationship == RelationshipType::Contains
&& edge.src.starts_with("sym:")
&& edge.dst.starts_with("sym:")
{
child_to_parent.insert(&edge.dst, &edge.src);
}
}
let node_kind_map: HashMap<&str, NodeKind> =
nodes.iter().map(|n| (n.id.as_str(), n.kind)).collect();
let mut intra_class_counts: HashMap<String, Vec<(String, String)>> = HashMap::new();
let mut intra_edge_ids: HashSet<String> = HashSet::new();
for edge in &edges {
if !matches!(
edge.relationship,
RelationshipType::Calls | RelationshipType::Reads | RelationshipType::Writes
) {
continue;
}
let src_parent = child_to_parent.get(edge.src.as_str());
let dst_parent = child_to_parent.get(edge.dst.as_str());
if let (Some(sp), Some(dp)) = (src_parent, dst_parent) {
let parent_kind = node_kind_map.get(sp).copied();
let is_class_like = matches!(
parent_kind,
Some(NodeKind::Class | NodeKind::Trait | NodeKind::Interface | NodeKind::Enum)
);
if sp == dp && is_class_like {
let src_leaf = edge.src.rsplit([':', '.']).next().unwrap_or(&edge.src);
let dst_leaf = edge.dst.rsplit([':', '.']).next().unwrap_or(&edge.dst);
intra_class_counts
.entry(sp.to_string())
.or_default()
.push((src_leaf.to_string(), dst_leaf.to_string()));
intra_edge_ids.insert(edge.id.clone());
}
}
}
if !intra_edge_ids.is_empty() {
edges.retain(|e| !intra_edge_ids.contains(&e.id));
for node in &mut nodes {
if let Some(calls) = intra_class_counts.get(&node.id) {
let call_entries: Vec<serde_json::Value> = calls
.iter()
.map(|(from, to)| serde_json::json!({"from": from, "to": to}))
.collect();
node.payload.insert(
"intra_class_calls".to_string(),
serde_json::Value::Array(call_entries),
);
}
}
}
}
let files_covered: HashSet<String> = scip.covered_files.iter().cloned().collect();
let existing_node_ids: HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
let mut missing_ids: HashSet<String> = HashSet::new();
for edge in &edges {
if !existing_node_ids.contains(edge.src.as_str()) {
missing_ids.insert(edge.src.clone());
}
if !existing_node_ids.contains(edge.dst.as_str()) {
missing_ids.insert(edge.dst.clone());
}
}
for missing_id in &missing_ids {
let (kind, label) = if let Some(file_path) = missing_id.strip_prefix("file:") {
(NodeKind::File, file_path.to_string())
} else if let Some(pkg_rest) = missing_id.strip_prefix("pkg:") {
let label = pkg_rest.rsplit(':').next().unwrap_or(pkg_rest).to_string();
ext_nodes_created += 1;
(NodeKind::External, label)
} else if missing_id.starts_with("ext:") {
let label = missing_id
.rsplit(':')
.next()
.unwrap_or(missing_id)
.to_string();
ext_nodes_created += 1;
(NodeKind::External, label)
} else if let Some(qname) = missing_id.strip_prefix("sym:") {
let label = qname.rsplit([':', '.']).next().unwrap_or(qname).to_string();
(NodeKind::Method, label)
} else {
continue; };
let mut payload = HashMap::new();
payload.insert(
"source".to_string(),
serde_json::Value::String("scip".to_string()),
);
nodes.push(GraphNode {
id: missing_id.clone(),
kind,
label,
payload,
centrality: 0.0,
memory_id: None,
namespace: ns.clone(),
valid_from: None,
valid_to: None,
});
}
let valid_node_ids: HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
let edge_count_before = edges.len();
edges.retain(|e| {
if is_stdlib_package(&e.dst) {
return false;
}
let src_ok = valid_node_ids.contains(e.src.as_str())
|| e.src.starts_with("file:")
|| e.src.starts_with("pkg:");
let dst_ok = valid_node_ids.contains(e.dst.as_str())
|| e.dst.starts_with("file:")
|| e.dst.starts_with("pkg:");
src_ok && dst_ok
});
let edges_dropped = edge_count_before - edges.len();
if edges_dropped > 0 {
tracing::debug!("Dropped {edges_dropped} SCIP edges referencing filtered/stdlib nodes");
}
ScipBuildResult {
nodes,
edges,
memories,
ext_nodes_created,
files_covered,
doc_memories_created,
}
}
fn find_enclosing_def_indexed<'a>(
defs_by_file: &HashMap<&str, Vec<&'a ScipDefinition>>,
file_path: &str,
line: u32,
) -> Option<&'a ScipDefinition> {
defs_by_file
.get(file_path)?
.iter()
.filter(|d| d.line_start <= line && d.line_end >= line)
.min_by_key(|d| d.line_end - d.line_start)
.copied()
}
fn is_source_path(path: &str) -> bool {
if path.starts_with('/') || path.starts_with("..") {
return false;
}
let reject_dirs = [
"node_modules/",
".venv/",
"site-packages/",
"__pycache__/",
".gradle/",
".m2/",
"/go-build/",
"vendor/", "dist/",
"build/",
];
if reject_dirs.iter().any(|r| path.contains(r)) {
return false;
}
if path.contains("__generated__") || path.contains(".generated.") {
return false;
}
if path.ends_with(".bundle.js")
|| path.ends_with(".min.js")
|| path.ends_with(".min.css")
|| path.contains("/webpack_bundles/")
{
return false;
}
true
}
fn is_noise_symbol(def: &ScipDefinition, parsed: &scip::types::Symbol) -> bool {
if def.is_generated {
return true;
}
if parsed
.descriptors
.iter()
.any(|d| d.name.contains("typeLiteral"))
{
return true;
}
let leaf = match parsed.descriptors.last() {
Some(d) => d,
None => return false,
};
use scip::types::descriptor::Suffix;
match leaf.suffix.enum_value() {
Ok(Suffix::Parameter | Suffix::TypeParameter | Suffix::Local) => return true,
Ok(Suffix::Meta) => return true,
Ok(Suffix::Term) => {
let parent_suffix = parsed
.descriptors
.iter()
.rev()
.nth(1)
.and_then(|d| d.suffix.enum_value().ok());
if matches!(parent_suffix, Some(Suffix::Method)) {
return true;
}
if has_trailing_digits(&leaf.name) {
return true;
}
}
_ => {}
}
false
}
fn has_trailing_digits(name: &str) -> bool {
name.len() > 1 && name.ends_with(|c: char| c.is_ascii_digit())
}
fn is_stdlib_package(node_id: &str) -> bool {
matches!(
node_id,
"pkg:npm:typescript"
| "pkg:npm:@types/node"
| "pkg:python:python-stdlib"
| "pkg:python:typing_extensions"
| "pkg:python:builtins"
| "pkg:maven:java.lang"
| "pkg:maven:java.util"
| "pkg:maven:java.io"
| "pkg:go:builtin"
| "pkg:go:fmt"
| "pkg:cargo:std"
| "pkg:cargo:core"
| "pkg:cargo:alloc"
)
}
fn parse_external_node_id(scip_symbol: &str) -> Option<String> {
let parsed = scip::symbol::parse_symbol(scip_symbol).ok()?;
let package = parsed.package.as_ref()?;
if package.manager.is_empty() || package.name.is_empty() {
return None;
}
Some(format!("pkg:{}:{}", package.manager, package.name))
}
fn extract_containment_chain_from_parsed(parsed: &scip::types::Symbol) -> Vec<(String, NodeKind)> {
let scheme = &parsed.scheme;
let sep = if scheme == "rust-analyzer" || scheme == "lsif-clang" {
"::"
} else {
"."
};
let mut chain = Vec::new();
let mut cumulative_parts: Vec<&str> = Vec::new();
let leaf_kind = super::infer_kind_from_parsed(parsed);
for desc in &parsed.descriptors {
if desc.name.is_empty() {
continue;
}
cumulative_parts.push(&desc.name);
let qname = cumulative_parts.join(sep);
let seg_kind = if cumulative_parts.len() < parsed.descriptors.len() {
use scip::types::descriptor::Suffix;
match desc.suffix.enum_value() {
Ok(Suffix::Package | Suffix::Namespace) => NodeKind::Module,
Ok(Suffix::Type) => NodeKind::Class,
Ok(Suffix::Method) => NodeKind::Method,
Ok(Suffix::Macro) => NodeKind::Macro,
_ => NodeKind::Module,
}
} else {
leaf_kind
};
chain.push((qname, seg_kind));
}
chain
}
fn is_wildcard_module(qualified_name: &str) -> bool {
qualified_name.contains("'*")
}
const SCIP_BASE_CONFIDENCE: f64 = 0.15;
fn scip_edge_properties() -> HashMap<String, serde_json::Value> {
use std::sync::LazyLock;
static PROPS: LazyLock<HashMap<String, serde_json::Value>> = LazyLock::new(|| {
let mut props = HashMap::new();
props.insert(
"source".to_string(),
serde_json::Value::String("scip".to_string()),
);
props.insert(
"confidence".to_string(),
serde_json::json!(SCIP_BASE_CONFIDENCE),
);
props.insert("source_layers".to_string(), serde_json::json!(["scip"]));
props
});
PROPS.clone()
}
#[cfg(test)]
#[path = "../tests/scip_graph_builder_tests.rs"]
mod tests;