Skip to content

Commit 7f64496

Browse files
aksOpsclaude
andcommitted
Fix serve to use Neo4j as primary query engine after enrich
Architecture fix: - serve now queries Neo4j (enriched graph) as primary data source - H2 fallback only when Neo4j is unavailable - GraphBootstrapper auto-loads H2 into Neo4j on serve startup if graph empty - CodeIqApplication sets graph.db path from codebase root for serve command Controllers: - GraphController: Neo4j-first for all 13 data endpoints, H2 fallback - TopologyController: loads from Neo4j (has SERVICE nodes from enrich) - FlowController: creates FlowEngine from GraphStore when Neo4j available Startup: - Suppress MCP scanner warnings (logging.level config) - Suppress BeanPostProcessor warnings - Reduce startup log verbosity in serving profile Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b9c28fc commit 7f64496

9 files changed

Lines changed: 380 additions & 48 deletions

File tree

src/main/java/io/github/randomcodespace/iq/CodeIqApplication.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,32 @@ public static void main(String[] args) {
6666
if (portStr != null) {
6767
System.setProperty("server.port", portStr);
6868
}
69+
70+
// Resolve codebase root so Neo4j points to the correct graph.db
71+
String codebasePath = extractPositionalArg(args, "serve");
72+
java.nio.file.Path root = java.nio.file.Path.of(
73+
codebasePath != null ? codebasePath : "."
74+
).toAbsolutePath().normalize();
75+
System.setProperty("codeiq.root-path", root.toString());
76+
77+
// Check if enrich has been run (graph.db exists), otherwise
78+
// check for the --graph flag override
79+
String graphOverride = extractFlag(args, "--graph");
80+
java.nio.file.Path graphDbPath;
81+
if (graphOverride != null) {
82+
graphDbPath = java.nio.file.Path.of(graphOverride).toAbsolutePath().normalize();
83+
} else {
84+
graphDbPath = root.resolve(".osscodeiq/graph.db");
85+
}
86+
87+
if (java.nio.file.Files.isDirectory(graphDbPath)) {
88+
// Enriched Neo4j graph exists -- point Neo4j config to it
89+
System.setProperty("codeiq.graph.path", graphDbPath.toString());
90+
} else {
91+
// No enriched graph -- Neo4j will start with an empty db,
92+
// GraphBootstrapper will auto-load from H2 cache if available
93+
System.setProperty("codeiq.graph.path", graphDbPath.toString());
94+
}
6995
} else if (isIndex) {
7096
app.setAdditionalProfiles("indexing");
7197
// Index command: no web server, no Neo4j

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import io.github.randomcodespace.iq.config.CodeIqConfig;
55
import io.github.randomcodespace.iq.flow.FlowEngine;
66
import io.github.randomcodespace.iq.flow.FlowModels.FlowDiagram;
7+
import io.github.randomcodespace.iq.graph.GraphStore;
78
import io.github.randomcodespace.iq.model.CodeNode;
9+
import org.springframework.beans.factory.annotation.Autowired;
810
import org.springframework.context.annotation.Profile;
911
import org.springframework.http.HttpStatus;
1012
import org.springframework.http.MediaType;
@@ -33,10 +35,14 @@
3335
public class FlowController {
3436

3537
private final FlowEngine flowEngine;
38+
private final GraphStore graphStore;
3639
private final CodeIqConfig config;
3740

38-
public FlowController(Optional<FlowEngine> flowEngine, CodeIqConfig config) {
41+
public FlowController(Optional<FlowEngine> flowEngine,
42+
@Autowired(required = false) GraphStore graphStore,
43+
CodeIqConfig config) {
3944
this.flowEngine = flowEngine.orElse(null);
45+
this.graphStore = graphStore;
4046
this.config = config;
4147
}
4248

@@ -106,6 +112,11 @@ private FlowEngine resolveEngine() {
106112
return flowEngine;
107113
}
108114

115+
// Prefer GraphStore (Neo4j) when available and populated
116+
if (graphStore != null && graphStore.count() > 0) {
117+
return new FlowEngine(graphStore);
118+
}
119+
109120
// Fall back to H2 cache
110121
Path root = Path.of(config.getRootPath()).toAbsolutePath().normalize();
111122
Path cachePath = root.resolve(config.getCacheDir()).resolve("analysis-cache.db");

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

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.github.randomcodespace.iq.analyzer.Analyzer;
55
import io.github.randomcodespace.iq.cache.AnalysisCache;
66
import io.github.randomcodespace.iq.config.CodeIqConfig;
7+
import io.github.randomcodespace.iq.graph.GraphStore;
78
import io.github.randomcodespace.iq.model.CodeEdge;
89
import io.github.randomcodespace.iq.model.CodeNode;
910
import io.github.randomcodespace.iq.model.NodeKind;
@@ -42,6 +43,7 @@
4243
public class GraphController {
4344

4445
private final QueryService queryService;
46+
private final GraphStore graphStore;
4547
private final Analyzer analyzer;
4648
private final CodeIqConfig config;
4749
private final StatsService statsService;
@@ -51,10 +53,12 @@ public class GraphController {
5153
private volatile List<CodeEdge> cachedEdges;
5254

5355
public GraphController(@org.springframework.beans.factory.annotation.Autowired(required = false) QueryService queryService,
56+
@org.springframework.beans.factory.annotation.Autowired(required = false) GraphStore graphStore,
5457
Analyzer analyzer,
5558
CodeIqConfig config, StatsService statsService,
5659
TopologyService topologyService) {
5760
this.queryService = queryService;
61+
this.graphStore = graphStore;
5862
this.analyzer = analyzer;
5963
this.config = config;
6064
this.statsService = statsService;
@@ -87,32 +91,61 @@ private void invalidateCache() {
8791
cachedEdges = null;
8892
}
8993

94+
/**
95+
* Get nodes from the best available source: Neo4j first, H2 fallback.
96+
* Returns null if neither source has data.
97+
*/
98+
private List<CodeNode> getEffectiveNodes() {
99+
if (graphStore != null && useNeo4j()) {
100+
return graphStore.findAll();
101+
}
102+
ensureCacheLoaded();
103+
return cachedNodes;
104+
}
105+
106+
/**
107+
* Get edges from the best available source: Neo4j first, H2 fallback.
108+
* Returns null if neither source has data.
109+
*/
110+
private List<CodeEdge> getEffectiveEdges() {
111+
if (graphStore != null && useNeo4j()) {
112+
// Collect all edges from Neo4j nodes
113+
return graphStore.findAll().stream()
114+
.flatMap(n -> n.getEdges().stream())
115+
.toList();
116+
}
117+
ensureCacheLoaded();
118+
return cachedEdges;
119+
}
120+
90121
@GetMapping("/stats")
91122
public Map<String, Object> getStats() {
123+
if (useNeo4j()) {
124+
return queryService.getStats();
125+
}
92126
ensureCacheLoaded();
93127
if (cachedNodes != null) {
94128
return statsService.computeStats(cachedNodes, cachedEdges);
95129
}
96-
if (queryService != null) {
97-
return queryService.getStats();
98-
}
99130
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
100131
"No analysis data available. Run analyze first.");
101132
}
102133

103134
@GetMapping("/stats/detailed")
104135
public Map<String, Object> getDetailedStats(
105136
@RequestParam(defaultValue = "all") String category) {
106-
ensureCacheLoaded();
107-
if (cachedNodes == null) {
137+
// Use Neo4j-backed stats if available, fall back to H2
138+
List<CodeNode> nodes = getEffectiveNodes();
139+
List<CodeEdge> edges = getEffectiveEdges();
140+
if (nodes == null) {
108141
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
109-
"No analysis cache found. Run analyze first.");
142+
"No analysis data found. Run analyze first.");
110143
}
111144

112145
if ("all".equalsIgnoreCase(category)) {
113-
return statsService.computeStats(cachedNodes, cachedEdges);
146+
return statsService.computeStats(nodes, edges);
114147
}
115-
Map<String, Object> catStats = statsService.computeCategory(cachedNodes, cachedEdges, category);
148+
Map<String, Object> catStats = statsService.computeCategory(nodes, edges, category);
116149
if (catStats == null) {
117150
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
118151
"Unknown category: " + category);
@@ -124,6 +157,9 @@ public Map<String, Object> getDetailedStats(
124157

125158
@GetMapping("/kinds")
126159
public Map<String, Object> listKinds() {
160+
if (useNeo4j()) {
161+
return queryService.listKinds();
162+
}
127163
ensureCacheLoaded();
128164
if (cachedNodes != null) {
129165
Map<String, Long> kindCounts = cachedNodes.stream()
@@ -144,7 +180,6 @@ public Map<String, Object> listKinds() {
144180
result.put("total", cachedNodes.size());
145181
return result;
146182
}
147-
if (queryService != null) return queryService.listKinds();
148183
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
149184
"No data available. Run analyze first.");
150185
}
@@ -155,6 +190,9 @@ public Map<String, Object> nodesByKind(
155190
@RequestParam(defaultValue = "50") int limit,
156191
@RequestParam(defaultValue = "0") int offset) {
157192
int safeLimit = Math.min(limit, 1000);
193+
if (useNeo4j()) {
194+
return queryService.nodesByKind(kind, safeLimit, offset);
195+
}
158196
ensureCacheLoaded();
159197
if (cachedNodes != null) {
160198
List<CodeNode> filtered = cachedNodes.stream()
@@ -170,7 +208,6 @@ public Map<String, Object> nodesByKind(
170208
result.put("nodes", filtered.subList(start, end).stream().map(this::nodeToMap).toList());
171209
return result;
172210
}
173-
if (queryService != null) return queryService.nodesByKind(kind, safeLimit, offset);
174211
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
175212
"No data available. Run analyze first.");
176213
}
@@ -181,6 +218,9 @@ public Map<String, Object> listNodes(
181218
@RequestParam(defaultValue = "100") int limit,
182219
@RequestParam(defaultValue = "0") int offset) {
183220
int safeLimit = Math.min(limit, 1000);
221+
if (useNeo4j()) {
222+
return queryService.listNodes(kind, safeLimit, offset);
223+
}
184224
ensureCacheLoaded();
185225
if (cachedNodes != null) {
186226
List<CodeNode> filtered = cachedNodes;
@@ -198,13 +238,15 @@ public Map<String, Object> listNodes(
198238
result.put("limit", safeLimit);
199239
return result;
200240
}
201-
if (queryService != null) return queryService.listNodes(kind, safeLimit, offset);
202241
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
203242
"No data available. Run analyze first.");
204243
}
205244

206245
@GetMapping("/nodes/find")
207246
public List<Map<String, Object>> findNode(@RequestParam String q) {
247+
if (graphStore != null && useNeo4j()) {
248+
return topologyService.findNode(q, graphStore.findAll());
249+
}
208250
ensureCacheLoaded();
209251
if (cachedNodes == null) {
210252
return List.of();
@@ -214,7 +256,12 @@ public List<Map<String, Object>> findNode(@RequestParam String q) {
214256

215257
@GetMapping("/nodes/{nodeId}/detail")
216258
public Map<String, Object> nodeDetail(@PathVariable String nodeId) {
217-
// Try H2 cache first
259+
// Try Neo4j first for rich detail with edges
260+
if (useNeo4j()) {
261+
Map<String, Object> result = queryService.nodeDetailWithEdges(nodeId);
262+
if (result != null) return result;
263+
}
264+
// Fall back to H2 cache
218265
ensureCacheLoaded();
219266
if (cachedNodes != null) {
220267
return cachedNodes.stream()
@@ -223,12 +270,7 @@ public Map<String, Object> nodeDetail(@PathVariable String nodeId) {
223270
.map(this::nodeToMap)
224271
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Node not found: " + nodeId));
225272
}
226-
if (queryService == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Node not found: " + nodeId);
227-
Map<String, Object> result = queryService.nodeDetailWithEdges(nodeId);
228-
if (result == null) {
229-
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Node not found: " + nodeId);
230-
}
231-
return result;
273+
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Node not found: " + nodeId);
232274
}
233275

234276
@GetMapping("/nodes/{nodeId}/neighbors")
@@ -245,6 +287,9 @@ public Map<String, Object> listEdges(
245287
@RequestParam(defaultValue = "100") int limit,
246288
@RequestParam(defaultValue = "0") int offset) {
247289
int safeLimit = Math.min(limit, 1000);
290+
if (useNeo4j()) {
291+
return queryService.listEdges(kind, safeLimit, offset);
292+
}
248293
ensureCacheLoaded();
249294
if (cachedEdges != null) {
250295
List<CodeEdge> filtered = cachedEdges;
@@ -261,8 +306,8 @@ public Map<String, Object> listEdges(
261306
result.put("total", filtered.size());
262307
return result;
263308
}
264-
requireQueryService();
265-
return queryService.listEdges(kind, safeLimit, offset);
309+
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
310+
"No data available. Run analyze first.");
266311
}
267312

268313
@GetMapping("/ego/{center}")
@@ -329,7 +374,12 @@ public ResponseEntity<?> findDeadCode(
329374
@RequestParam(defaultValue = "100") int limit) {
330375
int safeLimit = Math.min(limit, 1000);
331376

332-
// Try H2 cache first for dead code analysis
377+
// Try Neo4j first
378+
if (useNeo4j()) {
379+
return ResponseEntity.ok(queryService.findDeadCode(kind, safeLimit));
380+
}
381+
382+
// Fall back to H2 cache
333383
ensureCacheLoaded();
334384
if (cachedNodes != null && cachedEdges != null) {
335385
List<CodeNode> candidates = cachedNodes;
@@ -368,10 +418,6 @@ public ResponseEntity<?> findDeadCode(
368418
return ResponseEntity.ok(result);
369419
}
370420

371-
// Fall back to QueryService (Neo4j)
372-
if (queryService != null) {
373-
return ResponseEntity.ok(queryService.findDeadCode(kind, safeLimit));
374-
}
375421
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
376422
"No data available. Run analyze first.");
377423
}
@@ -396,16 +442,29 @@ public List<Map<String, Object>> searchGraph(
396442
@RequestParam String q,
397443
@RequestParam(defaultValue = "50") int limit) {
398444
int safeLimit = Math.min(limit, 1000);
399-
// Search from H2 cache
445+
// Search via Neo4j first
446+
if (useNeo4j()) {
447+
return queryService.searchGraph(q, safeLimit);
448+
}
449+
// Fall back to H2 cache
400450
ensureCacheLoaded();
401451
if (cachedNodes != null) {
402452
return topologyService.findNode(q, cachedNodes);
403453
}
404-
if (queryService != null) return queryService.searchGraph(q, safeLimit);
405454
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
406455
"No data available. Run analyze first.");
407456
}
408457

458+
/**
459+
* Check whether Neo4j (via QueryService/GraphStore) is available for queries.
460+
* When true, Neo4j is the primary data source (enriched graph with SERVICE
461+
* nodes, layer classifications, linker edges).
462+
* When false, falls back to H2 cache (basic indexed data only).
463+
*/
464+
private boolean useNeo4j() {
465+
return queryService != null;
466+
}
467+
409468
private void requireQueryService() {
410469
if (queryService == null) {
411470
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,

0 commit comments

Comments
 (0)