Skip to content

Commit bcc3df2

Browse files
aksOpsclaude
andcommitted
feat: fix MCP/API intelligence, add Neo4j persistence, enhance UI dashboard
Major improvements to the code intelligence pipeline: - Fix getDetailedStats to use StatsService for rich categorized stats (was returning hardcoded empty maps for frameworks/infra/connections/auth) - Fix findRelatedEndpoints to actually traverse graph for connected endpoints - Fix readFile to return JSON errors consistently with other MCP tools - Fix Neo4j label serialization bug (was returning count instead of names) - Add Cypher-based bulkSave() to GraphStore for reliable Neo4j persistence - Wire POST /api/analyze to persist analysis results to Neo4j + evict caches - Attach edges to source nodes in Analyzer for downstream consumers - Fix Quarkus/Micronaut detectors to require framework-specific imports - Add ENTITY and 8 other node kinds to LayerClassifier backend classification - Restore node properties from Neo4j prop_* prefixed keys on read - Fix edge hydration in findAll(), findById(), findEdgesPaginated() - Enhance UI dashboard with rich stats: languages, frameworks, infra, connections - Add edge labels and layer badges to node detail panel E2E validated against spring-petclinic: 719 nodes, 1396 edges, 21 REST endpoints Tests: 1308 pass, 0 failures (up from 1298, +10 new tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 89cb586 commit bcc3df2

17 files changed

Lines changed: 499 additions & 34 deletions

File tree

src/main/java/io/github/randomcodespace/iq/analyzer/AnalysisResult.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package io.github.randomcodespace.iq.analyzer;
22

3+
import io.github.randomcodespace.iq.model.CodeNode;
4+
35
import java.time.Duration;
6+
import java.util.List;
47
import java.util.Map;
58

69
/**
@@ -14,6 +17,7 @@
1417
* @param nodeBreakdown count of nodes per NodeKind value
1518
* @param frameworkBreakdown count of nodes per detected framework
1619
* @param elapsed wall-clock duration of the analysis
20+
* @param nodes the actual graph nodes (may be null for backward compat)
1721
*/
1822
public record AnalysisResult(
1923
int totalFiles,
@@ -24,5 +28,17 @@ public record AnalysisResult(
2428
Map<String, Integer> nodeBreakdown,
2529
Map<String, Integer> edgeBreakdown,
2630
Map<String, Integer> frameworkBreakdown,
27-
Duration elapsed
28-
) {}
31+
Duration elapsed,
32+
List<CodeNode> nodes
33+
) {
34+
/** Backward-compatible constructor without nodes. */
35+
public AnalysisResult(
36+
int totalFiles, int filesAnalyzed, int nodeCount, int edgeCount,
37+
Map<String, Integer> languageBreakdown, Map<String, Integer> nodeBreakdown,
38+
Map<String, Integer> edgeBreakdown, Map<String, Integer> frameworkBreakdown,
39+
Duration elapsed) {
40+
this(totalFiles, filesAnalyzed, nodeCount, edgeCount,
41+
languageBreakdown, nodeBreakdown, edgeBreakdown, frameworkBreakdown,
42+
elapsed, null);
43+
}
44+
}

src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,14 +333,26 @@ private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCach
333333
}
334334
}
335335

336-
// 6. Compute node breakdown
336+
// 6. Attach edges to their source nodes for downstream consumers
337+
Map<String, CodeNode> nodeById = new HashMap<>(allNodes.size());
338+
for (CodeNode node : allNodes) {
339+
nodeById.put(node.getId(), node);
340+
}
341+
for (var edge : builder.getEdges()) {
342+
CodeNode source = nodeById.get(edge.getSourceId());
343+
if (source != null) {
344+
source.getEdges().add(edge);
345+
}
346+
}
347+
348+
// 7. Compute node breakdown
337349
Map<String, Integer> nodeBreakdown = new HashMap<>();
338350
for (CodeNode node : allNodes) {
339351
String kindValue = node.getKind().getValue();
340352
nodeBreakdown.merge(kindValue, 1, Integer::sum);
341353
}
342354

343-
// 7. Compute edge breakdown
355+
// 8. Compute edge breakdown
344356
Map<String, Integer> edgeBreakdown = new HashMap<>();
345357
for (var edge : builder.getEdges()) {
346358
String kindValue = edge.getKind().getValue();
@@ -383,7 +395,8 @@ private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCach
383395
nodeBreakdown,
384396
edgeBreakdown,
385397
frameworkBreakdown,
386-
elapsed
398+
elapsed,
399+
allNodes
387400
);
388401
}
389402

