Skip to content

Commit eb049ce

Browse files
fix(intelligence): Phase 5 JavaLanguageExtractor — false-positive CALLS, confidence, per-edge provenance (RAN-164)
- Scope lookupByLabel() to dedup by node ID — drop ambiguous matches where 2+ nodes share the same method name, eliminating false-positive CALLS edges (save, get, execute…) - Change confidence to PARTIAL for all registry-lookup edges (cross-file by definition) - Populate confidence + extractorName properties on every emitted edge for all 4 extractors - Add negative test: two unrelated classes with same method name produce zero CALLS edges - Update extract_confidenceIsExact_whenCallsFound → expect PARTIAL (correct semantics) Co-Authored-By: Paperclip <noreply@paperclip.ing>
1 parent 4f9eddd commit eb049ce

5 files changed

Lines changed: 65 additions & 14 deletions

File tree

src/main/java/io/github/randomcodespace/iq/intelligence/extractor/go/GoLanguageExtractor.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ private List<CodeEdge> extractImportEdges(DetectorContext ctx, CodeNode node,
9393
}
9494
if (target != null && !target.getId().equals(node.getId())) {
9595
String edgeId = "imports:%s:%s".formatted(node.getId(), target.getId());
96-
edges.add(new CodeEdge(edgeId, EdgeKind.IMPORTS, node.getId(), target));
96+
CodeEdge edge = new CodeEdge(edgeId, EdgeKind.IMPORTS, node.getId(), target);
97+
edge.getProperties().put("confidence", "PARTIAL");
98+
edge.getProperties().put("extractorName", "go_language_extractor");
99+
edges.add(edge);
97100
}
98101
}
99102

src/main/java/io/github/randomcodespace/iq/intelligence/extractor/java/JavaLanguageExtractor.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ public LanguageExtractionResult extract(DetectorContext ctx, CodeNode node) {
7575
extractTypeHierarchyHints(cu, node, typeHints);
7676
}
7777

78-
CapabilityLevel confidence = callEdges.isEmpty() ? CapabilityLevel.PARTIAL : CapabilityLevel.EXACT;
79-
return new LanguageExtractionResult(callEdges, List.of(), typeHints, confidence);
78+
return new LanguageExtractionResult(callEdges, List.of(), typeHints, CapabilityLevel.PARTIAL);
8079
}
8180

8281
private void extractCallEdges(CompilationUnit cu, CodeNode methodNode,
@@ -96,7 +95,10 @@ private void extractCallEdges(CompilationUnit cu, CodeNode methodNode,
9695
String edgeId = "calls:%s:%s:%d".formatted(
9796
methodNode.getId(), target.getId(),
9897
mce.getBegin().map(p -> p.line).orElse(0));
99-
callEdges.add(new CodeEdge(edgeId, EdgeKind.CALLS, methodNode.getId(), target));
98+
CodeEdge edge = new CodeEdge(edgeId, EdgeKind.CALLS, methodNode.getId(), target);
99+
edge.getProperties().put("confidence", "PARTIAL");
100+
edge.getProperties().put("extractorName", "java_language_extractor");
101+
callEdges.add(edge);
100102
}
101103
}));
102104
}
@@ -134,14 +136,18 @@ private Optional<CompilationUnit> parse(DetectorContext ctx) {
134136
}
135137
}
136138

137-
/** Look up a node by label in the registry (label is the simple method/class name). */
139+
/**
140+
* Look up a node by label in the registry (label is the simple method name).
141+
* Returns null if zero or more than one distinct node matches to avoid false-positive
142+
* CALLS edges for common method names like {@code save}, {@code get}, {@code execute}.
143+
*/
138144
private CodeNode lookupByLabel(String label, Map<String, CodeNode> registry) {
139-
// Try FQN suffix match first (exact lookup by label)
145+
Map<String, CodeNode> matches = new java.util.LinkedHashMap<>();
140146
for (CodeNode candidate : registry.values()) {
141147
if (label.equals(candidate.getLabel()) && candidate.getKind() == NodeKind.METHOD) {
142-
return candidate;
148+
matches.put(candidate.getId(), candidate);
143149
}
144150
}
145-
return null;
151+
return matches.size() == 1 ? matches.values().iterator().next() : null;
146152
}
147153
}

