From 88ecec5c58eb53bebdad9eecc2a02b0b87fb44c8 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 17 Apr 2026 11:05:39 +0000 Subject: [PATCH 1/2] fix(serve): invoke GraphBootstrapper directly instead of via dead ApplicationReadyEvent listener GraphBootstrapper's H2->Neo4j fallback was registered for @EventListener(ApplicationReadyEvent.class), but that event never fires during a normal serve run because ServeCommand.call() blocks in Thread.join() as a CommandLineRunner. Consequence: if someone shipped a bundle with only the H2 analysis cache (no enriched Neo4j graph) and ran `code-iq serve`, Neo4j would stay empty forever and graph-traversal queries would return nothing. Not observed in our pipeline today only because `enrich` always runs before `serve`. Fix: - Drop @EventListener from bootstrapNeo4jFromCache() and its associated imports (ApplicationReadyEvent, @EventListener). - ServeCommand.call() now calls graphBootstrapper.bootstrapNeo4jFromCache() explicitly before the Neo4j status report, so the advertised node/edge counts reflect post-bootstrap state. Injection is Optional since the bean only exists in the "serving" profile (matches the existing @Profile + @ConditionalOnBean guards). - Class javadoc updated to document the new invocation model and the reason the event-listener path did not work. The method is idempotent (guards on count>0 and on missing H2 file), so moving the invocation is safe even in scenarios where Neo4j is already populated by `enrich`. Tests: existing GraphBootstrapperTest still passes unchanged (it calls the method directly). Integration verification: # full pipeline to populate both H2 + Neo4j ./scripts/baseline/run-pipeline.sh spring-petclinic # wipe ONLY Neo4j; keep H2 rm -rf .seeds/spring-petclinic/.code-iq/graph # re-run serve java -jar target/code-iq-*-cli.jar serve .seeds/spring-petclinic --port 18080 & # wait for /actuator/health=200, then: curl http://127.0.0.1:18080/api/stats Before this fix: Neo4j empty, stats=0. After this fix: log shows "Bootstrapped Neo4j with 634 nodes and 604 edges from H2 cache", /api/stats returns 677 nodes / 604 edges / 65 files. --- .../randomcodespace/iq/cli/ServeCommand.java | 14 ++++++++++++++ .../iq/config/GraphBootstrapper.java | 11 +++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java index 39bf4526..e5276473 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java @@ -1,6 +1,7 @@ package io.github.randomcodespace.iq.cli; import io.github.randomcodespace.iq.config.CodeIqConfig; +import io.github.randomcodespace.iq.config.GraphBootstrapper; import io.github.randomcodespace.iq.graph.GraphStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,6 +64,10 @@ public class ServeCommand implements Callable { @Autowired private ApplicationEventPublisher events; + // Optional: only present in the "serving" profile (same conditions as the bean). + @Autowired(required = false) + private GraphBootstrapper graphBootstrapper; + @Override public Integer call() { Path root = path.toAbsolutePath().normalize(); @@ -72,6 +77,15 @@ public Integer call() { } NumberFormat nf = NumberFormat.getIntegerInstance(Locale.US); + // Bootstrap Neo4j from the H2 analysis cache if Neo4j is empty. This is + // a no-op when enrich has already run (guarded internally by a count>0 + // check) and when the H2 cache file is missing. Must happen before the + // status report below so the advertised node/edge counts are truthful. + // See GraphBootstrapper javadoc for why this is not an @EventListener. + if (graphBootstrapper != null) { + graphBootstrapper.bootstrapNeo4jFromCache(); + } + // Report Neo4j graph status if (graphStore != null) { try { diff --git a/src/main/java/io/github/randomcodespace/iq/config/GraphBootstrapper.java b/src/main/java/io/github/randomcodespace/iq/config/GraphBootstrapper.java index e1cb2f07..283686fc 100644 --- a/src/main/java/io/github/randomcodespace/iq/config/GraphBootstrapper.java +++ b/src/main/java/io/github/randomcodespace/iq/config/GraphBootstrapper.java @@ -7,9 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; -import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import java.nio.file.Files; @@ -25,7 +23,13 @@ * {@code serve} (which needs Neo4j for graph traversal queries like shortest path, * cycles, impact trace, ego graph, and neighbors). *

- * Runs after the Spring context is fully ready, so Neo4j and GraphStore are available. + * Invoked explicitly by {@link io.github.randomcodespace.iq.cli.ServeCommand} + * before the server reports "Server started", so the advertised node/edge counts + * reflect the bootstrapped state. Previously triggered via + * {@code @EventListener(ApplicationReadyEvent.class)}, but that event never fires + * because {@code ServeCommand.call()} blocks (as a {@code CommandLineRunner}) and + * Spring's ready-event publication waits for runners to return. + *