src/main/java/io/github/randomcodespace/iq/analyzer/LayerClassifier.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ public class LayerClassifier {
2424

2525
private static final Set<NodeKind> BACKEND_NODE_KINDS = Set.of(
2626
NodeKind.GUARD, NodeKind.MIDDLEWARE, NodeKind.ENDPOINT,
27-
NodeKind.REPOSITORY, NodeKind.DATABASE_CONNECTION, NodeKind.QUERY
27+
NodeKind.REPOSITORY, NodeKind.DATABASE_CONNECTION, NodeKind.QUERY,
28+
NodeKind.ENTITY, NodeKind.MIGRATION, NodeKind.SERVICE,
29+
NodeKind.TOPIC, NodeKind.QUEUE, NodeKind.EVENT,
30+
NodeKind.MESSAGE_QUEUE, NodeKind.RMI_INTERFACE,
31+
NodeKind.WEBSOCKET_ENDPOINT
2832
);
2933

3034
private static final Set<NodeKind> INFRA_NODE_KINDS = Set.of(
@@ -36,7 +40,8 @@ public class LayerClassifier {
3640
);
3741

3842
private static final Set<NodeKind> SHARED_NODE_KINDS = Set.of(
39-
NodeKind.CONFIG_FILE, NodeKind.CONFIG_KEY, NodeKind.CONFIG_DEFINITION
43+
NodeKind.CONFIG_FILE, NodeKind.CONFIG_KEY, NodeKind.CONFIG_DEFINITION,
44+
NodeKind.PROTOCOL_MESSAGE
4045
);
4146

4247
private static final Pattern FRONTEND_PATH_RE = Pattern.compile(

src/main/java/io/github/randomcodespace/iq/api/GraphController.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import java.util.Map;
2626
import java.util.concurrent.atomic.AtomicBoolean;
2727

28+
import io.github.randomcodespace.iq.graph.GraphStore;
29+
import org.springframework.cache.CacheManager;
30+
2831
/**
2932
* REST API controller matching the Python OSSCodeIQ API paths.
3033
*/
@@ -36,14 +39,20 @@ public class GraphController {
3639
private final QueryService queryService;
3740
private final Analyzer analyzer;
3841
private final CodeIqConfig config;
42+
private final CacheManager cacheManager;
43+
private final GraphStore graphStore;
3944
private final AtomicBoolean analysisRunning = new AtomicBoolean(false);
4045

4146
public GraphController(@org.springframework.beans.factory.annotation.Autowired(required = false) QueryService queryService,
4247
Analyzer analyzer,
43-
CodeIqConfig config) {
48+
CodeIqConfig config,
49+
@org.springframework.beans.factory.annotation.Autowired(required = false) CacheManager cacheManager,
50+
@org.springframework.beans.factory.annotation.Autowired(required = false) GraphStore graphStore) {
4451
this.queryService = queryService;
4552
this.analyzer = analyzer;
4653
this.config = config;
54+
this.cacheManager = cacheManager;
55+
this.graphStore = graphStore;
4756
}
4857

4958
@GetMapping("/stats")
@@ -269,6 +278,19 @@ public ResponseEntity<?> triggerAnalysis(
269278
try {
270279
AnalysisResult result = analyzer.run(Path.of(config.getRootPath()), null);
271280

281+
// Persist to Neo4j if GraphStore is available
282+
if (graphStore != null && result.nodes() != null && !result.nodes().isEmpty()) {
283+
graphStore.bulkSave(result.nodes());
284+
}
285+
286+
// Evict all Spring caches so queries pick up new data
287+
if (cacheManager != null) {
288+
cacheManager.getCacheNames().forEach(name -> {
289+
var cache = cacheManager.getCache(name);
290+
if (cache != null) cache.clear();
291+
});
292+
}
293+
272294
Map<String, Object> response = new LinkedHashMap<>();
273295
response.put("status", "complete");
274296
response.put("total_files", result.totalFiles());

src/main/java/io/github/randomcodespace/iq/detector/java/MicronautDetector.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ public DetectorResult detect(DetectorContext ctx) {
5555
String text = ctx.content();
5656
if (text == null || text.isEmpty()) return DetectorResult.empty();
5757

58+
// First, require a Micronaut-specific indicator to avoid false positives on
59+
// Spring Boot or other frameworks that share common annotations like
60+
// @Controller, @Singleton, @Inject, @Scheduled, @EventListener, etc.
61+
boolean hasMicronautIndicator = text.contains("io.micronaut")
62+
|| text.contains("@Client");
63+
if (!hasMicronautIndicator) {
64+
return DetectorResult.empty();
65+
}
66+
5867
if (!text.contains("@Controller") && !text.contains("@Get") && !text.contains("@Post")
5968
&& !text.contains("@Put") && !text.contains("@Delete")
6069
&& !text.contains("@Singleton") && !text.contains("@Prototype") && !text.contains("@Infrastructure")

src/main/java/io/github/randomcodespace/iq/detector/java/QuarkusDetector.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ public DetectorResult detect(DetectorContext ctx) {
4949
String text = ctx.content();
5050
if (text == null || text.isEmpty()) return DetectorResult.empty();
5151

52+
// First, require a Quarkus-specific indicator to avoid false positives on
53+
// Spring Boot or other frameworks that share common annotations like
54+
// @Transactional, @Scheduled, @Singleton, @ApplicationScoped, etc.
55+
boolean hasQuarkusIndicator = text.contains("io.quarkus")
56+
|| text.contains("io.smallrye")
57+
|| text.contains("@QuarkusTest");
58+
if (!hasQuarkusIndicator) {
59+
return DetectorResult.empty();
60+
}
61+
5262
if (!text.contains("@QuarkusTest") && !text.contains("@ConfigProperty")
5363
&& !text.contains("@Singleton") && !text.contains("@ApplicationScoped")
5464
&& !text.contains("@RequestScoped") && !text.contains("@Scheduled")

0 commit comments

Comments
 (0)