Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions src/indexer/graphrag/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,23 +192,38 @@ impl<'a> DatabaseOperations<'a> {
let desc_array = extract_string_column(&rel_batch, "description")?;
let conf_array = extract_f32_column(&rel_batch, "confidence")?;

// Process each relationship
// Deduplicate relationships by (source, target, type) triple —
// batch writes can produce duplicates across incremental flushes
let mut seen = std::collections::HashSet::new();
let mut dedup_count = 0usize;
for i in 0..rel_batch.num_rows() {
let source = source_array.value(i);
let target = target_array.value(i);
let rel_type = type_array.value(i);
let key = (source.to_string(), target.to_string(), rel_type.to_string());
if !seen.insert(key) {
dedup_count += 1;
continue;
}
let relationship = CodeRelationship {
source: source_array.value(i).to_string(),
target: target_array.value(i).to_string(),
relation_type: type_array
.value(i)
source: source.to_string(),
target: target.to_string(),
relation_type: rel_type
.parse()
.unwrap_or(crate::indexer::graphrag::types::RelationType::Imports),
description: desc_array.value(i).to_string(),
confidence: conf_array.value(i),
weight: 1.0, // Default weight for legacy relationships
weight: 1.0,
};

// Add to graph
graph.relationships.push(relationship);
}
if dedup_count > 0 && !quiet {
println!(
" Deduplicated {} → {} unique relationships",
rel_batch.num_rows(),
graph.relationships.len()
);
}
}