src/main/java/io/github/randomcodespace/iq/intelligence/extractor/python/PythonLanguageExtractor.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ private List<CodeEdge> extractImportEdges(DetectorContext ctx, CodeNode node,
7979
CodeNode target = registry.get(sym);
8080
if (target != null && !target.getId().equals(node.getId())) {
8181
String edgeId = "imports:%s:%s".formatted(node.getId(), target.getId());
82-
edges.add(new CodeEdge(edgeId, EdgeKind.IMPORTS, node.getId(), target));
82+
CodeEdge edge = new CodeEdge(edgeId, EdgeKind.IMPORTS, node.getId(), target);
83+
edge.getProperties().put("confidence", "PARTIAL");
84+
edge.getProperties().put("extractorName", "python_language_extractor");
85+
edges.add(edge);
8386
}
8487
}
8588
}
@@ -90,7 +93,10 @@ private List<CodeEdge> extractImportEdges(DetectorContext ctx, CodeNode node,
9093
CodeNode target = registry.get(sym);
9194
if (target != null && !target.getId().equals(node.getId())) {
9295
String edgeId = "imports:%s:%s".formatted(node.getId(), target.getId());
93-
edges.add(new CodeEdge(edgeId, EdgeKind.IMPORTS, node.getId(), target));
96+
CodeEdge edge = new CodeEdge(edgeId, EdgeKind.IMPORTS, node.getId(), target);
97+
edge.getProperties().put("confidence", "PARTIAL");
98+
edge.getProperties().put("extractorName", "python_language_extractor");
99+
edges.add(edge);
94100
}
95101
}
96102

src/main/java/io/github/randomcodespace/iq/intelligence/extractor/typescript/TypeScriptLanguageExtractor.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ private List<CodeEdge> extractImportEdges(DetectorContext ctx, CodeNode node,
8686
CodeNode target = registry.get(sym);
8787
if (target != null && !target.getId().equals(node.getId())) {
8888
String edgeId = "imports:%s:%s".formatted(node.getId(), target.getId());
89-
edges.add(new CodeEdge(edgeId, EdgeKind.IMPORTS, node.getId(), target));
89+
CodeEdge edge = new CodeEdge(edgeId, EdgeKind.IMPORTS, node.getId(), target);
90+
edge.getProperties().put("confidence", "PARTIAL");
91+
edge.getProperties().put("extractorName", "typescript_language_extractor");
92+
edges.add(edge);
9093
}
9194
}
9295
}
@@ -97,7 +100,10 @@ private List<CodeEdge> extractImportEdges(DetectorContext ctx, CodeNode node,
97100
CodeNode target = registry.get(sym);
98101
if (target != null && !target.getId().equals(node.getId())) {
99102
String edgeId = "imports:%s:%s".formatted(node.getId(), target.getId());
100-
edges.add(new CodeEdge(edgeId, EdgeKind.IMPORTS, node.getId(), target));
103+
CodeEdge edge = new CodeEdge(edgeId, EdgeKind.IMPORTS, node.getId(), target);
104+
edge.getProperties().put("confidence", "PARTIAL");
105+
edge.getProperties().put("extractorName", "typescript_language_extractor");
106+
edges.add(edge);
101107
}
102108
}
103109

src/test/java/io/github/randomcodespace/iq/intelligence/extractor/java/JavaLanguageExtractorTest.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ void extract_determinism_sameTwice() {
108108
}
109109

110110
@Test
111-
void extract_confidenceIsExact_whenCallsFound() {
111+
void extract_confidenceIsPartial_whenCallsFound() {
112+
// Registry-lookup edges are cross-file by definition → always PARTIAL
112113
CodeNode caller = node("method:Foo:doWork", NodeKind.METHOD, "doWork");
113114
CodeNode callee = node("method:Bar:helper", NodeKind.METHOD, "helper");
114115
Map<String, CodeNode> registry = Map.of(callee.getId(), callee, callee.getLabel(), callee);
@@ -117,7 +118,36 @@ void extract_confidenceIsExact_whenCallsFound() {
117118
DetectorContext ctx = new DetectorContext("Foo.java", "java", content, registry, null);
118119

119120
LanguageExtractionResult result = extractor.extract(ctx, caller);
120-
assertThat(result.confidence()).isEqualTo(CapabilityLevel.EXACT);
121+
assertThat(result.confidence()).isEqualTo(CapabilityLevel.PARTIAL);
122+
}
123+
124+
@Test
125+
void extract_noFalsePositive_whenTwoClassesHaveSameMethodName() {
126+
// Two unrelated classes both have process() — match is ambiguous → no CALLS edge
127+
CodeNode caller = node("method:Alpha:process", NodeKind.METHOD, "process");
128+
CodeNode calleeA = node("method:Alpha:process", NodeKind.METHOD, "process");
129+
CodeNode calleeB = node("method:Beta:process", NodeKind.METHOD, "process");
130+
131+
Map<String, CodeNode> registry = new HashMap<>();
132+
registry.put(calleeA.getId(), calleeA);
133+
registry.put(calleeB.getId(), calleeB);
134+
135+
String content = """
136+
public class Alpha {
137+
public void run() {
138+
process();
139+
}
140+
public void process() {}
141+
}
142+
""";
143+
144+
CodeNode runMethod = node("method:Alpha:run", NodeKind.METHOD, "run");
145+
DetectorContext ctx = new DetectorContext("Alpha.java", "java", content, registry, null);
146+
LanguageExtractionResult result = extractor.extract(ctx, runMethod);
147+
148+
assertThat(result.callEdges())
149+
.as("ambiguous method name must not produce false-positive CALLS edge")
150+
.isEmpty();
121151
}
122152

123153
@Test

0 commit comments

Comments
 (0)