Skip to content

Commit 3f0ee38

Browse files
committed
checkpoint: pre-yolo 20260330-220034
1 parent 9095e3b commit 3f0ee38

6 files changed

Lines changed: 127 additions & 101 deletions

File tree

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,7 @@ public Map<String, Object> getStats() {
5656
public Map<String, Object> getDetailedStats(
5757
@RequestParam(defaultValue = "all") String category) {
5858
requireQueryService();
59-
if ("all".equalsIgnoreCase(category) || "graph".equalsIgnoreCase(category)) {
60-
return queryService.getStats();
61-
}
62-
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED,
63-
"Detailed stats by category not yet supported via Neo4j. Use /api/stats instead.");
59+
return queryService.getDetailedStats(category);
6460
}
6561

6662
@GetMapping("/kinds")

src/main/java/io/github/randomcodespace/iq/mcp/McpTools.java

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public String getStats() {
9494
public String getDetailedStats(
9595
@ToolParam(description = "Category filter (default: all)", required = false) String category) {
9696
try {
97-
return toJson(queryService.getStats());
97+
return toJson(queryService.getDetailedStats(category != null ? category : "all"));
9898
} catch (Exception e) {
9999
return toJson(Map.of("error", e.getMessage()));
100100
}
@@ -335,13 +335,7 @@ public String traceImpact(
335335
public String findRelatedEndpoints(
336336
@ToolParam(description = "File, class, or entity identifier") String identifier) {
337337
try {
338-
// Search for the identifier, then find endpoints connected to the results
339-
List<Map<String, Object>> results = queryService.searchGraph(identifier, 50);
340-
Map<String, Object> response = new LinkedHashMap<>();
341-
response.put("identifier", identifier);
342-
response.put("related_nodes", results);
343-
response.put("count", results.size());
344-
return toJson(response);
338+
return toJson(queryService.findRelatedEndpoints(identifier));
345339
} catch (Exception e) {
346340
return toJson(Map.of("error", e.getMessage()));
347341
}
@@ -368,7 +362,7 @@ public String readFile(
368362
Path resolved = root.resolve(filePath).normalize();
369363
// Path traversal protection
370364
if (!resolved.startsWith(root)) {
371-
return "Error: Path traversal detected";
365+
return toJson(Map.of("error", "Path traversal detected"));
372366
}
373367
String content = java.nio.file.Files.readString(resolved, java.nio.charset.StandardCharsets.UTF_8);
374368
if (startLine != null || endLine != null) {
@@ -387,7 +381,7 @@ public String readFile(
387381
}
388382
return content;
389383
} catch (Exception e) {
390-
return "Error: " + e.getMessage();
384+
return toJson(Map.of("error", "Failed to read file: " + e.getMessage()));
391385
}
392386
}
393387

@@ -523,7 +517,9 @@ private Object toSerializable(Object val) {
523517
if (val instanceof org.neo4j.graphdb.Node node) {
524518
Map<String, Object> map = new LinkedHashMap<>();
525519
map.put("_id", node.getElementId());
526-
map.put("_labels", node.getLabels().spliterator().estimateSize());
520+
List<String> labels = new ArrayList<>();
521+
node.getLabels().forEach(l -> labels.add(l.name()));
522+
map.put("_labels", labels);
527523
for (String key : node.getPropertyKeys()) {
528524
map.put(key, node.getProperty(key));
529525
}

src/main/java/io/github/randomcodespace/iq/query/QueryService.java

Lines changed: 76 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.LinkedHashMap;
1414
import java.util.List;
1515
import java.util.Map;
16+
import java.util.Set;
1617

1718
/**
1819
* High-level query service wrapping GraphStore with caching.
@@ -24,90 +25,65 @@ public class QueryService {
2425

2526
private final GraphStore graphStore;
2627
private final CodeIqConfig config;
28+
private final StatsService statsService;
2729

28-
public QueryService(GraphStore graphStore, CodeIqConfig config) {
30+
public QueryService(GraphStore graphStore, CodeIqConfig config, StatsService statsService) {
2931
this.graphStore = graphStore;
3032
this.config = config;
33+
this.statsService = statsService;
3134
}
3235

3336
@Cacheable("graph-stats")
3437
public Map<String, Object> getStats() {
35-
long nodeCount = graphStore.count();
36-
long edgeCount = graphStore.countEdges();
37-
long fileCount = graphStore.countDistinctFiles();
38+
// Load full graph data and compute rich categorized stats
39+
List<CodeNode> nodes = graphStore.findAll();
40+
List<CodeEdge> edges = nodes.stream()
41+
.flatMap(n -> n.getEdges().stream())
42+
.toList();
43+
44+
Map<String, Object> result = statsService.computeStats(nodes, edges);
3845

46+
// Also include raw counts and breakdowns for backward compat
3947
Map<String, Long> nodesByKind = new LinkedHashMap<>();
4048
for (Map<String, Object> row : graphStore.countNodesByKind()) {
4149
nodesByKind.put((String) row.get("kind"), ((Number) row.get("cnt")).longValue());
4250
}
43-
4451
Map<String, Long> nodesByLayer = new LinkedHashMap<>();
4552
for (Map<String, Object> row : graphStore.countNodesByLayer()) {
4653
nodesByLayer.put((String) row.get("layer"), ((Number) row.get("cnt")).longValue());
4754
}
4855

49-
// Language breakdown from file extensions
50-
Map<String, Long> languages = new LinkedHashMap<>();
51-
for (Map<String, Object> row : graphStore.countByFileExtension()) {
52-
String ext = (String) row.get("ext");
53-
long cnt = ((Number) row.get("cnt")).longValue();
54-
String lang = extToLanguage(ext);
55-
languages.merge(lang, cnt, Long::sum);
56-
}
57-
58-
// Return in ComputedStatsResponse format for frontend compatibility
59-
Map<String, Object> graph = new LinkedHashMap<>();
60-
graph.put("nodes", nodeCount);
61-
graph.put("edges", edgeCount);
62-
graph.put("files", fileCount);
63-
64-
Map<String, Object> result = new LinkedHashMap<>();
65-
result.put("graph", graph);
66-
result.put("languages", languages);
67-
result.put("frameworks", Map.of());
68-
result.put("infra", Map.of("databases", Map.of(), "messaging", Map.of(), "cloud", Map.of()));
69-
result.put("connections", Map.of("rest", Map.of("total", 0, "by_method", Map.of()),
70-
"grpc", 0, "websocket", 0, "producers", 0, "consumers", 0));
71-
result.put("auth", Map.of());
72-
result.put("architecture", Map.of());
73-
// Also include raw counts for backward compat
74-
result.put("node_count", nodeCount);
75-
result.put("edge_count", edgeCount);
56+
result.put("node_count", nodes.size());
57+
result.put("edge_count", edges.size());
7658
result.put("nodes_by_kind", nodesByKind);
7759
result.put("nodes_by_layer", nodesByLayer);
7860
return result;
7961
}
8062

81-
private static String extToLanguage(String ext) {
82-
if (ext == null) return "unknown";
83-
return switch (ext.toLowerCase()) {
84-
case "java" -> "java";
85-
case "kt", "kts" -> "kotlin";
86-
case "py" -> "python";
87-
case "js", "mjs", "cjs" -> "javascript";
88-
case "ts", "tsx" -> "typescript";
89-
case "go" -> "go";
90-
case "rs" -> "rust";
91-
case "cs" -> "csharp";
92-
case "scala" -> "scala";
93-
case "cpp", "cc", "cxx", "h", "hpp" -> "cpp";
94-
case "c" -> "c";
95-
case "rb" -> "ruby";
96-
case "proto" -> "protobuf";
97-
case "yml", "yaml" -> "yaml";
98-
case "json" -> "json";
99-
case "xml" -> "xml";
100-
case "tf" -> "terraform";
101-
case "sql" -> "sql";
102-
case "md" -> "markdown";
103-
case "html", "htm" -> "html";
104-
case "css", "scss", "sass" -> "css";
105-
case "vue" -> "vue";
106-
case "svelte" -> "svelte";
107-
case "jsx" -> "jsx";
108-
case "sh", "bash" -> "shell";
109-
default -> ext;
110-
};
63+
/**
64+
* Get detailed stats for a specific category, or all categories.
65+
* Categories: all, graph, languages, frameworks, infra, connections, auth, architecture
66+
*/
67+
@Cacheable(value = "detailed-stats", key = "#category")
68+
public Map<String, Object> getDetailedStats(String category) {
69+
List<CodeNode> nodes = graphStore.findAll();
70+
List<CodeEdge> edges = nodes.stream()
71+
.flatMap(n -> n.getEdges().stream())
72+
.toList();
73+
74+
if (category == null || "all".equalsIgnoreCase(category)) {
75+
return statsService.computeStats(nodes, edges);
76+
}
77+
Map<String, Object> catResult = statsService.computeCategory(nodes, edges, category);
78+
if (catResult == null) {
79+
Map<String, Object> error = new LinkedHashMap<>();
80+
error.put("error", "Unknown category: " + category
81+
+ ". Valid: all, graph, languages, frameworks, infra, connections, auth, architecture");
82+
return error;
83+
}
84+
Map<String, Object> result = new LinkedHashMap<>();
85+
result.put(category.toLowerCase(), catResult);
86+
return result;
11187
}
11288

11389
@Cacheable("kinds-list")
@@ -352,6 +328,44 @@ public List<Map<String, Object>> searchGraph(String query, int limit) {
352328
return results.stream().map(this::nodeToMap).toList();
353329
}
354330

331+
/**
332+
* Find API endpoints related to an identifier (file, class, entity).
333+
* Searches for matching nodes, then traverses the graph to find connected endpoints.
334+
*/
335+
public Map<String, Object> findRelatedEndpoints(String identifier) {
336+
// Find nodes matching the identifier
337+
List<CodeNode> matches = graphStore.search(identifier, 50);
338+
339+
// Collect endpoints: any match that IS an endpoint, plus neighbors of matches that are endpoints
340+
Set<String> seenIds = new java.util.LinkedHashSet<>();
341+
List<Map<String, Object>> endpoints = new ArrayList<>();
342+
343+
for (CodeNode match : matches) {
344+
if (match.getKind() == NodeKind.ENDPOINT || match.getKind() == NodeKind.WEBSOCKET_ENDPOINT) {
345+
if (seenIds.add(match.getId())) {
346+
endpoints.add(nodeToMap(match));
347+
}
348+
}
349+
// Check neighbors for connected endpoints
350+
List<CodeNode> neighbors = graphStore.findNeighbors(match.getId());
351+
for (CodeNode neighbor : neighbors) {
352+
if ((neighbor.getKind() == NodeKind.ENDPOINT || neighbor.getKind() == NodeKind.WEBSOCKET_ENDPOINT)
353+
&& seenIds.add(neighbor.getId())) {
354+
Map<String, Object> epMap = nodeToMap(neighbor);
355+
epMap.put("connected_via", match.getId());
356+
endpoints.add(epMap);
357+
}
358+
}
359+
}
360+
361+
Map<String, Object> result = new LinkedHashMap<>();
362+
result.put("identifier", identifier);
363+
result.put("endpoints", endpoints);
364+
result.put("count", endpoints.size());
365+
result.put("searched_nodes", matches.size());
366+
return result;
367+
}
368+
355369
// --- Topology ---
356370

357371
@Cacheable("topology")

src/test/java/io/github/randomcodespace/iq/api/TopologyEndpointTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ void getTopologyReturnsEmptyListsWhenNoData() throws Exception {
184184
@Test
185185
void queryServiceGetTopologyDelegatesToGraphStore() {
186186
CodeIqConfig cfg = new CodeIqConfig();
187-
QueryService service = new QueryService(graphStore, cfg);
187+
QueryService service = new QueryService(graphStore, cfg, new io.github.randomcodespace.iq.query.StatsService());
188188
when(graphStore.getTopology()).thenReturn(buildTopologyResponse());
189189

190190
Map<String, Object> result = service.getTopology();
@@ -199,7 +199,7 @@ void queryServiceGetTopologyDelegatesToGraphStore() {
199199
@Test
200200
void queryServiceGetTopologyReturnsServicesInfraConnections() {
201201
CodeIqConfig cfg = new CodeIqConfig();
202-
QueryService service = new QueryService(graphStore, cfg);
202+
QueryService service = new QueryService(graphStore, cfg, new io.github.randomcodespace.iq.query.StatsService());
203203
Map<String, Object> topology = buildTopologyResponse();
204204
when(graphStore.getTopology()).thenReturn(topology);
205205

src/test/java/io/github/randomcodespace/iq/mcp/McpToolsTest.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -450,10 +450,12 @@ void traceImpactShouldUseCustomDepth() throws IOException {
450450

451451
@Test
452452
void findRelatedEndpointsShouldSearchAndReturn() throws IOException {
453-
List<Map<String, Object>> searchResults = List.of(
454-
Map.of("id", "n1", "kind", "endpoint")
455-
);
456-
when(queryService.searchGraph("UserService", 50)).thenReturn(searchResults);
453+
Map<String, Object> endpointResult = new java.util.LinkedHashMap<>();
454+
endpointResult.put("identifier", "UserService");
455+
endpointResult.put("endpoints", List.of(Map.of("id", "ep1", "kind", "endpoint")));
456+
endpointResult.put("count", 1);
457+
endpointResult.put("searched_nodes", 3);
458+
when(queryService.findRelatedEndpoints("UserService")).thenReturn(endpointResult);
457459

458460
String result = mcpTools.findRelatedEndpoints("UserService");
459461
Map<String, Object> parsed = parseJson(result);
@@ -496,21 +498,24 @@ void readFileShouldReadContent(@TempDir Path tempDir) throws IOException {
496498
}
497499

498500
@Test
499-
void readFileShouldRejectPathTraversal(@TempDir Path tempDir) {
501+
void readFileShouldRejectPathTraversal(@TempDir Path tempDir) throws IOException {
500502
config.setRootPath(tempDir.toString());
501503

502504
String result = mcpTools.readFile("../../etc/passwd", null, null);
505+
Map<String, Object> parsed = parseJson(result);
503506

504-
assertEquals("Error: Path traversal detected", result);
507+
assertEquals("Path traversal detected", parsed.get("error"));
505508
}
506509

507510
@Test
508-
void readFileShouldHandleMissingFile(@TempDir Path tempDir) {
511+
void readFileShouldHandleMissingFile(@TempDir Path tempDir) throws IOException {
509512
config.setRootPath(tempDir.toString());
510513

511514
String result = mcpTools.readFile("nonexistent.txt", null, null);
515+
Map<String, Object> parsed = parseJson(result);
512516

513-
assertTrue(result.startsWith("Error:"));
517+
assertNotNull(parsed.get("error"));
518+
assertTrue(parsed.get("error").toString().contains("Failed to read file"));
514519
}
515520

516521
@Test

src/test/java/io/github/randomcodespace/iq/query/QueryServiceTest.java

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@ class QueryServiceTest {
2828
private GraphStore graphStore;
2929

3030
private CodeIqConfig config;
31+
private StatsService statsService;
3132
private QueryService service;
3233

3334
@BeforeEach
3435
void setUp() {
3536
config = new CodeIqConfig();
3637
config.setMaxDepth(10);
3738
config.setMaxRadius(10);
38-
service = new QueryService(graphStore, config);
39+
statsService = new StatsService();
40+
service = new QueryService(graphStore, config, statsService);
3941
}
4042

4143
private CodeNode makeNode(String id, NodeKind kind, String label) {
@@ -59,30 +61,43 @@ private CodeNode makeNodeWithEdge(String id, NodeKind kind, String label,
5961

6062
@Test
6163
void getStatsShouldReturnNodeAndEdgeCounts() {
62-
when(graphStore.count()).thenReturn(2L);
63-
when(graphStore.countEdges()).thenReturn(1L);
64-
when(graphStore.countDistinctFiles()).thenReturn(5L);
64+
var endpoint = makeNodeWithEdge("ep:1", NodeKind.ENDPOINT, "GET /users",
65+
"svc:1", EdgeKind.CALLS);
66+
endpoint.getProperties().put("http_method", "GET");
67+
endpoint.setFilePath("src/Main.java");
68+
var cls = makeNode("cls:1", NodeKind.CLASS, "UserService");
69+
cls.setFilePath("src/UserService.java");
70+
cls.setEdges(new ArrayList<>());
71+
when(graphStore.findAll()).thenReturn(List.of(endpoint, cls));
6572
when(graphStore.countNodesByKind()).thenReturn(List.of(
6673
Map.of("kind", "endpoint", "cnt", 1L),
6774
Map.of("kind", "class", "cnt", 1L)));
6875
when(graphStore.countNodesByLayer()).thenReturn(List.of(
6976
Map.of("layer", "backend", "cnt", 2L)));
70-
when(graphStore.countByFileExtension()).thenReturn(List.of(
71-
Map.of("ext", "java", "cnt", 3L),
72-
Map.of("ext", "py", "cnt", 2L)));
7377

7478
Map<String, Object> stats = service.getStats();
7579

76-
// ComputedStatsResponse format
80+
// ComputedStatsResponse format — graph section from StatsService
7781
@SuppressWarnings("unchecked")
7882
Map<String, Object> graph = (Map<String, Object>) stats.get("graph");
79-
assertEquals(2L, graph.get("nodes"));
80-
assertEquals(1L, graph.get("edges"));
81-
assertEquals(5L, graph.get("files"));
83+
assertEquals(2, graph.get("nodes"));
84+
assertEquals(1, graph.get("edges"));
85+
assertEquals(2L, graph.get("files"));
8286
assertNotNull(stats.get("languages"));
87+
assertNotNull(stats.get("frameworks"));
88+
assertNotNull(stats.get("infra"));
89+
assertNotNull(stats.get("connections"));
90+
assertNotNull(stats.get("auth"));
91+
assertNotNull(stats.get("architecture"));
92+
// REST endpoint detection
93+
@SuppressWarnings("unchecked")
94+
Map<String, Object> connections = (Map<String, Object>) stats.get("connections");
95+
@SuppressWarnings("unchecked")
96+
Map<String, Object> rest = (Map<String, Object>) connections.get("rest");
97+
assertEquals(1L, rest.get("total"));
8398
// Backward compat
84-
assertEquals(2L, stats.get("node_count"));
85-
assertEquals(1L, stats.get("edge_count"));
99+
assertEquals(2, stats.get("node_count"));
100+
assertEquals(1, stats.get("edge_count"));
86101
assertNotNull(stats.get("nodes_by_kind"));
87102
assertNotNull(stats.get("nodes_by_layer"));
88103
}

0 commit comments

Comments
 (0)