if !graph.nodes.is_empty() && !quiet {
Expand Down
77 changes: 46 additions & 31 deletions src/indexer/graphrag/relationships.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,36 @@ impl RelationshipDiscovery {

for source_file in new_files {
// 1. Import/Export relationships (high confidence)
for import in &source_file.imports {
for target_file in all_nodes {
if target_file.id == source_file.id {
continue;
}
// For markdown files, use proper path-based import resolution
// (the symbol-matching approach doesn't work for file path imports)
if source_file.language == "markdown" {
Self::discover_import_relationships(source_file, all_nodes, &mut relationships);
} else {
for import in &source_file.imports {
for target_file in all_nodes {
if target_file.id == source_file.id {
continue;
}

// Check if target exports what source imports
if target_file
.exports
.iter()
.any(|exp| symbols_match(import, exp))
|| target_file
.symbols
// Check if target exports what source imports
if target_file
.exports
.iter()
.any(|sym| symbols_match(import, sym))
{
relationships.push(CodeRelationship {
source: source_file.id.clone(),
target: target_file.id.clone(),
relation_type: crate::indexer::graphrag::types::RelationType::Imports,
description: format!("Imports {} from {}", import, target_file.name),
confidence: 0.9,
weight: 1.0,
});
.any(|exp| symbols_match(import, exp))
|| target_file
.symbols
.iter()
.any(|sym| symbols_match(import, sym))
{
relationships.push(CodeRelationship {
source: source_file.id.clone(),
target: target_file.id.clone(),
relation_type: crate::indexer::graphrag::types::RelationType::Imports,
description: format!("Imports {} from {}", import, target_file.name),
confidence: 0.9,
weight: 1.0,
});
}
}
}
}
Expand Down Expand Up @@ -193,16 +199,26 @@ impl RelationshipDiscovery {
{
// Find the target node
if let Some(target_node) = file_map.get(&resolved_path) {
// Create semantic import relationship
// Use References for markdown cross-links, Imports for code
let rel_type = if source_file.language == "markdown" {
crate::indexer::graphrag::types::RelationType::References
} else {
crate::indexer::graphrag::types::RelationType::Imports
};
let description_prefix = if source_file.language == "markdown" {
"References"
} else {
"Direct import"
};
relationships.push(CodeRelationship {
source: source_file.id.clone(),
target: target_node.id.clone(),
relation_type: crate::indexer::graphrag::types::RelationType::Imports,
relation_type: rel_type,
description: format!(
"Direct import: {} -> {}",
import_path, resolved_path
"{}: {} -> {}",
description_prefix, import_path, resolved_path
),
confidence: 0.95, // High confidence for resolved imports
confidence: 0.95,
weight: 1.0,
});

Expand Down Expand Up @@ -482,10 +498,9 @@ impl RelationshipDiscovery {
|| relative_path.contains(".test.")
{
"test_file".to_string()
} else if relative_path.ends_with(".md")
|| relative_path.ends_with(".txt")
|| relative_path.ends_with(".rst")
{
} else if relative_path.ends_with(".md") || relative_path.ends_with(".markdown") {
"document_file".to_string()
} else if relative_path.ends_with(".txt") || relative_path.ends_with(".rst") {
"documentation".to_string()
} else if relative_path.contains("/config") || relative_path.contains(".config") {
"config_file".to_string()
Expand Down
97 changes: 97 additions & 0 deletions src/indexer/graphrag/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -954,4 +954,101 @@ def _private_function():
extract_imports_exports_recursive(child, contents, lang_impl, all_imports, all_exports);
}
}

/// Integration test: verify discover_relationships_efficiently produces
/// References edges for markdown nodes with file-path imports.
/// This is the exact code path used at runtime (not discover_import_relationships directly).
#[tokio::test]
async fn test_markdown_references_via_efficient_discovery() {
use crate::indexer::graphrag::types::RelationType;

// Source: adapters-integrations.md imports credit-suite.md and credit-accounts.md
let source = CodeNode {
id: "projects/docs/core/adapters-integrations.md".to_string(),
name: "adapters-integrations".to_string(),
kind: "document_file".to_string(),
path: "projects/docs/core/adapters-integrations.md".to_string(),
description: String::new(),
symbols: vec![],
imports: vec![
"credit-suite.md".to_string(), // same-dir
"../intro/credit-accounts.md".to_string(), // parent-dir
],
exports: vec!["Adapters".to_string()],
functions: vec![],
hash: "aaa".to_string(),
embedding: vec![],
size_lines: 50,
language: "markdown".to_string(),
};

let target1 = CodeNode {
id: "projects/docs/core/credit-suite.md".to_string(),
name: "credit-suite".to_string(),
kind: "document_file".to_string(),
path: "projects/docs/core/credit-suite.md".to_string(),
description: String::new(),
symbols: vec![],
imports: vec![],
exports: vec!["Credit Suite".to_string()],
functions: vec![],
hash: "bbb".to_string(),
embedding: vec![],
size_lines: 100,
language: "markdown".to_string(),
};

let target2 = CodeNode {
id: "projects/docs/intro/credit-accounts.md".to_string(),
name: "credit-accounts".to_string(),
kind: "document_file".to_string(),
path: "projects/docs/intro/credit-accounts.md".to_string(),
description: String::new(),
symbols: vec![],
imports: vec![],
exports: vec!["Credit Accounts".to_string()],
functions: vec![],
hash: "ccc".to_string(),
embedding: vec![],
size_lines: 80,
language: "markdown".to_string(),
};

let all_nodes = vec![source.clone(), target1.clone(), target2.clone()];
let new_files = vec![source.clone()];

// Call the SAME function used at runtime
let relationships = RelationshipDiscovery::discover_relationships_efficiently(
&new_files,
&all_nodes,
)
.await
.expect("relationship discovery should succeed");

// Find References relationships (not just sibling_module)
let refs: Vec<_> = relationships
.iter()
.filter(|r| r.relation_type == RelationType::References)
.collect();

assert!(
refs.len() >= 2,
"Expected at least 2 References relationships, got {}: {:?}",
refs.len(),
refs.iter().map(|r| format!("{} -> {}", r.source, r.target)).collect::<Vec<_>>()
);

// Verify specific edges
let has_suite = refs.iter().any(|r| {
r.source == "projects/docs/core/adapters-integrations.md"
&& r.target == "projects/docs/core/credit-suite.md"
});
assert!(has_suite, "Should have reference to credit-suite.md");

let has_accounts = refs.iter().any(|r| {
r.source == "projects/docs/core/adapters-integrations.md"
&& r.target == "projects/docs/intro/credit-accounts.md"
});
assert!(has_accounts, "Should have reference to credit-accounts.md");
}
}
23 changes: 23 additions & 0 deletions src/indexer/graphrag/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pub enum RelationType {
StrategyPattern,
/// Adapter pattern (interface adaptation)
AdapterPattern,
/// Document cross-reference via markdown link
References,

// Low importance - Organizational relationships (weight: 0.3)
/// Files in the same directory
Expand All @@ -85,6 +87,7 @@ impl RelationType {
| Self::ObserverPattern
| Self::StrategyPattern
| Self::AdapterPattern => 0.8,
Self::References => 0.6,

// Low importance - organizational structure
Self::SiblingModule | Self::ParentModule | Self::ChildModule => 0.3,
Expand All @@ -105,6 +108,7 @@ impl RelationType {
Self::ObserverPattern => "observer_pattern",
Self::StrategyPattern => "strategy_pattern",
Self::AdapterPattern => "adapter_pattern",
Self::References => "references",
Self::SiblingModule => "sibling_module",
Self::ParentModule => "parent_module",
Self::ChildModule => "child_module",
Expand All @@ -129,6 +133,7 @@ impl FromStr for RelationType {
"observer_pattern" => Self::ObserverPattern,
"strategy_pattern" => Self::StrategyPattern,
"adapter_pattern" => Self::AdapterPattern,
"references" => Self::References,
"sibling_module" => Self::SiblingModule,
"parent_module" => Self::ParentModule,
"child_module" => Self::ChildModule,
Expand Down Expand Up @@ -264,6 +269,9 @@ mod tests {
assert_eq!(RelationType::Uses.importance_weight(), 0.7);
assert_eq!(RelationType::FactoryCreates.importance_weight(), 0.8);

// Document references (between structural and organizational)
assert_eq!(RelationType::References.importance_weight(), 0.6);

// Low importance relationships
assert_eq!(RelationType::SiblingModule.importance_weight(), 0.3);
assert_eq!(RelationType::ParentModule.importance_weight(), 0.3);
Expand All @@ -290,6 +298,16 @@ mod tests {
RelationType::Calls.importance_weight()
> RelationType::ParentModule.importance_weight()
);

// Verify references sit between structural imports and organizational
assert!(
RelationType::Imports.importance_weight()
> RelationType::References.importance_weight()
);
assert!(
RelationType::References.importance_weight()
> RelationType::SiblingModule.importance_weight()
);
}

#[test]
Expand All @@ -315,6 +333,10 @@ mod tests {
"sibling_module".parse::<RelationType>().unwrap(),
RelationType::SiblingModule
);
assert_eq!(
"references".parse::<RelationType>().unwrap(),
RelationType::References
);

// Test unknown type defaults to Imports
assert_eq!(
Expand Down Expand Up @@ -343,6 +365,7 @@ mod tests {
RelationType::Imports,
RelationType::Calls,
RelationType::Uses,
RelationType::References,
RelationType::SiblingModule,
];

Expand Down
Loading
Loading