Skip to content

Commit 8b38667

Browse files
committed
[gobby-cli-#768] feat: label graph edge confidence
1 parent 8e32a23 commit 8b38667

7 files changed

Lines changed: 202 additions & 16 deletions

File tree

crates/gcode/src/commands/graph/reads.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,29 @@ where
117117
lines.join("\n")
118118
}
119119

120+
pub(super) fn format_caller_result_line(result: &GraphResult, target_name: &str) -> String {
121+
format!(
122+
"{} [{}] {} -> {}",
123+
result.line, result.confidence, result.name, target_name
124+
)
125+
}
126+
127+
pub(super) fn format_usage_result_line(result: &GraphResult, target_name: &str) -> String {
128+
let rel = result.relation.as_deref().unwrap_or("unknown");
129+
format!(
130+
"{} [{}] [{}] {} -> {}",
131+
result.line, result.confidence, rel, result.name, target_name
132+
)
133+
}
134+
135+
pub(super) fn format_blast_radius_result_line(result: &GraphResult) -> String {
136+
let distance = result.distance.unwrap_or(0);
137+
format!(
138+
"{} [{}] [distance={}] {}",
139+
result.line, result.confidence, distance, result.name
140+
)
141+
}
142+
120143
#[derive(Serialize)]
121144
struct GraphPathEndpoint {
122145
#[serde(skip_serializing_if = "Option::is_none")]
@@ -385,7 +408,7 @@ pub fn callers(
385408
eprintln!("No callers at offset {offset} (total {total})");
386409
} else {
387410
output::print_text(&format_grouped_graph_results(&results, |r| {
388-
format!("{} {} -> {}", r.line, r.name, symbol.display_name)
411+
format_caller_result_line(r, &symbol.display_name)
389412
}))?;
390413
if total > offset + results.len() {
391414
eprintln!(
@@ -438,8 +461,7 @@ pub fn usages(
438461
eprintln!("No usages at offset {offset} (total {total})");
439462
} else {
440463
output::print_text(&format_grouped_graph_results(&results, |r| {
441-
let rel = r.relation.as_deref().unwrap_or("unknown");
442-
format!("{} [{}] {} -> {}", r.line, rel, r.name, symbol.display_name)
464+
format_usage_result_line(r, &symbol.display_name)
443465
}))?;
444466
if total > offset + results.len() {
445467
eprintln!(
@@ -556,8 +578,7 @@ pub fn blast_radius(
556578
print_graph_hint_text(ctx, None);
557579
} else {
558580
output::print_text(&format_grouped_graph_results(&results, |r| {
559-
let dist = r.distance.unwrap_or(0);
560-
format!("{} [distance={}] {}", r.line, dist, r.name)
581+
format_blast_radius_result_line(r)
561582
}))?;
562583
}
563584
Ok(())

crates/gcode/src/commands/graph/tests.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use super::lifecycle::{
33
skipped_missing_indexed_file_payload, skipped_no_graph_facts_payload,
44
};
55
use super::payload::format_report_text;
6-
use super::reads::{format_grouped_graph_results, format_symbol_path_text};
6+
use super::reads::{
7+
format_blast_radius_result_line, format_caller_result_line, format_grouped_graph_results,
8+
format_symbol_path_text, format_usage_result_line,
9+
};
710
use super::{imports, report};
811
use crate::config::Context;
912
use crate::graph::code_graph::{self, GraphLifecycleAction, GraphLifecycleOutput};
@@ -59,6 +62,7 @@ fn graph_text_groups_by_file_and_sorts_entries() {
5962
name: "beta".to_string(),
6063
file_path: "src/b.rs".to_string(),
6164
line: 9,
65+
confidence: ProjectionProvenance::Extracted,
6266
relation: Some("CALLS".to_string()),
6367
distance: None,
6468
metadata: None,
@@ -68,6 +72,7 @@ fn graph_text_groups_by_file_and_sorts_entries() {
6872
name: "zeta".to_string(),
6973
file_path: "src/a.rs".to_string(),
7074
line: 8,
75+
confidence: ProjectionProvenance::Extracted,
7176
relation: Some("CALLS".to_string()),
7277
distance: None,
7378
metadata: None,
@@ -77,6 +82,7 @@ fn graph_text_groups_by_file_and_sorts_entries() {
7782
name: "alpha".to_string(),
7883
file_path: "src/a.rs".to_string(),
7984
line: 3,
85+
confidence: ProjectionProvenance::Extracted,
8086
relation: Some("CALLS".to_string()),
8187
distance: None,
8288
metadata: None,
@@ -90,6 +96,33 @@ fn graph_text_groups_by_file_and_sorts_entries() {
9096
assert_eq!(text, "src/a.rs\n3 alpha\n8 zeta\nsrc/b.rs\n9 beta");
9197
}
9298

99+
#[test]
100+
fn graph_read_text_lines_surface_confidence_labels() {
101+
let result = GraphResult {
102+
id: "sym-1".to_string(),
103+
name: "run".to_string(),
104+
file_path: "src/lib.rs".to_string(),
105+
line: 12,
106+
confidence: ProjectionProvenance::Inferred,
107+
relation: Some("CALLS".to_string()),
108+
distance: Some(2),
109+
metadata: None,
110+
};
111+
112+
assert_eq!(
113+
format_caller_result_line(&result, "main"),
114+
"12 [INFERRED] run -> main"
115+
);
116+
assert_eq!(
117+
format_usage_result_line(&result, "main"),
118+
"12 [INFERRED] [CALLS] run -> main"
119+
);
120+
assert_eq!(
121+
format_blast_radius_result_line(&result),
122+
"12 [INFERRED] [distance=2] run"
123+
);
124+
}
125+
93126
#[test]
94127
fn graph_path_text_prints_ordered_chain_with_locations() {
95128
let path = vec![
@@ -394,6 +427,7 @@ fn top_level_read_commands_preserve_json_shape() {
394427
name: "run".to_string(),
395428
file_path: "src/lib.rs".to_string(),
396429
line: 12,
430+
confidence: ProjectionProvenance::Extracted,
397431
relation: Some("CALLS".to_string()),
398432
distance: Some(1),
399433
metadata: None,
@@ -411,6 +445,7 @@ fn top_level_read_commands_preserve_json_shape() {
411445
assert_eq!(value["results"][0]["name"], "run");
412446
assert_eq!(value["results"][0]["file_path"], "src/lib.rs");
413447
assert_eq!(value["results"][0]["line"], 12);
448+
assert_eq!(value["results"][0]["confidence"], "EXTRACTED");
414449
assert_eq!(value["results"][0]["relation"], "CALLS");
415450
assert_eq!(value["results"][0]["distance"], 1);
416451
assert!(value["hint"].is_null());
@@ -426,6 +461,7 @@ fn top_level_read_commands_preserve_json_shape() {
426461
name: "run".to_string(),
427462
file_path: "src/lib.rs".to_string(),
428463
line: 12,
464+
confidence: ProjectionProvenance::Inferred,
429465
relation: Some("CALLS".to_string()),
430466
distance: Some(1),
431467
metadata: Some(
@@ -437,6 +473,7 @@ fn top_level_read_commands_preserve_json_shape() {
437473
};
438474
let value = serde_json::to_value(&response).expect("serialize metadata response");
439475

476+
assert_eq!(value["results"][0]["confidence"], "INFERRED");
440477
assert_eq!(
441478
value["results"][0]["metadata"]["source_file_path"],
442479
"src/lib.rs"

crates/gcode/src/contract.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ fn graph_read_keys() -> Vec<&'static str> {
594594
"name",
595595
"file_path",
596596
"line",
597+
"confidence",
597598
"relation",
598599
"distance",
599600
"metadata",

crates/gcode/src/graph/code_graph/read/relationship_queries.rs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use std::collections::HashMap;
22

33
use crate::graph::typed_query;
44

5-
use super::support::{CALL_TARGET_PREDICATE, clamp_limit, clamp_offset};
5+
use super::support::{
6+
CALL_TARGET_PREDICATE, CONFIDENCE_LABEL_CASE, LINK_METADATA_RETURN, clamp_limit, clamp_offset,
7+
};
68

79
pub(crate) fn count_callers_query(
810
project_id: &str,
@@ -47,8 +49,11 @@ pub(crate) fn find_callers_query(
4749
format!(
4850
"MATCH (caller:CodeSymbol {{project: $project}})-[r:CALLS]->(target {{id: $id, project: $project}}) \
4951
WHERE {CALL_TARGET_PREDICATE} \
50-
RETURN DISTINCT caller.id AS caller_id, caller.name AS caller_name, \
51-
caller.file_path AS file, caller.line_start AS line \
52+
WITH caller, collect(coalesce(r.provenance, 'EXTRACTED')) AS provenances \
53+
WITH caller, {CONFIDENCE_LABEL_CASE} AS confidence_label \
54+
RETURN caller.id AS caller_id, caller.name AS caller_name, \
55+
caller.file_path AS file, caller.line_start AS line, \
56+
confidence_label AS confidence_label \
5257
ORDER BY caller.id \
5358
SKIP {offset} LIMIT {limit}"
5459
),
@@ -69,7 +74,8 @@ pub(crate) fn find_usages_query(
6974
"MATCH (source:CodeSymbol {{project: $project}})-[r:CALLS]->(target {{id: $id, project: $project}}) \
7075
WHERE {CALL_TARGET_PREDICATE} \
7176
RETURN source.id AS source_id, source.name AS source_name, \
72-
'CALLS' AS rel_type, r.file AS file, r.line AS line \
77+
'CALLS' AS rel_type, r.file AS file, r.line AS line, \
78+
{LINK_METADATA_RETURN} \
7379
ORDER BY source.id, r.line, r.file \
7480
SKIP {offset} LIMIT {limit}"
7581
),
@@ -124,9 +130,11 @@ pub(crate) fn find_callers_batch_query(
124130
format!(
125131
"MATCH (caller:CodeSymbol {{project: $project}})-[r:CALLS]->(target {{project: $project}}) \
126132
WHERE ({CALL_TARGET_PREDICATE}) AND target.id IN [{ids}] \
127-
WITH caller, min(r.file) AS file, min(r.line) AS line \
133+
WITH caller, min(r.file) AS file, min(r.line) AS line, \
134+
collect(coalesce(r.provenance, 'EXTRACTED')) AS provenances \
135+
WITH caller, file, line, {CONFIDENCE_LABEL_CASE} AS confidence_label \
128136
RETURN caller.id AS caller_id, caller.name AS caller_name, \
129-
file AS file, line AS line \
137+
file AS file, line AS line, confidence_label AS confidence_label \
130138
ORDER BY caller.id \
131139
LIMIT {limit}"
132140
),
@@ -164,9 +172,11 @@ pub(crate) fn find_callees_batch_query(
164172
format!(
165173
"MATCH (src:CodeSymbol {{project: $project}})-[r:CALLS]->(target {{project: $project}}) \
166174
WHERE src.id IN [{ids}] AND ({CALL_TARGET_PREDICATE}) \
167-
WITH target, min(r.file) AS file, min(r.line) AS line \
175+
WITH target, min(r.file) AS file, min(r.line) AS line, \
176+
collect(coalesce(r.provenance, 'EXTRACTED')) AS provenances \
177+
WITH target, file, line, {CONFIDENCE_LABEL_CASE} AS confidence_label \
168178
RETURN target.id AS callee_id, target.name AS callee_name, \
169-
file AS file, line AS line \
179+
file AS file, line AS line, confidence_label AS confidence_label \
170180
ORDER BY target.id \
171181
LIMIT {limit}"
172182
),
@@ -285,3 +295,36 @@ pub(crate) fn blast_radius_query(depth: usize, limit: usize) -> String {
285295
LIMIT {limit}"
286296
)
287297
}
298+
299+
#[cfg(test)]
300+
mod tests {
301+
use super::*;
302+
303+
#[test]
304+
fn callers_query_projects_confidence_without_edge_metadata_duplication() {
305+
let (query, _) = find_callers_query("project-1", "symbol-1", 0, 10);
306+
307+
assert!(query.contains("confidence_label AS confidence_label"));
308+
assert!(query.contains("WHEN 'AMBIGUOUS' IN provenances THEN 'AMBIGUOUS'"));
309+
assert!(!query.contains("r.confidence AS confidence"));
310+
}
311+
312+
#[test]
313+
fn usages_query_projects_edge_confidence_and_metadata() {
314+
let (query, _) = find_usages_query("project-1", "symbol-1", 0, 10);
315+
316+
assert!(query.contains("r.provenance AS provenance"));
317+
assert!(query.contains("r.confidence AS confidence"));
318+
}
319+
320+
#[test]
321+
fn batched_relationship_queries_project_confidence_label() {
322+
let ids = vec!["symbol-1".to_string()];
323+
324+
let (callers_query, _) = find_callers_batch_query("project-1", &ids, 10);
325+
let (callees_query, _) = find_callees_batch_query("project-1", &ids, 10);
326+
327+
assert!(callers_query.contains("confidence_label AS confidence_label"));
328+
assert!(callees_query.contains("confidence_label AS confidence_label"));
329+
}
330+
}

crates/gcode/src/graph/code_graph/read/support.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::HashSet;
22

33
use crate::graph::typed_query;
4-
use crate::models::GraphResult;
4+
use crate::models::{GraphResult, ProjectionProvenance};
55
use gobby_core::falkor::Row;
66

77
use super::super::payload::{row_string_owned, row_to_projection_metadata, row_usize};
@@ -20,6 +20,11 @@ pub(super) const NEIGHBOR_TYPE_CASE: &str = "CASE \
2020
WHEN neighbor:ExternalSymbol THEN 'external' \
2121
ELSE 'unresolved' \
2222
END";
23+
pub(super) const CONFIDENCE_LABEL_CASE: &str = "CASE \
24+
WHEN 'AMBIGUOUS' IN provenances THEN 'AMBIGUOUS' \
25+
WHEN 'INFERRED' IN provenances THEN 'INFERRED' \
26+
ELSE 'EXTRACTED' \
27+
END";
2328
pub(super) const NODE_TYPE_CASE: &str = "CASE \
2429
WHEN n:CodeFile THEN 'file' \
2530
WHEN n:CodeModule THEN 'module' \
@@ -69,6 +74,12 @@ pub(crate) fn row_to_graph_result(row: &Row) -> GraphResult {
6974
.and_then(|v| v.as_u64())
7075
.and_then(|value| usize::try_from(value).ok())
7176
.unwrap_or(0),
77+
confidence: row
78+
.get("confidence_label")
79+
.or_else(|| row.get("provenance"))
80+
.and_then(|v| v.as_str())
81+
.and_then(ProjectionProvenance::from_wire_value)
82+
.unwrap_or_default(),
7283
relation: row
7384
.get("relation")
7485
.or_else(|| row.get("rel_type"))
@@ -129,3 +140,49 @@ pub(super) fn count_from_rows(rows: &[Row]) -> usize {
129140
.and_then(|value| usize::try_from(value).ok())
130141
.unwrap_or(0)
131142
}
143+
144+
#[cfg(test)]
145+
mod tests {
146+
use super::*;
147+
use serde_json::json;
148+
149+
#[test]
150+
fn graph_result_confidence_defaults_to_extracted_without_metadata() {
151+
let row = Row::from([
152+
("caller_id".to_string(), json!("caller-1")),
153+
("caller_name".to_string(), json!("run")),
154+
("file".to_string(), json!("src/lib.rs")),
155+
("line".to_string(), json!(12)),
156+
]);
157+
158+
let result = row_to_graph_result(&row);
159+
160+
assert_eq!(result.confidence, ProjectionProvenance::Extracted);
161+
assert!(result.metadata.is_none());
162+
}
163+
164+
#[test]
165+
fn graph_result_confidence_uses_provenance_label_with_metadata_score() {
166+
let row = Row::from([
167+
("source_id".to_string(), json!("caller-1")),
168+
("source_name".to_string(), json!("run")),
169+
("file".to_string(), json!("src/lib.rs")),
170+
("line".to_string(), json!(12)),
171+
("rel_type".to_string(), json!("CALLS")),
172+
("provenance".to_string(), json!("INFERRED")),
173+
("confidence".to_string(), json!(0.72)),
174+
("source_system".to_string(), json!("semantic")),
175+
]);
176+
177+
let result = row_to_graph_result(&row);
178+
179+
assert_eq!(result.confidence, ProjectionProvenance::Inferred);
180+
assert_eq!(
181+
result
182+
.metadata
183+
.as_ref()
184+
.and_then(|metadata| metadata.confidence),
185+
Some(0.72)
186+
);
187+
}
188+
}

0 commit comments

Comments
 (0)