* Only active in the "serving" profile when GraphStore is present. */ @Component @@ -43,7 +47,6 @@ public GraphBootstrapper(GraphStore graphStore, CodeIqConfig config) { this.config = config; } - @EventListener(ApplicationReadyEvent.class) public void bootstrapNeo4jFromCache() { // Skip bootstrap in read-only mode (no writes allowed) if (config.isReadOnly()) { From 1c227bcbfbe58d764206bfc92da1618a24b26a1c Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 17 Apr 2026 11:06:13 +0000 Subject: [PATCH 2/2] docs(baseline): mark GraphBootstrapper dead-listener gap RESOLVED --- docs/superpowers/baselines/2026-04-17/BASELINE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/superpowers/baselines/2026-04-17/BASELINE.md b/docs/superpowers/baselines/2026-04-17/BASELINE.md index 8cb4b8b9..71260720 100644 --- a/docs/superpowers/baselines/2026-04-17/BASELINE.md +++ b/docs/superpowers/baselines/2026-04-17/BASELINE.md @@ -239,6 +239,9 @@ Ordered by severity. Each item cites the raw artifact it was derived from. - **`GraphHealthIndicator` reports `OUT_OF_SERVICE` (503) even when the graph is loaded.** Discovered during the pipeline smoke-test fix. `/actuator/health` body: `{"groups":["liveness","readiness"],"status":"OUT_OF_SERVICE"}`. The server is fully functional (`/api/stats` returns real data) but the health indicator makes `/actuator/health` unusable as a readiness probe for orchestrators (K8s, Compose, CI). Fix in `src/main/java/io/github/randomcodespace/iq/health/GraphHealthIndicator.java`. Low for baseline use; High when we start Dockerizing or targeting K8s. - **RESOLVED (2026-04-17, branch `phase-a/fix-graph-health`)**: Root cause was *not* in `GraphHealthIndicator` (which correctly returns UP when nodes>0). It was in `ServeCommand`: the CLI blocks on `Thread.currentThread().join()` inside Spring Boot's `CommandLineRunner.run()`, which prevents `ApplicationReadyEvent` from ever firing. Without that event, Spring's default readiness publisher never flips `ReadinessState` from `REFUSING_TRAFFIC` (503 `OUT_OF_SERVICE`) to `ACCEPTING_TRAFFIC` (200 `UP`). Fix: `ServeCommand` now explicitly publishes `AvailabilityChangeEvent` for `LivenessState.CORRECT` + `ReadinessState.ACCEPTING_TRAFFIC` before blocking, via a new `markReady()` method (unit-tested). Verified end-to-end: `health_http` is now 200 on both seeds (petclinic ready 13s, express ready 14s; status "UP"). Follow-up filed: `GraphBootstrapper`'s `@EventListener(ApplicationReadyEvent.class)` is effectively dead code for the same reason β€” only noticed because enrich always runs before serve in our pipeline, so the bootstrap fallback never actually needs to fire. +- **`GraphBootstrapper` dead listener** (Low severity, follow-up to the health fix). The H2β†’Neo4j bootstrap path was triggered via `@EventListener(ApplicationReadyEvent.class)`, which never fires while `ServeCommand` blocks as a `CommandLineRunner`. + - **RESOLVED (2026-04-17, branch `phase-a/fix-bootstrap-listener`)**: dropped the `@EventListener` annotation and instead invoke `GraphBootstrapper.bootstrapNeo4jFromCache()` explicitly from `ServeCommand.call()` before the status report. The method is idempotent (guards on Neo4j count>0 and on missing H2 cache file). Verified end-to-end: after running a full pipeline then wiping only `.code-iq/graph/` (Neo4j) while keeping `.code-iq/cache/` (H2), `serve` logs `Bootstrapped Neo4j with 634 nodes and 604 edges from H2 cache` and `/api/stats` returns real data (nodes=677, edges=604, files=65). Existing `GraphBootstrapperTest` cases still pass unchanged (they always called the method directly). + - **SpotBugs: 8 HIGH-priority findings (priority=1) + 1,484 at priority=2.** Total 1,492. HIGH findings must be triaged individually (read `raw/spotbugs.xml`). Noise-dominant rules (`NM_METHOD_NAMING_CONVENTION`=730, `SF_SWITCH_NO_DEFAULT`=448) should be filtered via a SpotBugs exclude file so real signal surfaces; real-concern patterns that deserve review now: `NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE` (26), `BC_UNCONFIRMED_CAST` (55), `UL_UNRELEASED_LOCK_EXCEPTION_PATH` (1), `WMI_WRONG_MAP_ITERATOR` (2), `ES_COMPARING_STRINGS_WITH_EQ` (2), `MT_CORRECTNESS` category (1). - Raw: `raw/spotbugs.xml`, `raw/spotbugs-summary.json`. - **RESOLVED (2026-04-17, branch `phase-a/fixups-spotbugs`)**: Added `spotbugs-exclude.xml` covering ANTLR-generated parsers and global noise rules (`NM_METHOD_NAMING_CONVENTION`, `SF_SWITCH_NO_DEFAULT`, `EI_EXPOSE_REP`/`EI_EXPOSE_REP2`, `MS_PKGPROTECT`/`MS_FINAL_PKGPROTECT`), wired via `pom.xml`. Fixed all 8 priority-1 findings in codeiq code (UTF-8 in `Analyzer.getGitHead`, narrowed catch in `IndexCommand`, dead-store removed in `PluginsCommand`, `.equals()` in `AntlrParserFactory` + `CSharpPreprocessorParserBase`, try-finally unlock in `AnalysisCache.removeFile`, merged duplicate branches in `CodeIqApplication`, removed dead `BundleCommand.writeEntry` overload, `entrySet()` iteration in `PluginsCommand` + `GitLabCiDetector`, narrowed `VersionCommand` catch). **Final: 1,492 β†’ 38 (-97.5%); priority-1: 8 β†’ 0.** Remaining 38 are priority-2 STYLE/BAD_PRACTICE; no CORRECTNESS/MT_CORRECTNESS/SECURITY left. Next-pass candidates: 26 `NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE`. Post-triage summary: `raw/spotbugs-summary-after-triage.json`.