Skip to content
Merged
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
33 changes: 33 additions & 0 deletions crates/rpg-nav/src/planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,39 @@ mod tests {
);
}

#[test]
fn test_plan_root_scope_matches_unscoped_search() {
let graph = build_test_graph();
let unscoped = plan_change(
&graph,
&PlanChangeRequest {
goal: "server",
scope: None,
max_entities: 10,
},
None,
);
let root_scoped = plan_change(
&graph,
&PlanChangeRequest {
goal: "server",
scope: Some("."),
max_entities: 10,
},
None,
);

assert_eq!(
root_scoped.relevant_entities.len(),
unscoped.relevant_entities.len(),
"root scope should search the full graph"
);
assert_eq!(
root_scoped.modification_order, unscoped.modification_order,
"root scope should preserve planning results"
);
}

#[test]
fn test_plan_dependency_ordering() {
let graph = build_test_graph();
Expand Down
10 changes: 10 additions & 0 deletions crates/rpg-nav/src/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,11 +562,21 @@ fn maybe_hybrid_rerank(
/// Collect entities from one or more hierarchy scopes.
/// Supports comma-separated scopes per paper's `search_scopes` (list of paths).
fn collect_scoped_entities(graph: &RPGraph, scope: &str) -> Vec<String> {
if matches!(scope.trim(), "" | ".") {
return graph.entities.keys().cloned().collect();
}

let scopes: Vec<&str> = scope.split(',').map(|s| s.trim()).collect();
let mut all_ids: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();

for single_scope in scopes {
if single_scope == "." {
return graph.entities.keys().cloned().collect();
}
if single_scope.is_empty() {
continue;
}
for id in collect_single_scope(graph, single_scope) {
if seen.insert(id.clone()) {
all_ids.push(id);
Expand Down
48 changes: 48 additions & 0 deletions crates/rpg-nav/tests/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,54 @@ fn test_multi_scope_with_invalid_segment() {
assert_eq!(results[0].entity_name, "validate_token");
}

#[test]
fn test_root_scope_matches_unscoped_search() {
let graph = make_graph();
let unscoped = search(&graph, "authentication", SearchMode::Features, None, 10);
let root_scoped = search(
&graph,
"authentication",
SearchMode::Features,
Some("."),
10,
);

let unscoped_ids: Vec<&str> = unscoped.iter().map(|r| r.entity_id.as_str()).collect();
let root_ids: Vec<&str> = root_scoped.iter().map(|r| r.entity_id.as_str()).collect();

assert_eq!(
root_ids, unscoped_ids,
"root scope should match unscoped search"
);
}

#[test]
fn test_multi_scope_with_empty_segment_does_not_widen() {
let graph = make_graph();
let scoped = search(
&graph,
"authentication",
SearchMode::Features,
Some("Security/auth/token,"),
10,
);
let expected = search(
&graph,
"authentication",
SearchMode::Features,
Some("Security/auth/token"),
10,
);

let scoped_ids: Vec<&str> = scoped.iter().map(|r| r.entity_id.as_str()).collect();
let expected_ids: Vec<&str> = expected.iter().map(|r| r.entity_id.as_str()).collect();

assert_eq!(
scoped_ids, expected_ids,
"empty scope segments should be ignored, not widen to the full graph"
);
}

#[test]
fn test_multi_scope_dedup_overlapping() {
let graph = make_graph();
Expand Down
Loading