Test.java` | Add resolved-mode + fallback-mode + mixed-mode assertions. |
-| `pom.xml` | Add `javaparser-symbol-solver-core` (latest stable matching `javaparser-core`) + `net.jqwik:jqwik` (test scope, pending license OK). PIT in non-default profile. |
-| `docs/codeiq.yml.example` | Document `intelligence.symbol_resolution.java.*` keys. |
-| `CHANGELOG.md` | Expand `[Unreleased]` entry once features are integrated. |
-| `CLAUDE.md` | "Gotchas" addition: confidence/provenance is now mandatory; resolver pass exists; cache version 5. |
-| `PROJECT_SUMMARY.md` | Tech stack + Gotchas update. |
-
----
-
-## How to use this plan
-
-- Each task is one logical commit (or small commit chain).
-- Each step inside a task is 2–5 minutes and ends with verifiable output.
-- Tests come first (TDD). Run them, see them fail, then implement, run them, see them pass, commit.
-- Determinism tests are mandatory for every detector that gets migrated (Phase 6) and for the resolver itself (Task 30 / Layer 6).
-- Frequent commits — one per task minimum, sometimes more.
-- Unless noted, **all commands run from the repo root** `/home/dev/projects/codeiq`.
-
-**Resume rule:** if interrupted mid-task, the next session re-runs the test command from the unfinished step to confirm where it stopped, then continues.
-
----
-
-## Phase 1 — Schema foundation (Tasks 1–7)
-
-### Task 1: `Confidence` enum
-
-**Files:**
-- Create: `src/main/java/io/github/randomcodespace/iq/model/Confidence.java`
-- Test: `src/test/java/io/github/randomcodespace/iq/model/ConfidenceTest.java`
-
-- [ ] **Step 1: Write the failing test**
-
-```java
-// src/test/java/io/github/randomcodespace/iq/model/ConfidenceTest.java
-package io.github.randomcodespace.iq.model;
-
-import org.junit.jupiter.api.Test;
-import static org.junit.jupiter.api.Assertions.*;
-
-class ConfidenceTest {
-
- @Test
- void scoreMappingIsStable() {
- assertEquals(0.6, Confidence.LEXICAL.score(), 1e-9);
- assertEquals(0.8, Confidence.SYNTACTIC.score(), 1e-9);
- assertEquals(0.95, Confidence.RESOLVED.score(), 1e-9);
- }
-
- @Test
- void naturalOrderingMatchesScore() {
- assertTrue(Confidence.LEXICAL.compareTo(Confidence.SYNTACTIC) < 0);
- assertTrue(Confidence.SYNTACTIC.compareTo(Confidence.RESOLVED) < 0);
- }
-
- @Test
- void valueOfNullIsRejected() {
- assertThrows(NullPointerException.class, () -> Confidence.fromString(null));
- }
-
- @Test
- void fromStringIsCaseInsensitive() {
- assertEquals(Confidence.RESOLVED, Confidence.fromString("resolved"));
- assertEquals(Confidence.RESOLVED, Confidence.fromString("RESOLVED"));
- assertEquals(Confidence.LEXICAL, Confidence.fromString("LeXiCaL"));
- }
-
- @Test
- void fromStringRejectsUnknown() {
- assertThrows(IllegalArgumentException.class, () -> Confidence.fromString("perfect"));
- }
-}
-```
-
-- [ ] **Step 2: Run test to verify it fails**
-
-```bash
-mvn test -Dtest=ConfidenceTest -Dfrontend.skip=true -Ddependency-check.skip=true -q
-```
-
-Expected: compile error — `Confidence` does not exist.
-
-- [ ] **Step 3: Write minimal implementation**
-
-```java
-// src/main/java/io/github/randomcodespace/iq/model/Confidence.java
-package io.github.randomcodespace.iq.model;
-
-import java.util.Objects;
-
-/**
- * Confidence in the truth of a node or edge, based on the parser pipeline that
- * produced it. Lower means the assertion is from text patterns; higher means
- * the assertion is backed by parsed structure or resolved symbol types.
- *
- * Comparable: {@code LEXICAL} < {@code SYNTACTIC} < {@code RESOLVED}.
- *
- *
Numeric mapping (via {@link #score()}) is stable and intended for Cypher /
- * MCP / SPA filtering. The enum itself is the authoritative form.
- */
-public enum Confidence {
- /** Pattern-only match (regex). */
- LEXICAL(0.6),
- /** AST or parse tree, no symbol resolution. */
- SYNTACTIC(0.8),
- /** Resolved via a {@code SymbolResolver}. */
- RESOLVED(0.95);
-
- private final double score;
-
- Confidence(double score) {
- this.score = score;
- }
-
- public double score() {
- return score;
- }
-
- public static Confidence fromString(String value) {
- Objects.requireNonNull(value, "Confidence value must not be null");
- for (Confidence c : values()) {
- if (c.name().equalsIgnoreCase(value)) {
- return c;
- }
- }
- throw new IllegalArgumentException("Unknown Confidence: " + value);
- }
-}
-```
-
-- [ ] **Step 4: Run test to verify it passes**
-
-```bash
-mvn test -Dtest=ConfidenceTest -Dfrontend.skip=true -Ddependency-check.skip=true -q
-```
-
-Expected: 5/5 tests pass.
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add src/main/java/io/github/randomcodespace/iq/model/Confidence.java \
- src/test/java/io/github/randomcodespace/iq/model/ConfidenceTest.java
-git commit -m "feat(model): add Confidence enum (LEXICAL/SYNTACTIC/RESOLVED)
-
-Per sub-project 1 spec §5.3. Numeric score() mapping stable (0.6/0.8/0.95).
-Comparable by natural order. fromString() is case-insensitive and rejects
-null + unknown values.
-
-Co-Authored-By: Claude Opus 4.7 (1M context) "
-```
-
----
-
-### Task 2: Add `confidence` + `source` to `CodeNode`
-
-**Files:**
-- Modify: `src/main/java/io/github/randomcodespace/iq/model/CodeNode.java`
-- Test: existing `CodeNodeTest.java` (or create one if missing) — add round-trip assertion via `equals`/`hashCode`
-
-- [ ] **Step 1: Read current `CodeNode.java`** to see its shape (record vs class, builder vs constructor).
-
-```bash
-sed -n '1,80p' src/main/java/io/github/randomcodespace/iq/model/CodeNode.java
-```
-
-- [ ] **Step 2: Write failing test**
-
-```java
-// src/test/java/io/github/randomcodespace/iq/model/CodeNodeConfidenceTest.java
-package io.github.randomcodespace.iq.model;
-
-import org.junit.jupiter.api.Test;
-import static org.junit.jupiter.api.Assertions.*;
-
-class CodeNodeConfidenceTest {
-
- @Test
- void newNodeCarriesConfidenceAndSource() {
- CodeNode n = CodeNode.builder()
- .id("node:foo:class:Foo")
- .kind(NodeKind.CLASS)
- .label("Foo")
- .confidence(Confidence.SYNTACTIC)
- .source("MyDetector")
- .build();
- assertEquals(Confidence.SYNTACTIC, n.confidence());
- assertEquals("MyDetector", n.source());
- }
-
- @Test
- void confidenceDefaultsToLexicalIfUnset() {
- CodeNode n = CodeNode.builder()
- .id("node:foo:class:Foo")
- .kind(NodeKind.CLASS)
- .label("Foo")
- .source("MyDetector")
- .build();
- assertEquals(Confidence.LEXICAL, n.confidence(),
- "missing confidence falls back to LEXICAL — least committal");
- }
-
- @Test
- void sourceIsRequired() {
- assertThrows(IllegalStateException.class, () -> CodeNode.builder()
- .id("node:foo:class:Foo")
- .kind(NodeKind.CLASS)
- .label("Foo")
- .build(),
- "source is mandatory — every node knows which detector emitted it");
- }
-}
-```
-
-- [ ] **Step 3: Run test to verify it fails**
-
-```bash
-mvn test -Dtest=CodeNodeConfidenceTest -Dfrontend.skip=true -Ddependency-check.skip=true -q
-```
-
-Expected: compile error — `confidence(...)` and `source(...)` not on builder.
-
-- [ ] **Step 4: Add fields + builder methods to `CodeNode`**
-
-Add fields, builder setters, getter accessors, equals/hashCode coverage. Field defaults: `confidence = Confidence.LEXICAL`, `source` required (validated in builder).
-
-(Code shown verbatim once existing structure is read in Step 1; the change must preserve all existing tests by leaving every other field's behavior unchanged.)
-
-- [ ] **Step 5: Run all model tests to verify nothing else regressed**
-
-```bash
-mvn test -Dtest='io.github.randomcodespace.iq.model.*' -Dfrontend.skip=true -Ddependency-check.skip=true -q
-```
-
-Expected: all green.
-
-- [ ] **Step 6: Commit**
-
-```bash
-git add src/main/java/io/github/randomcodespace/iq/model/CodeNode.java \
- src/test/java/io/github/randomcodespace/iq/model/CodeNodeConfidenceTest.java
-git commit -m "feat(model): add confidence + source to CodeNode
-
-Per sub-project 1 spec §5.2. Both fields non-null. Confidence defaults to
-LEXICAL (least committal). Source is mandatory — every node knows which
-detector emitted it.
-
-Co-Authored-By: Claude Opus 4.7 (1M context) "
-```
-
----
-
-### Task 3: Add `confidence` + `source` to `CodeEdge`
-
-Same shape as Task 2, but on `CodeEdge`. Mirror the test class as `CodeEdgeConfidenceTest`. Same builder semantics.
-
-- [ ] **Step 1: Read current `CodeEdge.java`**
-- [ ] **Step 2: Write failing test (`CodeEdgeConfidenceTest`)** — mirror Task 2's three test cases on `CodeEdge.builder()`.
-- [ ] **Step 3: Run + see failure.**
-- [ ] **Step 4: Add fields + builder methods.**
-- [ ] **Step 5: Run all model tests.**
-- [ ] **Step 6: Commit:** `feat(model): add confidence + source to CodeEdge`.
-
----
-
-### Task 4: Round-trip `confidence` + `source` through Neo4j (write path)
-
-**Files:**
-- Modify: `src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java`
-- Test: `src/test/java/io/github/randomcodespace/iq/graph/GraphStoreConfidenceRoundTripTest.java` (new)
-
-- [ ] **Step 1: Write the failing test.**
-
-```java
-// src/test/java/io/github/randomcodespace/iq/graph/GraphStoreConfidenceRoundTripTest.java
-package io.github.randomcodespace.iq.graph;
-
-import io.github.randomcodespace.iq.model.*;
-import org.junit.jupiter.api.*;
-import org.junit.jupiter.api.io.TempDir;
-import java.nio.file.Path;
-import java.util.List;
-import static org.junit.jupiter.api.Assertions.*;
-
-class GraphStoreConfidenceRoundTripTest {
-
- @TempDir Path tmp;
- GraphStore store;
-
- @BeforeEach void setup() { store = GraphStore.openEmbedded(tmp.resolve("graph.db")); }
- @AfterEach void close() { store.close(); }
-
- @Test
- void confidenceAndSourceRoundTrip() {
- CodeNode in = CodeNode.builder()
- .id("node:Foo.java:class:Foo")
- .kind(NodeKind.CLASS).label("Foo")
- .confidence(Confidence.RESOLVED).source("SpringServiceDetector")
- .build();
- store.bulkSave(List.of(in), List.of());
-
- CodeNode out = store.findById("node:Foo.java:class:Foo").orElseThrow();
- assertEquals(Confidence.RESOLVED, out.confidence());
- assertEquals("SpringServiceDetector", out.source());
- }
-}
-```
-
-- [ ] **Step 2: Run; verify compile or assertion fail.**
-
-```bash
-mvn test -Dtest=GraphStoreConfidenceRoundTripTest -Dfrontend.skip=true -Ddependency-check.skip=true -q
-```
-
-Expected: assertion fails (fields written via existing path don't include confidence/source).
-
-- [ ] **Step 3: Update `GraphStore.bulkSave` to write `prop_confidence` and `prop_source`**, and `nodeFromNeo4j` / `edgeFromNeo4j` to read them. Defaults if missing in Neo4j: `Confidence.LEXICAL` and `"unknown"`.
-
-- [ ] **Step 4: Run round-trip test; verify pass.**
-- [ ] **Step 5: Run wider GraphStore test suite to ensure no regression.**
-
-```bash
-mvn test -Dtest='io.github.randomcodespace.iq.graph.*' -Dfrontend.skip=true -Ddependency-check.skip=true -q
-```
-
-- [ ] **Step 6: Commit:** `feat(graph): round-trip confidence + source through Neo4j`.
-
----
-
-### Task 5: H2 cache schema migration to v5
-
-**Files:**
-- Modify: `src/main/java/io/github/randomcodespace/iq/cache/AnalysisCache.java`
-- Test: existing `AnalysisCacheTest.java` (extend) + new round-trip case.
-
-- [ ] **Step 1: Failing test.** Add `confidence` and `source` columns to the SCHEMA_SQL `nodes` and `edges` tables. Failing assertion: `cache.put(file, [node with confidence=RESOLVED]); cache.get(file).confidence == RESOLVED`.
-
-- [ ] **Step 2: Run; see fail.**
-- [ ] **Step 3: Bump `CACHE_VERSION` 4→5. Add columns. Update INSERT/SELECT statements. Update Jackson serialization helpers if used.**
-- [ ] **Step 4: Run cache tests; verify all pass.**
-- [ ] **Step 5: Commit:** `feat(cache): bump CACHE_VERSION to 5; add confidence + source columns`.
-
----
-
-### Task 6: Default `Confidence` per detector base class
-
-**Files:**
-- Modify: `AbstractRegexDetector.java`, `AbstractJavaParserDetector.java`, `AbstractAntlrDetector.java`, `AbstractStructuredDetector.java`, `AbstractPythonAntlrDetector.java`, `AbstractTypeScriptDetector.java`, `AbstractJavaMessagingDetector.java`, `AbstractPythonDbDetector.java`.
-- Test: a synthetic `BaseClassConfidenceDefaultTest.java` per base class (or a single parameterized test).
-
-- [ ] **Step 1: Failing parameterized test.** Subclass each base, emit a node with no explicit confidence, assert it carries the expected default (LEXICAL for regex, SYNTACTIC for AST/ANTLR/structured/python-antlr/typescript/messaging/python-db).
-- [ ] **Step 2: Run; see fail (currently always LEXICAL or null).**
-- [ ] **Step 3: Add a `defaultConfidence()` method on each base class returning the matching enum. Make `addNode`/`addEdge` helpers stamp it when not explicitly set.**
-- [ ] **Step 4: Run; verify pass.**
-- [ ] **Step 5: Run full detector suite to ensure no regression.**
-
-```bash
-mvn test -Dtest='io.github.randomcodespace.iq.detector.*' -Dfrontend.skip=true -Ddependency-check.skip=true -q
-```
-
-- [ ] **Step 6: Commit:** `feat(detector): set Confidence default per base class`.
-
----
-
-### Task 7: Snapshot-test refresh (one-time)
-
-JSON-snapshot or golden-file tests will now include the additive `confidence` and `source` fields. Acceptance criterion §13 #3 in the spec requires the diff is limited to those two fields per record.
-
-- [ ] **Step 1: Run full test suite, capture failures.**
-
-```bash
-mvn test -Dfrontend.skip=true -Ddependency-check.skip=true -q -DfailIfNoTests=false 2>&1 | tee /tmp/snapshot-failures.log
-```
-
-- [ ] **Step 2: For each snapshot diff, verify the diff is only the two additive fields.** If anything else changed, that's a bug — fix it before refreshing the snapshot.
-
-- [ ] **Step 3: Refresh snapshots one file at a time with separate commits per file** (so reviewers can diff cleanly).
-
-- [ ] **Step 4: Run full suite; expect green.**
-- [ ] **Step 5: Commit each snapshot refresh:** `chore(test): refresh snapshot for confidence + source fields`.
-
----
-
-## Phase 2 — SPI scaffolding (Tasks 8–13)
-
-### Task 8: `Resolved` interface + `EmptyResolved` singleton
-
-**Files:**
-- Create: `intelligence/resolver/Resolved.java`, `intelligence/resolver/EmptyResolved.java`
-- Test: `ResolvedContractTest.java`
-
-- [ ] **Step 1: Failing test.**
-
-```java
-// src/test/java/io/github/randomcodespace/iq/intelligence/resolver/ResolvedContractTest.java
-package io.github.randomcodespace.iq.intelligence.resolver;
-
-import io.github.randomcodespace.iq.model.Confidence;
-import org.junit.jupiter.api.Test;
-import static org.junit.jupiter.api.Assertions.*;
-
-class ResolvedContractTest {
-
- @Test
- void emptyResolvedIsSingleton() {
- assertSame(EmptyResolved.INSTANCE, EmptyResolved.INSTANCE);
- }
-
- @Test
- void emptyResolvedHasLexicalConfidence() {
- assertEquals(Confidence.LEXICAL, EmptyResolved.INSTANCE.sourceConfidence());
- }
-
- @Test
- void emptyResolvedReportsUnsupported() {
- assertFalse(EmptyResolved.INSTANCE.isAvailable());
- }
-}
-```
-
-- [ ] **Step 2: Run; see fail.**
-- [ ] **Step 3: Implement** `Resolved` (interface with `boolean isAvailable()`, `Confidence sourceConfidence()`, plus language-specific extension points to be added by `JavaResolved`) and `EmptyResolved.INSTANCE` (always returns `false` / `LEXICAL`).
-- [ ] **Step 4: Run; pass.**
-- [ ] **Step 5: Commit:** `feat(resolver): add Resolved interface + EmptyResolved singleton`.
-
----
-
-### Task 9: `ResolutionException`
-
-- [ ] **Step 1: Failing test:** assert `ResolutionException` carries the file path and language fields.
-- [ ] **Step 2: Run; see fail.**
-- [ ] **Step 3: Implement** as a checked exception (subclass `Exception`) with `Path file()`, `String language()`.
-- [ ] **Step 4: Pass.**
-- [ ] **Step 5: Commit:** `feat(resolver): add ResolutionException`.
-
----
-
-### Task 10: `SymbolResolver` interface
-
-```java
-// src/main/java/io/github/randomcodespace/iq/intelligence/resolver/SymbolResolver.java
-package io.github.randomcodespace.iq.intelligence.resolver;
-
-import io.github.randomcodespace.iq.analyzer.DiscoveredFile;
-import java.nio.file.Path;
-import java.util.Set;
-
-public interface SymbolResolver {
- Set getSupportedLanguages();
- void bootstrap(Path projectRoot) throws ResolutionException;
- Resolved resolve(DiscoveredFile file, Object parsedAst) throws ResolutionException;
- default void shutdown() {}
-}
-```
-
-- [ ] **Step 1: Failing contract test** — assert any concrete implementation (start with a stub) honors `getSupportedLanguages()` returning a non-empty `Set` and `resolve(...)` returning non-null.
-- [ ] **Step 2: Run; see fail.**
-- [ ] **Step 3: Implement** the interface as shown.
-- [ ] **Step 4: Pass.**
-- [ ] **Step 5: Commit:** `feat(resolver): add SymbolResolver SPI`.
-
----
-
-### Task 11: `ResolverRegistry` Spring bean
-
-**Files:**
-- Create: `intelligence/resolver/ResolverRegistry.java`
-- Test: `ResolverRegistryTest.java`
-
-- [ ] **Step 1: Failing test.** Two `@Component` stub resolvers (`JavaStubResolver` for `"java"`, `TsStubResolver` for `"typescript"`). Wire via `@SpringBootTest(classes=...)`. Assert `registry.resolverFor("java")` is the Java stub; unknown language returns a no-op (returns `EmptyResolved`); `bootstrap(rootPath)` calls bootstrap on every registered resolver exactly once.
-
-- [ ] **Step 2: Run; see fail.**
-
-- [ ] **Step 3: Implement** `ResolverRegistry` as a `@Component` that takes `List` via constructor injection, builds a `Map` keyed by lowercase language. `resolverFor(String language)` returns matching or a default that emits `EmptyResolved`. `bootstrap(rootPath)` iterates resolvers in alphabetical order by class simple name (determinism), calling each.
-
-- [ ] **Step 4: Pass.**
-
-- [ ] **Step 5: Commit:** `feat(resolver): add ResolverRegistry with auto-discovery`.
-
----
-
-### Task 12: `DetectorContext.resolved()` accessor
-
-**Files:**
-- Modify: `detector/DetectorContext.java`
-- Test: existing `DetectorContextTest.java` (or new) + assertion that legacy detectors still compile.
-
-- [ ] **Step 1: Failing test.** Build a `DetectorContext` with `.resolved(EmptyResolved.INSTANCE)`; assert the accessor returns it. Also assert default returns `Optional.empty()`.
-
-- [ ] **Step 2: Run; see fail.**
-
-- [ ] **Step 3: Add field + builder method + accessor**, additive (default `Optional.empty()`).
-
-- [ ] **Step 4: Run all detector tests** to confirm legacy detectors still compile and behave identically.
-
-```bash
-mvn test -Dtest='io.github.randomcodespace.iq.detector.*' -Dfrontend.skip=true -Ddependency-check.skip=true -q
-```
-
-- [ ] **Step 5: Commit:** `feat(detector): add Optional accessor to DetectorContext`.
-
----
-
-### Task 13: Sanity build
-
-- [ ] **Step 1: Compile + run all model + resolver + detector tests.**
-
-```bash
-mvn test -Dtest='io.github.randomcodespace.iq.{model,intelligence.resolver,detector}.*' \
- -Dfrontend.skip=true -Ddependency-check.skip=true -q
-```
-
-- [ ] **Step 2: Confirm green; if not, fix the smallest possible failure before moving on.**
-
-- [ ] **Step 3: Commit (only if any cleanup landed):** `chore: sanity build after Phase 2`.
-
----
-
-## Phase 3 — Java backend (Tasks 14–18)
-
-### Task 14: Add `javaparser-symbol-solver-core` dep
-
-**Files:**
-- Modify: `pom.xml`
-
-- [ ] **Step 1: Resolve the latest stable version compatible with `javaparser-core` 3.28.0.** Use `context7` MCP first; fall back to Maven Central via `ctx_fetch_and_index`.
-
-- [ ] **Step 2: Add the dependency** to the `` block in `pom.xml`. Pin the version explicitly. Note: JavaParser publishes both core and symbol-solver from the same release train — they should share the same version.
-
-```xml
-
- com.github.javaparser
- javaparser-symbol-solver-core
- ${javaparser.version}
-
-```
-
-(Add a `3.28.0` property if not already present; reuse the existing version everywhere.)
-
-- [ ] **Step 3: Run dependency check.**
-
-```bash
-mvn dependency:tree -Dincludes=com.github.javaparser -Dfrontend.skip=true -Ddependency-check.skip=true
-```
-
-Expected: `javaparser-core` and `javaparser-symbol-solver-core` both at the pinned version.
-
-- [ ] **Step 4: Verify license** is Apache-2.0 (it is, but check `mvn dependency:tree` doesn't pull GPL/AGPL transitives).
-
-- [ ] **Step 5: Compile.**
-
-```bash
-mvn test-compile -Dfrontend.skip=true -Ddependency-check.skip=true -q
-```
-
-- [ ] **Step 6: Commit:** `chore(deps): add javaparser-symbol-solver-core `.
-
----
-
-### Task 15: `JavaSourceRootDiscovery`
-
-**Files:**
-- Create: `intelligence/resolver/java/JavaSourceRootDiscovery.java`
-- Test: `JavaSourceRootDiscoveryTest.java` with synthetic dir layouts via `@TempDir`.
-
-- [ ] **Step 1: Failing test.** Cover:
- - Maven single-module: `/pom.xml`, `src/main/java`, `src/test/java` → returns sorted `[src/main/java, src/test/java]`.
- - Maven multi-module: root `pom.xml` with `service-a` + `service-b`; each has `src/main/java`. Returns sorted union.
- - Gradle (`build.gradle.kts` or `build.gradle`): same `src/main/java` convention.
- - Plain layout: just `src/` without Maven/Gradle markers — returns `[src/]` if it has `*.java`.
- - Empty project (no Java): returns empty list, no exception.
- - Symlink loop in tree: terminates without exception.
-
-```java
-@Test void mavenSingleModule(@TempDir Path tmp) throws Exception {
- Files.createDirectories(tmp.resolve("src/main/java"));
- Files.createDirectories(tmp.resolve("src/test/java"));
- Files.writeString(tmp.resolve("pom.xml"), "");
- var roots = new JavaSourceRootDiscovery().discover(tmp);
- assertEquals(List.of(tmp.resolve("src/main/java"), tmp.resolve("src/test/java")), roots);
-}
-```
-
-- [ ] **Step 2: Run; see fail.**
-- [ ] **Step 3: Implement** discovery using `Files.walk` with depth limits. Return `List` sorted alphabetically. Idempotent.
-- [ ] **Step 4: Run all 6+ scenarios; verify pass.**
-- [ ] **Step 5: Commit:** `feat(resolver/java): add JavaSourceRootDiscovery (Maven/Gradle/plain auto-detect)`.
-
----
-
-### Task 16: `JavaResolved` record
-
-**Files:**
-- Create: `intelligence/resolver/java/JavaResolved.java`
-- Test: `JavaResolvedTest.java`
-
-- [ ] **Step 1: Failing test.** Construct a `JavaResolved` with a stub `JavaSymbolSolver` and a parsed `CompilationUnit`. Assert `isAvailable() == true`, `sourceConfidence() == RESOLVED`, exposes `.cu()` and `.solver()`.
-
-- [ ] **Step 2: Run; see fail.**
-
-- [ ] **Step 3: Implement** as a `record JavaResolved(CompilationUnit cu, JavaSymbolSolver solver) implements Resolved`. `isAvailable() = true`. `sourceConfidence() = Confidence.RESOLVED`.
-
-- [ ] **Step 4: Pass.**
-
-- [ ] **Step 5: Commit:** `feat(resolver/java): add JavaResolved record`.
-
----
-
-### Task 17: `JavaSymbolResolver` (`@Component`)
-
-**Files:**
-- Create: `intelligence/resolver/java/JavaSymbolResolver.java`
-- Test: covered by Task 18 (unit tests) and Task 30+ (aggressive layers).
-
-- [ ] **Step 1: Failing skeleton test.**
-
-```java
-@Test void supportsJava() {
- var r = new JavaSymbolResolver(new JavaSourceRootDiscovery());
- assertEquals(Set.of("java"), r.getSupportedLanguages());
-}
-
-@Test void bootstrapBuildsCombinedTypeSolver(@TempDir Path tmp) throws Exception {
- Files.createDirectories(tmp.resolve("src/main/java"));
- Files.writeString(tmp.resolve("pom.xml"), "");
- var r = new JavaSymbolResolver(new JavaSourceRootDiscovery());
- r.bootstrap(tmp);
- assertNotNull(r.combinedTypeSolver());
-}
-```
-
-- [ ] **Step 2: Run; see fail.**
-
-- [ ] **Step 3: Implement.**
-
-```java
-@Component
-public class JavaSymbolResolver implements SymbolResolver {
- private final JavaSourceRootDiscovery discovery;
- private CombinedTypeSolver combined;
- private JavaSymbolSolver solver;
-
- public JavaSymbolResolver(JavaSourceRootDiscovery discovery) {
- this.discovery = discovery;
- }
-
- @Override public Set getSupportedLanguages() { return Set.of("java"); }
-
- @Override
- public void bootstrap(Path projectRoot) throws ResolutionException {
- try {
- CombinedTypeSolver cts = new CombinedTypeSolver();
- cts.add(new ReflectionTypeSolver());
- for (Path root : discovery.discover(projectRoot)) {
- cts.add(new JavaParserTypeSolver(root.toFile()));
- }
- this.combined = cts;
- this.solver = new JavaSymbolSolver(cts);
- // Configure JavaParser default ParserConfiguration so any subsequent parse
- // benefits from the solver — but allow per-parse override for tests.
- StaticJavaParser.getParserConfiguration().setSymbolResolver(this.solver);
- } catch (Exception e) {
- throw new ResolutionException("bootstrap failed for " + projectRoot, e, projectRoot, "java");
- }
- }
-
- @Override
- public Resolved resolve(DiscoveredFile file, Object parsedAst) throws ResolutionException {
- if (!"java".equalsIgnoreCase(file.language())) return EmptyResolved.INSTANCE;
- if (!(parsedAst instanceof CompilationUnit cu)) return EmptyResolved.INSTANCE;
- if (this.solver == null) return EmptyResolved.INSTANCE;
- return new JavaResolved(cu, solver);
- }
-
- public CombinedTypeSolver combinedTypeSolver() { return combined; }
-}
-```
-
-- [ ] **Step 4: Pass.**
-- [ ] **Step 5: Commit:** `feat(resolver/java): add JavaSymbolResolver`.
-
----
-
-### Task 18: `JavaSymbolResolverTest` — Layer 1 (resolver unit tests)
-
-**Files:**
-- Create: `JavaSymbolResolverTest.java`
-- Create: synthetic Java sources under `src/test/resources/intelligence/resolver/java//`.
-
-Cover all 15+ scenarios from spec §12 layer 1: empty file, single class, generics deep nesting, inner classes (static/non-static/anonymous/local), lambdas, records, sealed, enum-with-methods, interface-with-default, abstract, annotations, imports (explicit/static/wildcard/missing/unused), cyclic imports, same-named-classes-different-packages, JDK symbol, multi-source-root cross-reference.
-
-- [ ] **Step 1: For each scenario, write the synthetic source file** under `src/test/resources/intelligence/resolver/java//Foo.java` (or multiple files where needed) with a `README.md` describing intent (one paragraph).
-
-- [ ] **Step 2: Write the failing test class** (one `@Test` per scenario, named `resolves`).
-
-- [ ] **Step 3: Run; see fail.**
-
-- [ ] **Step 4: Verify fixtures alone are valid Java** by compiling them with `javac`; fix any syntax errors.
-
-- [ ] **Step 5: Run resolver tests; iteratively fix any unexpected resolver behavior.**
-
-- [ ] **Step 6: Commit (after each batch of ~5 scenarios passes):** `test(resolver/java): add Layer 1 scenarios `.
-
----
-
-## Phase 4 — Pipeline wiring (Tasks 19–21)
-
-### Task 19: Wire `ResolverRegistry` into `Analyzer.run()`
-
-- [ ] **Step 1: Failing test** (`AnalyzerResolverWiringTest`): assert `Analyzer.run(rootPath)` calls `registry.bootstrap(rootPath)` exactly once before any file is processed.
-
-- [ ] **Step 2: Run; fail.**
-
-- [ ] **Step 3: Inject `ResolverRegistry` into `Analyzer` (constructor injection, additive).** Add the bootstrap call at the top of `run()`. Order: discovery → resolver bootstrap → file iteration. (Discovery first so we know there's something to scan.)
-
-- [ ] **Step 4: Pass.**
-
-- [ ] **Step 5: Commit:** `feat(analyzer): bootstrap ResolverRegistry once per run`.
-
----
-
-### Task 20: Wire per-file resolution into the file-iteration loop
-
-- [ ] **Step 1: Failing test:** assert that for each file, `registry.resolverFor(file.language()).resolve(...)` is called and the returned `Resolved` is set on the `DetectorContext`.
-
-- [ ] **Step 2: Fail.**
-
-- [ ] **Step 3: Update the file-iteration block in `Analyzer`** to call `registry.resolverFor(file.language()).resolve(file, parsedAst)` and stuff the result into `DetectorContext.builder().resolved(...)`. Catch `ResolutionException` per file (log DEBUG, fall back to `EmptyResolved`).
-
-- [ ] **Step 4: Pass.**
-
-- [ ] **Step 5: Commit:** `feat(analyzer): per-file symbol resolution wired into pipeline`.
-
----
-
-### Task 21: Mirror in `IndexCommand`
-
-`IndexCommand` has its own batched H2 pipeline that's not entirely shared with `Analyzer`. Mirror the resolver bootstrap + per-file resolve path there.
-
-- [ ] **Step 1: Failing test** (`IndexCommandResolverWiringTest`).
-- [ ] **Step 2: Fail.**
-- [ ] **Step 3: Update `IndexCommand` similarly** — same constructor injection of `ResolverRegistry`, same call shape.
-- [ ] **Step 4: Pass.**
-- [ ] **Step 5: Commit:** `feat(cli): wire ResolverRegistry into IndexCommand`.
-
----
-
-## Phase 5 — Configuration (Tasks 22–23)
-
-### Task 22: `intelligence.symbol_resolution.java.*` config keys
-
-- [ ] **Step 1: Failing test** (`UnifiedConfigResolverKeysTest`): assert config object after parsing the example YAML carries `enabled = true`, `sourceRoots = "auto"`, `jdkReflection = true`, `bootstrapTimeoutSeconds = 30`, `maxPerFileResolveMs = 500`.
-
-- [ ] **Step 2: Fail.**
-
-- [ ] **Step 3: Add the new section + binding code** in unified config + `CodeIqConfig` legacy bridge (per `UnifiedConfigBeans`).
-
-- [ ] **Step 4: Pass.**
-
-- [ ] **Step 5: Commit:** `feat(config): add intelligence.symbol_resolution.java.* keys`.
-
----
-
-### Task 23: Document the keys in `docs/codeiq.yml.example`
-
-- [ ] **Step 1: Add the YAML block** matching spec §7 verbatim.
-- [ ] **Step 2: Run `codeiq config validate`** against the example file (after building the JAR if needed) to confirm it parses.
-- [ ] **Step 3: Commit:** `docs(config): document intelligence.symbol_resolution.java.* keys`.
-
----
-
-## Phase 6 — Detector migration (Tasks 24–29)
-
-Each migration follows the same TDD pattern. Concrete code differs per detector, but the test scaffolding is identical.
-
-### Task pattern (apply to each detector below)
-
-For detector `Detector`:
-
-- [ ] **Step 1: Read current detector + test** so you have the existing edge logic in context.
-
-```bash
-sed -n '1,200p' src/main/java/io/github/randomcodespace/iq/detector/jvm/java/Detector.java
-```
-
-- [ ] **Step 2: Add three new test methods to `DetectorTest`:**
- - `resolvedModeProducesResolvedEdge` — feed a fixture where the receiver type would be ambiguous lexically; with resolved context, assert edge target is the *correct* node ID.
- - `fallbackModeMatchesPreSpecBaseline` — `ctx.resolved() == Optional.empty()`; assert logical-content output identical to the baseline (modulo additive fields).
- - `mixedModeUsesResolverWhereAvailable` — half the files have resolved context, half don't; assert per-file confidence labelling.
-
-- [ ] **Step 3: Run; see fails.**
-
-- [ ] **Step 4: Update the detector to:**
- - Accept `ctx.resolved()` as `Optional`.
- - When present and is `JavaResolved`, use `solver` to resolve receiver types / generic args / referenced classes for the specific edges relevant to this detector.
- - Stamp `Confidence.RESOLVED` on resolved-mode edges; existing path stamps base-class default.
-
-- [ ] **Step 5: Run all `DetectorTest`; verify pass + no regression.**
-
-- [ ] **Step 6: Run determinism case** (run detector twice on same input, assert byte-identical output).
-
-- [ ] **Step 7: Commit:** `feat(detector/): use resolved symbol info for `.
-
-### Task 24: `SpringServiceDetector` migration
-
-- Resolves `@Autowired UserService userService` to the actual `UserService` class node ID.
-- Edge: `INJECTS` from the consumer class to the declared `UserService` type.
-- Fixture: two `UserService` classes in different packages; assert resolution picks the imported one.
-
-### Task 25: `SpringRepositoryDetector` migration
-
-- Resolves the entity type parameter on `JpaRepository`.
-- Edge: `MAPS_TO` from repository interface to the resolved entity class.
-
-### Task 26: `JpaEntityDetector` migration
-
-- Resolves generic args on `@OneToMany List`.
-- Edge: `MAPS_TO` between entities (the holder and the related entity).
-
-### Task 27: `JpaRepositoryDetector` migration
-
-- Same as Spring repo, deeper. Resolves derived-query method-name return types where applicable (less reliable; flag as `Confidence.SYNTACTIC` if resolution is partial).
-
-### Task 28: `KafkaListenerDetector` migration
-
-- Resolves `@KafkaListener(topics = TOPIC_CONST)` where `TOPIC_CONST` is a static field — produce edges to the resolved topic name.
-- Edge: `LISTENS` to the topic node.
-
-### Task 29: `SpringRestDetector` migration
-
-- Resolves `@RequestBody UserDto dto` and `@PathVariable` types.
-- Edge: `MAPS_TO` from endpoint node to the resolved DTO class.
-
----
-
-## Phase 7 — Aggressive testing layers (Tasks 30–38)
-
-### Task 30: Layer 6 — Determinism (resolver-stage)
-
-**Files:**
-- Create: `JavaSymbolResolverDeterminismTest.java`
-
-- [ ] **Step 1: Failing test.** Run the resolver twice against the same fixture; assert byte-identical serialized `Resolved` output (use Jackson with stable ordering).
-
-- [ ] **Step 2: Fail.**
-
-- [ ] **Step 3: Confirm resolver implementation already sorts source roots, uses `TreeMap` etc. — fix if not.**
-
-- [ ] **Step 4: Pass.**
-
-- [ ] **Step 5: Add the second variant: source roots passed in different order, same output.**
-
-- [ ] **Step 6: Commit:** `test(resolver/java): determinism — Layer 6`.
-
----
-
-### Task 31: Layer 3 — Concurrency stress
-
-**Files:**
-- Create: `JavaSymbolResolverConcurrencyTest.java`
-
-- [ ] **Step 1: Generate 1000 synthetic Java files** in `@TempDir` (one class each, distinct names). Single source root.
-
-- [ ] **Step 2: Failing test:** resolve all 1000 files via virtual-thread fan-out; assert no exceptions, no duplicate node IDs in the union of `Resolved` outputs, total time within 2× the sequential baseline.
-
-- [ ] **Step 3: Fail/pass.** If fail, investigate (likely: bootstrap not idempotent under concurrent first-call). Add a `synchronized`/`volatile` initialization guard.
-
-- [ ] **Step 4: Add invocation-count test** — bootstrap is called exactly once even under N concurrent first-callers.
-
-- [ ] **Step 5: Commit:** `test(resolver/java): concurrency stress — Layer 3`.
-
----
-
-### Task 32: Layer 4 — Memory / pathological
-
-**Files:**
-- Create: `JavaSymbolResolverPathologicalTest.java`
-
-- [ ] **Step 1: Generate fixtures** (synthesizable in setup):
- - 10K-line class with mostly trivial methods.
- - File with 1000 imports (most unresolvable).
- - 10-deep generic nesting.
-
-- [ ] **Step 2: Failing tests under `-Xmx512m`** (set via Surefire config in pom).
-
-- [ ] **Step 3: Run; pass or fix.** Likely passes; if not, investigate JavaSymbolSolver's caching footprint.
-
-- [ ] **Step 4: Add timeout assertion** — each pathological case completes within `max_per_file_resolve_ms`.
-
-- [ ] **Step 5: Commit:** `test(resolver/java): pathological inputs — Layer 4`.
-
----
-
-### Task 33: Layer 5 — Adversarial
-
-- [ ] **Step 1:** Cover the spec §12 layer 5 cases: syntax-error file, mis-tagged language, mixed source root, ReflectionTypeSolver disabled (config flag).
-- [ ] **Step 2:** Run; fix.
-- [ ] **Step 3: Commit:** `test(resolver/java): adversarial inputs — Layer 5`.
-
----
-
-### Task 34: Layer 7 — E2E petclinic regression
-
-**Files:**
-- Modify: existing `E2EQualityTest` (extend) or create `E2EQualityResolverTest`.
-
-- [ ] **Step 1: Capture baseline numbers.** Run `E2EQualityTest` with `intelligence.symbol_resolution.java.enabled=false`. Record edge precision/recall against `src/test/resources/e2e/ground-truth-petclinic.json`. Save to a baseline JSON checked into the test resources.
-
-- [ ] **Step 2: Run with `enabled=true`. Record post-change numbers.**
-
-- [ ] **Step 3: Failing assertion:** `precision_after > precision_before AND recall_after >= recall_before` (improvement on at least one, no regression on the other).
-
-- [ ] **Step 4: If precision/recall didn't move: investigate why.** Likely the migrated detectors aren't producing the expected resolved edges yet — go back to Phase 6 and fix.
-
-- [ ] **Step 5: Commit:** `test(e2e): petclinic resolver-mode improvement gate — Layer 7`.
-
----
-
-### Task 35: Layer 8 — Property-based (jqwik) — license check first
-
-- [ ] **Step 1: License check.** jqwik is EPL-2.0. Per `~/.claude/rules/dependencies.md` it's not on the preferred (MIT/Apache/BSD) list. **Ask the user explicitly before adding.** If declined, write hand-rolled randomized generators using existing JUnit + `java.util.Random` instead.
-
-- [ ] **Step 2: If approved, add jqwik to `pom.xml`** at test scope. Resolve latest stable via `context7`.
-
-- [ ] **Step 3: Failing properties:**
- - `forall valid_java_source: resolver does not throw unchecked` (only `ResolutionException`).
- - `forall valid_java_source: resolver terminates within max_per_file_resolve_ms`.
- - `forall valid_java_source × file_in_unrelated_root: editing file_in_unrelated_root does not change resolution of valid_java_source`.
-
-- [ ] **Step 4: Run; iterate.**
-
-- [ ] **Step 5: Commit:** `test(resolver/java): property-based — Layer 8`.
-
----
-
-### Task 36: Layer 9 — PIT mutation testing (non-gating profile)
-
-- [ ] **Step 1: Add PIT plugin to `pom.xml` under a non-default profile** `mutation`.
-
-```xml
-
- mutation
-
-
-
- org.pitest
- pitest-maven
- 1.18.0
-
-
- io.github.randomcodespace.iq.intelligence.resolver.*
- io.github.randomcodespace.iq.model.Confidence
-
-
-
-
-
-
-```
-
-- [ ] **Step 2: Run** `mvn -P mutation pitest:mutationCoverage -Dfrontend.skip=true -Ddependency-check.skip=true`.
-
-- [ ] **Step 3: Inspect the mutation kill rate.** Target ≥ 80% on the new packages. If lower, add focused tests until the rate clears 80%.
-
-- [ ] **Step 4: Commit:** `test(resolver): mutation testing profile (PIT) — Layer 9`.
-
----
-
-### Task 37: Aggregate test gate
-
-- [ ] **Step 1: Run full `mvn test` with both config states.**
-
-```bash
-# enabled=false
-CODEIQ_INTELLIGENCE_SYMBOL_RESOLUTION_JAVA_ENABLED=false \
- mvn test -Dfrontend.skip=true -Ddependency-check.skip=true
-
-# enabled=true (default)
-mvn test -Dfrontend.skip=true -Ddependency-check.skip=true
-```
-
-- [ ] **Step 2: Fix any unexpected failure.**
-
-- [ ] **Step 3: Run `mvn verify` for the security gate** (this downloads NVD on first run — allow ~10 min).
-
-```bash
-mvn verify -Dfrontend.skip=true
-```
-
-- [ ] **Step 4: Commit:** `test: aggregate gate green for sub-project 1`.
-
----
-
-### Task 38: Performance gate
-
-- [ ] **Step 1: Time `index` against `spring-petclinic`.**
-
-```bash
-time java -jar target/code-iq-*-cli.jar index $E2E_PETCLINIC_DIR
-```
-
-Compare to the pre-change baseline (run on `main` once, before this branch's first impl commit landed). Acceptance: bootstrap < 10 s; per-Java-file resolve median ≤ 200 ms; total Java analysis time ≤ +60% of baseline.
-
-- [ ] **Step 2: If exceeded, profile** with `async-profiler` or VisualVM. Fix the regression. (Spec §9 documents the budget; exceeding it without justification is a bug.)
-
-- [ ] **Step 3: Record numbers in PR description.**
-
-- [ ] **Step 4: No commit needed unless a fix landed.**
-
----
-
-## Phase 8 — Doc updates + PR (Tasks 39–42)
-
-### Task 39: Expand `CHANGELOG.md` `[Unreleased]` entry
-
-- [ ] **Step 1: Add an `### Added` bullet** under `[Unreleased]` describing the resolver SPI, Java pilot, confidence/provenance schema, cache-version bump, migrated detectors. Cross-reference the spec at `docs/specs/2026-04-27-resolver-spi-and-java-pilot-design.md`.
-
-- [ ] **Step 2: Add a `### Changed` bullet** noting `CACHE_VERSION` 4 → 5 (one-time cache rebuild on first run after upgrade).
-
-- [ ] **Step 3: Commit:** `docs(changelog): add sub-project 1 entry`.
-
----
-
-### Task 40: `CLAUDE.md` Gotchas update
-
-- [ ] **Step 1: Add bullets:**
- - Confidence + source are now mandatory on every node/edge — base classes set defaults; detectors override to `RESOLVED` when consuming `ctx.resolved()`.
- - The pipeline now has a resolve stage between parse and detect. Profile selection unchanged.
- - `CACHE_VERSION` is 5 — bumping invalidates all existing `.codeiq/cache/` dirs on first run.
- - `intelligence.symbol_resolution.java.enabled=false` is the off-switch for raw-speed scans or backward-compat snapshots.
-
-- [ ] **Step 2: Commit:** `docs(claude): gotchas for sub-project 1`.
-
----
-
-### Task 41: `PROJECT_SUMMARY.md` updates
-
-- [ ] **Step 1: Tech-stack row addition:** `| AST + symbols | JavaParser 3.28.0 + javaparser-symbol-solver-core | pom.xml |`.
-
-- [ ] **Step 2: Gotchas updates:** mention `Confidence`, the resolve stage, the `CACHE_VERSION` bump.
-
-- [ ] **Step 3: Commit:** `docs(summary): note resolver pipeline + Confidence schema`.
-
----
-
-### Task 42: Push branch + open PR
-
-- [ ] **Step 1: Push branch** to `origin`.
-
-```bash
-git push -u origin feat/sub-project-1-resolver-spi-and-java-pilot
-```
-
-- [ ] **Step 2: Open PR via `gh`.**
-
-```bash
-gh pr create --title "feat: sub-project 1 — resolver SPI + Java pilot + confidence schema" \
- --body "$(cat <<'EOF'
-## Summary
-- Symbol-resolution stage between parse and detect, per-language `SymbolResolver` SPI auto-discovered as Spring `@Component`s.
-- Java backend wraps JavaParser's `JavaSymbolSolver` (no new dependency tree — same release train as `javaparser-core`).
-- `Confidence` enum (`LEXICAL`/`SYNTACTIC`/`RESOLVED`) and `source` field on every `CodeNode` / `CodeEdge`, round-tripped through Neo4j (`prop_*` convention) and H2 cache (schema v5).
-- 4–6 Java detectors migrated as proof of value (Spring service / repository, JPA entity / repo, Kafka listener, Spring REST).
-- 9 layers of aggressive testing (unit, integration, concurrency, pathological, adversarial, determinism, E2E petclinic regression, property-based via jqwik [pending license OK], PIT mutation profile).
-
-## Spec
-[`docs/specs/2026-04-27-resolver-spi-and-java-pilot-design.md`](docs/specs/2026-04-27-resolver-spi-and-java-pilot-design.md)
-
-## Acceptance criteria
-See spec §13. All checked.
-
-## Test plan
-- [x] `mvn verify` green on CI
-- [x] No logical-content regression with `enabled: false` (snapshots refreshed in separate commits — see history)
-- [x] E2E petclinic precision / recall measurably up with `enabled: true` (numbers below)
-- [x] Determinism gate: resolver runs byte-identical 10× on same input
-- [x] Concurrency stress: 1000 files via virtual threads, no deadlocks
-- [x] Layer 8 jqwik / Layer 9 PIT non-gating signals captured in the PR
-
-## Petclinic numbers
-| Metric | enabled=false (baseline) | enabled=true (this PR) | Δ |
-|---|---|---|---|
-| edge precision | _filled at impl time_ | _filled at impl time_ | + |
-| edge recall | _filled at impl time_ | _filled at impl time_ | + |
-
-## Out of scope
-- Sub-projects 2–8 (TS / Python / Go / Rust+C+++C# resolvers, framework-aware detect refactor, FP harness, MCP read-path hardening). Each gets its own spec → plan → impl cycle.
-
-🤖 Generated with [Claude Code](https://claude.com/claude-code)
-EOF
-)"
-```
-
-- [ ] **Step 3: Wait for CI;** if any failure, fix on the branch and push (do not `--amend` and force-push). Repeat until CI green.
-
-- [ ] **Step 4: Hand back to user** per default check-in cadence (b): "PR is open, tests green, ready for human review."
-
----
-
-## Self-review (run after writing the plan, before execution)
-
-1. **Spec coverage** — every acceptance criterion (§13) maps to at least one task. Verified.
-2. **Placeholder scan** — no "TBD"/"TODO"/"figure out"; concrete code blocks for foundational tasks; templated patterns for repeated migrations. Acceptable per skill DRY guidance.
-3. **Type / naming consistency** — `Confidence`, `Resolved`, `EmptyResolved`, `SymbolResolver`, `ResolverRegistry`, `JavaSymbolResolver`, `JavaResolved`, `JavaSourceRootDiscovery` — all referenced consistently across tasks.
-4. **Backward compatibility** — Phase 6 detectors keep their existing logic; resolver consumption is purely additive.
-5. **Determinism** — Tasks 30, 31 (concurrency), and detector determinism (per Task pattern Step 6) all preserve the determinism gate.
-6. **Performance budget** — Task 38 explicitly checks the spec §9 numbers.
-7. **License decisions** — Task 35 (jqwik) is gated on user approval; Task 36 (PIT) is Apache-2.0, fine.
-8. **Test refresh hazard** — Task 7 isolates the snapshot refresh into its own commit chain so reviewers can verify the diff is bounded to the additive fields.
diff --git a/docs/plans/2026-04-28-sub-project-2-aks-read-only-deploy.md b/docs/plans/2026-04-28-sub-project-2-aks-read-only-deploy.md
deleted file mode 100644
index 2034d303..00000000
--- a/docs/plans/2026-04-28-sub-project-2-aks-read-only-deploy.md
+++ /dev/null
@@ -1,156 +0,0 @@
-# Sub-project 2 implementation plan — AKS read-only deploy hardening
-
-> **Spec:** [`docs/specs/2026-04-28-aks-read-only-deploy-design.md`](../specs/2026-04-28-aks-read-only-deploy-design.md)
->
-> **Goal:** ship a runbook + JVM-flag-preset launch script + a sentinel test, so `codeiq serve` runs cleanly inside an AKS pod with read-only root filesystem and writable `/tmp`. No source-code changes to the serve profile or Neo4j wiring.
->
-> **Scope:** small. Five files changed, single PR off `main`. Independent of sub-project 1.
-
-## File map
-
-| Action | Path | Purpose |
-|---|---|---|
-| **CREATE** | `docs/specs/2026-04-28-aks-read-only-deploy-design.md` | Architecture spec (✅ done with this plan). |
-| **CREATE** | `docs/plans/2026-04-28-sub-project-2-aks-read-only-deploy.md` | This file. |
-| **CREATE** | `shared/runbooks/aks-read-only-deploy.md` | Canonical deploy runbook. |
-| **CREATE** | `scripts/aks-launch.sh` | JVM-flag-preset launch wrapper. |
-| **CREATE** | `src/test/java/io/github/randomcodespace/iq/deploy/AksLaunchScriptSentinelTest.java` | Asserts the launch script contains the required flags. Catches drift. |
-| **MODIFY** | `CHANGELOG.md` | New `[Unreleased] / Added` bullet. |
-| **MODIFY** | `shared/runbooks/engineering-standards.md` | §7.1 cross-link to the new runbook. |
-
-## Tasks
-
-### Task 1 — Runbook
-
-**File:** `shared/runbooks/aks-read-only-deploy.md`.
-
-**Sections:** Overview · Deploy shape · Init-container pattern (Kubernetes manifest snippet) · JVM flag preset · Local docker smoke · Rollback · Cross-references.
-
-**Hard requirement:** every command in the runbook must be runnable as-is. No placeholder URLs. Where a Nexus URL is needed, parameterize via `$NEXUS_URL` env, document it once.
-
-### Task 2 — Launch script
-
-**File:** `scripts/aks-launch.sh`.
-
-**Skeleton:**
-
-```bash
-#!/usr/bin/env bash
-# AKS read-only deploy launcher for codeiq serve.
-# Usage: aks-launch.sh /tmp/codeiq-data
-set -euo pipefail
-
-if [[ $# -ne 1 ]]; then
- echo "usage: $(basename "$0") " >&2
- exit 64
-fi
-DATA_DIR="$1"
-
-# Resolve the codeiq JAR location. Container image installs it at /app.
-JAR="${CODEIQ_JAR:-/app/code-iq.jar}"
-
-# Pre-flight: ensure /tmp has enough headroom (1 GB minimum).
-TMP_FREE_KB="$(df -Pk /tmp | awk 'NR==2 {print $4}')"
-if [[ "$TMP_FREE_KB" -lt 1048576 ]]; then
- echo "fatal: /tmp has < 1 GB free ($TMP_FREE_KB KB)" >&2
- exit 70
-fi
-
-# JVM flag preset: every entry has a non-default behavior that without it
-# would write outside /tmp. Order is intentional — system properties first,
-# then -XX flags, so any -XX value referencing a system property resolves.
-JAVA_OPTS=(
- -Dorg.springframework.boot.loader.tmpDir=/tmp/spring-boot-loader
- -Djava.io.tmpdir=/tmp
- -XX:ErrorFile=/tmp/hs_err_pid%p.log
- -XX:HeapDumpPath=/tmp
- -XX:+HeapDumpOnOutOfMemoryError
-)
-
-mkdir -p /tmp/spring-boot-loader
-
-exec java "${JAVA_OPTS[@]}" -jar "$JAR" serve "$DATA_DIR"
-```
-
-**Permissions:** `chmod +x scripts/aks-launch.sh` after create. Must be executable (the sentinel test asserts this).
-
-### Task 3 — Sentinel test
-
-**File:** `src/test/java/io/github/randomcodespace/iq/deploy/AksLaunchScriptSentinelTest.java`.
-
-**Assertions** (one per required flag, plus structural checks):
-
-```java
-@Test void scriptIsExecutable() { ... }
-@Test void scriptUsesStrictBashMode() { ... } // set -euo pipefail
-@Test void scriptValidatesArgCount() { ... }
-@Test void scriptSetsSpringBootLoaderTmpDir() { ... }
-@Test void scriptSetsJavaIoTmpdir() { ... }
-@Test void scriptSetsJvmErrorFile() { ... }
-@Test void scriptSetsHeapDumpPath() { ... }
-@Test void scriptEnablesHeapDumpOnOom() { ... }
-@Test void scriptExecsJava() { ... } // exec java to PID 1
-```
-
-The test reads the script as a `String` and grep-matches each required substring. Cheap, deterministic, drift-proof.
-
-### Task 4 — CHANGELOG entry
-
-**File:** `CHANGELOG.md`.
-
-**Add to `[Unreleased] / ### Added`:**
-
-```markdown
-- AKS read-only deploy hardening (sub-project 2): runbook at
- `shared/runbooks/aks-read-only-deploy.md`, JVM-flag-preset launcher at
- `scripts/aks-launch.sh`, and a sentinel test asserting the script
- contains every required flag. Enables `codeiq serve` inside an AKS pod
- with read-only root filesystem + writable `/tmp` (init-container
- copies bundle from Nexus → `/tmp/codeiq-data`; main container runs
- `aks-launch.sh /tmp/codeiq-data`). Zero source-code changes to the
- serve profile or Neo4j wiring — solved at the deployment layer plus
- Spring-Boot-loader / JVM crash-file path overrides. Spec at
- `docs/specs/2026-04-28-aks-read-only-deploy-design.md`.
-```
-
-### Task 5 — engineering-standards cross-link
-
-**File:** `shared/runbooks/engineering-standards.md` §7.1.
-
-Add a one-line bullet right under the existing "deploy surface" sentence:
-
-```markdown
-- AKS read-only deploy is supported via `shared/runbooks/aks-read-only-deploy.md`
- and `scripts/aks-launch.sh` (sub-project 2). The Maven Central artifact + the
- launch script + an init-container that copies the graph bundle from Nexus
- into `/tmp/codeiq-data` is the full surface — no separate hosted backend.
-```
-
-### Task 6 — Test loop + commit
-
-```bash
-mvn test -Dtest=AksLaunchScriptSentinelTest
-mvn test # full suite — confirm nothing else regressed
-git add docs/specs/ docs/plans/ shared/runbooks/ scripts/aks-launch.sh \
- src/test/java/io/github/randomcodespace/iq/deploy/ CHANGELOG.md
-git commit -m "feat(deploy): AKS read-only deploy hardening (sub-project 2)"
-git push -u origin feat/sub-project-2-aks-read-only-deploy
-gh pr create --base main \
- --title "feat: AKS read-only deploy hardening (sub-project 2)" \
- --body "..."
-```
-
-## Acceptance gates
-
-- [ ] All seven files in the file map exist and are non-empty.
-- [ ] Sentinel test green.
-- [ ] Full `mvn test` green.
-- [ ] Runbook commands are copy-pasteable; no placeholder URLs that the operator can't substitute.
-- [ ] PR open against `main`.
-
-## Out of scope (deliberate)
-
-- A heavyweight JVM-level filesystem-write detector (Java has no clean `chroot` / `unshare` API; environment-fragile in CI). The runbook docker smoke is the SSoT for "did this actually work in a RO root."
-- A `/api/diagnostics` endpoint surfacing JVM flag preset values. Tracked separately if ops need it.
-- Switching the storage layer to a static snapshot (Approach D in the spec). Reserved as the fallback if init-container copy proves operationally insufficient.
-- Helm chart / OCI artifact packaging. The runbook ships a vanilla Kubernetes manifest snippet; productionizing into Helm is the deployer's call.
diff --git a/docs/specs/2026-04-27-resolver-spi-and-java-pilot-design.md b/docs/specs/2026-04-27-resolver-spi-and-java-pilot-design.md
deleted file mode 100644
index 81326446..00000000
--- a/docs/specs/2026-04-27-resolver-spi-and-java-pilot-design.md
+++ /dev/null
@@ -1,379 +0,0 @@
-# Sub-project 1 — Resolver SPI + Java Pilot + Confidence Schema
-
-> **Status:** Awaiting approval. Brainstormed 2026-04-27.
-> **Authors:** brainstormed via `superpowers:brainstorming` with the project maintainer.
-> **Audience:** the agent / engineer who will implement this. Every claim should be checkable against the codebase referenced by `CLAUDE.md` and `PROJECT_SUMMARY.md`.
-
-## 1. Context
-
-codeiq's detector layer is the right abstraction. The **layer below it** is the bottleneck: detectors receive a parse tree (ANTLR) or AST (JavaParser) but no resolved symbol table. As a result, edges like `CALLS`, `INJECTS`, `IMPLEMENTS`, `EXTENDS`, and many framework-specific edges are emitted *by name*, not by **resolved type**. Two same-named symbols across packages collapse into one node; `userService.findById(id)` resolves to whichever `findById` the detector happens to see first.
-
-This is the architectural seam between "rich code map" and "ground-truth semantic graph." Every other planned improvement — TypeScript / Python / Go / Rust / C++ / C# resolution, framework-aware detection refactor, cross-framework false-positive harness — slots into this seam. Doing it second means inventing the seam ad-hoc inside whichever sub-project lands first, then retrofitting.
-
-This spec covers **sub-project 1 of 8** in the larger "robust graph" decomposition:
-
-| # | Scope | This spec? |
-|---|---|---|
-| 1 | Resolver SPI + Java pilot + confidence/provenance schema | **Yes** |
-| 2 | TypeScript / JavaScript resolution | No |
-| 3 | Python resolution | No |
-| 4 | Go resolution | No |
-| 5 | Rust / C++ / C# resolution | No |
-| 6 | Framework-aware detection refactor | No |
-| 7 | Cross-framework false-positive harness | No |
-| 8 | MCP HTTP-streamable hardening (read-path) | No |
-
-## 2. Goals
-
-1. **Add a symbol-resolution stage** to the indexing pipeline, between parse and detect, that exposes a resolved symbol table to detectors.
-2. **Wire a Java backend** using JavaParser's `JavaSymbolSolver`, with no new dependency tree (the solver is published alongside JavaParser).
-3. **Add a confidence/provenance schema** (`Confidence` enum + `source` field) on every `CodeNode` and `CodeEdge`, round-tripped through Neo4j.
-4. **Migrate 4–6 Java detectors** to use the resolver as proof of value: at least one Spring DI detector, one JPA detector, one messaging detector.
-5. **Preserve backward compatibility:** all existing detectors compile and run unchanged. Resolution is opt-in per detector via `ctx.resolved()`.
-6. **Preserve determinism:** resolver-stage output is byte-identical run-to-run, with the same input.
-7. **Aggressive testing**, including adversarial inputs, concurrency stress, property-based, fuzz, mutation testing, and regression against the existing E2E quality bar.
-
-## 3. Non-goals
-
-- Maven / Gradle classpath JAR resolution beyond what `ReflectionTypeSolver` covers via the running JDK. (Possible follow-up: sub-project 1.5.)
-- Resolution for non-Java languages. (Sub-projects 2–5.)
-- Refactoring detectors to detect by resolved type rather than import-name. (Sub-project 6 — separate concern; a migrated detector here keeps its current detection mechanism, only resolving outgoing edges' targets more accurately.)
-- Performance optimization beyond what the design naturally affords. (Defer until measured.)
-- Changes to the serving layer (REST API, MCP tools, web UI).
-- Changes to `application.yml` Spring-owned keys (CORS, Neo4j Bolt port, UI toggle).
-
-## 4. Architecture
-
-### 4.1 Pipeline shape
-
-The current `index` and `analyze` pipelines look like:
-
-```
-discover → parse → detect → link → classify → store
-```
-
-After this sub-project, they become:
-
-```
-discover → parse → resolve → detect → link → classify → store
-```
-
-The resolve stage runs after `analyzer/StructuredParser` produces a parsed file and before the detector fan-out kicks off.
-
-### 4.2 Resolver-pass placement
-
-- **Bootstrapping:** `analyzer/Analyzer` (or `cli/IndexCommand`'s in-process pipeline) calls `ResolverRegistry.bootstrap(rootPath)` once per analysis run, before file iteration begins. The Java resolver uses this hook to build a single `CombinedTypeSolver` configured with sorted source roots and `ReflectionTypeSolver`. Other languages' resolvers (future sub-projects) plug into the same hook.
-- **Per-file resolution:** for each file, after parse, the analyzer asks `ResolverRegistry.resolverFor(language)` for the matching resolver, calls `resolve(parsedFile)`, and stores the result on the `DetectorContext` as `Optional`.
-- **Detector consumption:** detectors call `ctx.resolved()`. If present, the detector may emit edges with `Confidence.RESOLVED`; if absent, the detector falls through to its existing logic and emits `Confidence.SYNTACTIC` (when AST-based) or `Confidence.LEXICAL` (when regex-based).
-
-### 4.3 Pipeline invariant
-
-The new stage must not change *which files are analyzed* or *which detectors run for them*. It only enriches the input each detector sees. A regression here breaks every downstream count and statistic.
-
-## 5. Components
-
-### 5.1 New components
-
-| Path | Type | Responsibility |
-|---|---|---|
-| `intelligence/resolver/SymbolResolver.java` | interface | SPI: `Set getSupportedLanguages(); Resolved resolve(ParsedFile parsed) throws ResolutionException;` |
-| `intelligence/resolver/Resolved.java` | interface (or sealed type) | Read-only resolution result for one file: per-symbol type info, resolved imports, declared types. Includes `Confidence sourceConfidence()` indicating the resolver's confidence in this particular result. |
-| `intelligence/resolver/EmptyResolved.java` | record / class | Singleton "no resolution available" — returned for unsupported languages, disabled config, or resolution failure. |
-| `intelligence/resolver/ResolverRegistry.java` | `@Component` | Auto-discovers `@Component` `SymbolResolver` beans (mirrors `DetectorRegistry`). Exposes `resolverFor(language)` and `bootstrap(rootPath)`. |
-| `intelligence/resolver/ResolutionException.java` | exception | Wraps backend-specific failures (e.g. `JavaSymbolSolver` errors) with context (file path, language). |
-| `intelligence/resolver/java/JavaSymbolResolver.java` | `@Component` | Wraps `JavaSymbolSolver`. Builds `CombinedTypeSolver` from sorted source roots + `ReflectionTypeSolver`. |
-| `intelligence/resolver/java/JavaResolved.java` | record | Java-specific `Resolved` carrying JavaParser `TypeSolver` + per-AST resolved type info. |
-| `intelligence/resolver/java/JavaSourceRootDiscovery.java` | helper | Discovers Java source roots from a project root (auto-detects `src/main/java`, `src/test/java`, multi-module via Maven `` / Gradle `include`). Pure logic, unit-testable. |
-| `model/Confidence.java` | enum | `LEXICAL` / `SYNTACTIC` / `RESOLVED` with a numeric mapping (0.6 / 0.8 / 0.95). Comparable. |
-| `model/EdgeProvenance.java` *(optional, see §5.3)* | record | Optional richer provenance carrier; if not adopted, just use `String source` on `CodeEdge`. |
-
-### 5.2 Changed components
-
-| Path | Change | Rationale |
-|---|---|---|
-| `detector/DetectorContext.java` | Add `Optional resolved()` accessor. Defaults to `Optional.empty()`. Existing constructors keep working. | Detector opt-in path. |
-| `model/CodeNode.java` | Add `Confidence confidence` and `String source` fields. `source` filled in by detector base classes (detector class simple name). `confidence` set per parser type (see §5.3): `AbstractRegexDetector` → `LEXICAL`, `AbstractJavaParserDetector` / `AbstractAntlrDetector` / `AbstractStructuredDetector` → `SYNTACTIC`. Detectors override to `RESOLVED` when emitting an edge derived from `ctx.resolved()`. | Confidence/provenance schema. |
-| `model/CodeEdge.java` | Same as `CodeNode`. | Same. |
-| `graph/GraphStore.java` | `bulkSave` writes `prop_confidence` and `prop_source`; `nodeFromNeo4j` / `edgeFromNeo4j` restore them. | Round-trip the new fields. |
-| `cache/AnalysisCache.java` | Bump `CACHE_VERSION` from 4 to 5. Add `confidence` and `source` columns to `nodes` and `edges` tables. | Schema change requires cache reset. |
-| `analyzer/Analyzer.java` | Insert resolve step. `bootstrapResolvers(rootPath)` once; `resolverFor(language).resolve(parsed)` per file. | Pipeline integration. |
-| `cli/IndexCommand.java` | Mirror `Analyzer`'s resolver bootstrap (the in-process H2 batched pipeline). | Both code paths must integrate. |
-| 4–6 Java detectors (see §5.4) | Use `ctx.resolved()`. Emit `Confidence.RESOLVED` when present; existing path emits `Confidence.SYNTACTIC`. | Proof of value. |
-| `pom.xml` | Add `com.github.javaparser:javaparser-symbol-solver-core` (Apache-2.0, version-pinned to match `javaparser-core`). Resolve **latest stable matching version** at implementation time. Add `net.jqwik:jqwik` (test scope, EPL-2.0) for property-based tests. | New deps. |
-| `codeiq.yml` schema (`docs/codeiq.yml.example`) | Document the new `intelligence.symbol_resolution.java` keys. | Surface the new config. |
-| `config/CodeIqConfig.java` (or unified-config equivalent) | Bind the new keys. | Enable the toggles. |
-
-### 5.3 Confidence / provenance — schema decisions
-
-- **Storage shape:** the simplest viable model is two scalar fields on every `CodeNode` and `CodeEdge`:
- - `confidence: Confidence` (enum, non-null). The default is set by the detector's base class — not a single hardcoded value — based on the parser used:
- - `AbstractRegexDetector` → `LEXICAL` (pattern-only, no AST)
- - `AbstractJavaParserDetector` / `AbstractAntlrDetector` / `AbstractStructuredDetector` / `AbstractPythonAntlrDetector` / `AbstractTypeScriptDetector` / `AbstractJavaMessagingDetector` / `AbstractPythonDbDetector` → `SYNTACTIC` (AST or parse tree, no symbol resolution)
- - Detector overrides to `RESOLVED` for any edge derived from `ctx.resolved()`.
- - `source: String` (non-null; detector class simple name, e.g. `"SpringServiceDetector"`)
-- **Numeric access:** consumers (Cypher queries, MCP tools, the SPA) get a numeric value via `Confidence.score()` (0.6 / 0.8 / 0.95). The mapping is a static lookup; the enum is the authoritative form.
-- **Future extensibility:** if richer provenance is needed later (e.g. resolver name, resolution timestamp), extend with optional `prop_resolver` etc. — the enum + source design does not preclude this. Don't pre-build for it.
-- **MCP / API surface:** `confidence` and `source` are passthrough fields in node/edge JSON serialization. No new endpoints. Cypher filters can use `WHERE n.confidence = 'RESOLVED'` once the schema lands.
-
-### 5.4 Detector migration candidates (4–6)
-
-Final selection happens at implementation time based on which gives the clearest signal in `spring-petclinic`. Likely set:
-
-| Detector | Path | Why |
-|---|---|---|
-| `SpringServiceDetector` | `detector/jvm/java/SpringServiceDetector.java` | `@Autowired UserService` — needs to resolve `UserService` to its actual type for cross-class wiring. Highest visibility win. |
-| `SpringRepositoryDetector` | `detector/jvm/java/SpringRepositoryDetector.java` | Repository interfaces extending `JpaRepository` — resolving `T` lets us link the repo to the entity. |
-| `JpaEntityDetector` | `detector/jvm/java/JpaEntityDetector.java` | `@OneToMany List` — resolving the generic argument links entity-to-entity correctly. |
-| `JpaRepositoryDetector` | `detector/jvm/java/JpaRepositoryDetector.java` | Same as Spring repo, deeper. |
-| `KafkaListenerDetector` | `detector/jvm/java/KafkaListenerDetector.java` | Topic resolution from `@KafkaListener(topics = TOPIC_CONST)`. |
-| `SpringRestDetector` | `detector/jvm/java/SpringRestDetector.java` | `@RequestBody UserDto dto` — resolving `UserDto` enables `MAPS_TO` edges from endpoint to entity. |
-
-Six is the upper bound; if four are sufficient to demonstrate measurable quality lift on petclinic, the rest can be migrated in follow-up PRs without changing this spec.
-
-## 6. Data flow (per analysis run)
-
-```
-1. cli/{Index,Analyze}Command.call() → analyzer/Analyzer.run(rootPath)
- 1.1. ResolverRegistry.bootstrap(rootPath)
- → JavaSymbolResolver.bootstrap()
- - JavaSourceRootDiscovery.discover(rootPath) → sorted List
- - new CombinedTypeSolver(
- new ReflectionTypeSolver(),
- sorted source roots wrapped in JavaParserTypeSolver)
- - new JavaSymbolSolver(combinedTypeSolver)
- - configure JavaParser default ParserConfiguration with the solver
-2. For each discovered file (virtual thread):
- 2.1. StructuredParser.parse(file) → ParsedFile (Java → CompilationUnit; others → existing types)
- 2.2. resolved = ResolverRegistry.resolverFor(file.language()).resolve(parsedFile)
- (returns EmptyResolved.INSTANCE for languages without a registered resolver)
- 2.3. ctx = DetectorContext.builder()...resolved(resolved)...build()
- 2.4. for each Detector matching language: detector.detect(ctx)
-3. GraphBuilder.flush() → AnalysisCache (or → GraphStore on enrich)
- - Each node and edge carries Confidence + source
- - Round-tripped via prop_confidence / prop_source in Neo4j
-```
-
-## 7. Configuration surface
-
-New keys in `codeiq.yml`:
-
-```yaml
-intelligence:
- symbol_resolution:
- java:
- enabled: true
- source_roots: auto # or explicit list of paths relative to repo root
- jdk_reflection: true # ReflectionTypeSolver — needs JDK on classpath (always true for codeiq's runtime)
- # bootstrap_timeout_seconds: 30 (kill switch if solver hangs)
- # max_per_file_resolve_ms: 500 (per-file resolution timeout)
-```
-
-**Defaults:**
-- `enabled: true` — most users want correctness > raw speed.
-- `source_roots: auto` — discovery covers Maven (`src/main/java`, `src/test/java`, multi-module via `` in `pom.xml`), Gradle (similar), and plain layouts.
-- `jdk_reflection: true`.
-- `bootstrap_timeout_seconds: 30`.
-- `max_per_file_resolve_ms: 500`.
-
-**Env overrides:** `CODEIQ_INTELLIGENCE_SYMBOL_RESOLUTION_JAVA_ENABLED=false` etc.
-
-**Config validation:** `codeiq config validate` must reject invalid combinations (e.g. `enabled: true` with empty `source_roots: []`).
-
-## 8. Backward compatibility
-
-- All existing `Detector` implementations compile and run unchanged. `ctx.resolved()` returns `Optional.empty()` for them by default (they never call it).
-- Existing tests must pass with `intelligence.symbol_resolution.java.enabled: false`. **Mandatory.** Two sub-cases:
- - **Logical-content tests** (assert on node IDs, edge counts, specific property values): pass unchanged.
- - **JSON-snapshot / golden-file tests** (assert on full serialized output): will shift by exactly two new fields per node/edge (`confidence`, `source`). These get a **one-time refresh** during implementation, with a separate commit so the diff is reviewable. The refresh must produce only those two added fields per record — any other diff is a bug.
-- With `enabled: true`, logical-content tests still pass — but some node/edge counts may shift **by design** (resolved-mode detectors emit different / additional edges that the lexical fallback could not produce). Expected diffs are recorded in the implementation plan and PR description.
-- `CACHE_VERSION` bump from 4 to 5 wipes old `.codeiq/cache/` on first run. Documented in `CHANGELOG.md` under `[Unreleased]` as a breaking cache change. End users lose nothing meaningful; the cache rebuilds on the next `index` run.
-
-## 9. Performance budget
-
-| Stage | Cost | Notes |
-|---|---|---|
-| Resolver bootstrap | 2–5 s on a medium repo | One-time per run. Cached `CombinedTypeSolver` reused across files. |
-| Per-Java-file resolve | 50–200 ms typical | Net +30–60% on Java analysis time. |
-| Per-non-Java-file resolve | 0 (EmptyResolved) | No-op. |
-| Memory overhead | tens to low hundreds of MB | `CombinedTypeSolver` caches resolved type info; bounded by source-root size. |
-| Determinism cost | none | Sorted source roots add ms-scale. |
-
-For a 44 K-file codebase:
-- Today: index ~220 s.
-- After: index ~280–350 s (Java-heavy repos worst case). Acceptable.
-- Mitigation: `intelligence.symbol_resolution.java.enabled: false` for raw-speed scans.
-
-**Performance gate:** if resolver bootstrap exceeds 10 s on `spring-petclinic`, the implementation has a bug — investigate before merge.
-
-## 10. Determinism guarantees
-
-- `JavaSourceRootDiscovery.discover(rootPath)` returns roots sorted alphabetically.
-- `CombinedTypeSolver` member solvers added in the sorted order.
-- `ResolverRegistry` exposes resolvers in stable iteration order (Spring `@Component` collection sorted by simple class name).
-- `Resolved` value-types use `TreeMap` / sorted `List` for any iteration-order-sensitive data.
-- New determinism test (mandatory): run resolver twice on the same input via separate JVM invocations, assert byte-identical serialized output. Mirrors existing detector convention.
-
-## 11. Error handling
-
-| Failure | Behavior |
-|---|---|
-| Source root configured but missing | Log WARN, drop from solver list, continue. |
-| Source root contains no Java files | Drop from solver list, continue. |
-| `CombinedTypeSolver` construction throws | Log ERROR with classpath context, fall back to `EmptyResolved` for all files (resolver disabled for this run), increment a metric. Do **not** abort the analysis. |
-| Per-file `resolve(parsedFile)` throws | Log DEBUG (these are expected for malformed sources), return `EmptyResolved` for that file, continue. |
-| Per-file resolution exceeds `max_per_file_resolve_ms` | Cancel via virtual-thread interruption, return `EmptyResolved` for that file, count timeout in metrics. |
-| Bootstrap exceeds `bootstrap_timeout_seconds` | Abort bootstrap, fall back to `EmptyResolved` for the run, log ERROR. Run continues without resolution. |
-| Detector calls `ctx.resolved().get()` and crashes | Caught by existing per-detector `try/catch` in `Analyzer` — file is skipped, detector is logged, run continues. (Existing behavior.) |
-
-## 12. Aggressive testing strategy
-
-This section is binding. Every layer below is mandatory for sub-project 1; the same template applies to sub-projects 2–8.
-
-### Layer 1 — Resolver unit tests (pure, fast)
-
-For `JavaSymbolResolver`, with one synthetic source tree per test:
-
-- Empty file (zero declarations).
-- Single class with no imports.
-- Class with multiple methods of varying signatures (overloads).
-- Class with generics (≥3 levels of nesting: `Map>>`).
-- Inner classes (static, non-static, anonymous, local).
-- Lambda expressions and method references.
-- Records and sealed classes (Java 25).
-- Enum with abstract methods.
-- Interface with default methods.
-- Abstract class.
-- Annotations (definition + use).
-- Imports: explicit, static, wildcard, missing target, unused.
-- Cyclic imports between two files (legal in Java) — both resolve.
-- Two classes with the same simple name in different packages — both resolve to distinct nodes.
-- Symbol defined in JDK (`Optional`, `Stream`, `List`) — resolves via `ReflectionTypeSolver`.
-- Multi-source-root: a class in `src/main/java` referencing one in `src/test/java`.
-
-Expected: every test asserts the *exact* `Resolved` content via golden files committed under `src/test/resources/intelligence/resolver/java/`.
-
-### Layer 2 — Detector × resolver integration tests
-
-For each migrated detector:
-- **Resolved-mode positive:** with resolver enabled, assert resolved-only edges that the lexical fallback could not produce (e.g. `INJECTS` edges to the *correct* `UserService` of two same-named classes in different packages).
-- **Fallback-mode positive:** with resolver disabled, assert logical-content output identical to the pre-spec baseline (modulo the additive `confidence` and `source` fields per §8).
-- **Mixed mode:** simulate resolver failure on half the files; the other half emits resolved edges, the failing half emits fallback edges. Both labeled with correct `Confidence`.
-
-### Layer 3 — Concurrency stress
-
-- 1000 synthetic Java files resolved on virtual threads. Assert: no exceptions, no deadlocks, no thread starvation, total throughput within 2× of sequential baseline. Output identical to sequential run (sort-then-compare).
-- Resolver bootstrap happens **once** even if 50 threads call `resolverFor` simultaneously at startup. Verify via mock + invocation count.
-
-### Layer 4 — Memory / pathological inputs
-
-- 10 000-line synthetic class file: resolves under -Xmx512m.
-- File with 1000 imports (most unresolved): resolves without OOM; produces the expected partial result.
-- Deep generic nesting (10 levels deep): resolves; runtime ≤ 1 s.
-
-### Layer 5 — Adversarial inputs
-
-- File with syntax errors (parser fails): resolver never invoked; `Analyzer` continues.
-- File mis-tagged as Java but actually Kotlin / Groovy / random bytes: parser fails first; resolver never sees it.
-- Mixed source root with `.java` and unrelated files: only `.java` files enter the solver.
-- `ReflectionTypeSolver` simulated as unavailable (test injects null JDK classpath): resolver works at reduced fidelity, returns `Confidence.SYNTACTIC` for JDK-dependent symbols.
-
-### Layer 6 — Determinism
-
-- Run resolver 10 times against the same input on the same JVM. Assert byte-identical serialized graphs.
-- Run resolver against the same input, with source roots passed in a different order. Assert byte-identical output (we sort internally).
-- Run on cold and warm JVMs. Identical.
-
-### Layer 7 — E2E quality regression (gating)
-
-- `E2EQualityTest` against `spring-petclinic` ground truth (`src/test/resources/e2e/ground-truth-petclinic.json`):
- - With `enabled: false`: logical-content output identical to the pre-spec baseline (modulo the additive `confidence` and `source` fields per record — see §8). Mandatory regression gate.
- - With `enabled: true`: edge precision / recall **measurably up** vs. the `enabled: false` baseline. The implementation plan will record before/after numbers; this spec demands measurable improvement with no regressions on other metrics in the ground-truth file.
-- Full `mvn test` green.
-- Full `mvn verify` green (SpotBugs, dependency-check). May skip locally; CI is authoritative.
-
-### Layer 8 — Property-based / fuzz (jqwik)
-
-- New test scope dependency: `net.jqwik:jqwik` (latest stable, EPL-2.0). License is EPL-2.0 — flag for explicit approval; if rejected, swap for a permissive alternative (or hand-write generators). **License decision deferred to implementation time** — see §15 below.
-- Generators produce small synthetic Java source strings (within JavaParser's grammar). Invariants tested:
- - Resolver never throws an unchecked exception (only `ResolutionException` or returns `EmptyResolved`).
- - Resolver always terminates within `max_per_file_resolve_ms`.
- - Same input → same output (deterministic).
- - Editing an unrelated file in a different source root never changes the resolution of file F.
-
-### Layer 9 — Mutation testing (PIT)
-
-- Add PIT mutation testing as a **non-gating** Maven goal (e.g. `mvn -P mutation pitest:mutationCoverage`).
-- Target: 80% mutant kill rate on the new packages (`io.github.randomcodespace.iq.intelligence.resolver.*`, `io.github.randomcodespace.iq.model.Confidence`).
-- Not bound to `mvn verify` — runs on demand. Used as a code-quality signal during PR review.
-
-### Test-data hygiene
-
-- Synthetic Java sources for unit tests live under `src/test/resources/intelligence/resolver/java//...`.
-- Each scenario has a `README.md` explaining intent (one paragraph).
-- Golden output (`expected.json`) checked in. Updated only via a documented refresh script.
-
-## 13. Acceptance criteria
-
-Sub-project 1 is "done" when **all** of the following are true on the feature branch:
-
-1. **All tests in §12 layers 1–7 pass.** Layers 8 and 9 are non-gating but must run cleanly.
-2. **`mvn verify` green** on CI (full Java CI workflow, including SpotBugs and OWASP dependency-check).
-3. **No logical-content regression** in any existing test (`mvn test` green with `enabled: false`). Snapshot tests refreshed in a separate commit per §8; the refresh diff must be limited to the two additive fields per record.
-4. **E2E petclinic precision/recall measurably improved** with `enabled: true`. The PR description records before/after numbers.
-5. **`CHANGELOG.md`** updated under `[Unreleased]` with a one-paragraph entry naming the new config keys, the schema additions, and the cache-version bump.
-6. **`CLAUDE.md`** updated under "Gotchas" to note: confidence/provenance is now mandatory on every node/edge; the resolver pass is part of the pipeline; cache version is 5.
-7. **`PROJECT_SUMMARY.md`** "Tech stack" + "Gotchas" updated.
-8. **Determinism re-verified** on the migrated detectors (existing determinism tests still pass; new ones added per §12 layer 6).
-9. **No new dependencies with non-permissive licenses** (Apache-2.0 / MIT / BSD only without explicit user sign-off; jqwik EPL-2.0 needs explicit OK or replacement — see §15).
-10. **No new High/Critical CVEs** introduced (`mvn verify` security gate green).
-
-## 14. Risks & mitigations
-
-| Risk | Likelihood | Impact | Mitigation |
-|---|---|---|---|
-| `JavaSymbolSolver` performance worse than budgeted | Medium | Pipeline unusable for very large repos | `enabled: false` escape hatch; performance gate in §9; profile before merge |
-| Source-root auto-discovery wrong on niche project layouts | Medium | Resolver falls back to `EmptyResolved` silently → user sees no improvement | Explicit `source_roots: [list]` override; clear log message at WARN when discovery yields zero roots; `codeiq config explain` shows discovered roots |
-| Confidence schema change breaks consumers (SPA, MCP clients) | Low (additive only) | API drift | Fields are additive; default `LEXICAL`/detector-name. Existing consumers ignore unknown fields per Jackson config (`FAIL_ON_UNKNOWN_PROPERTIES = false`). |
-| Cache-version bump surprises users | Low | One-time slow re-index after upgrade | `CHANGELOG` entry; user-facing log line on first run after bump |
-| jqwik EPL-2.0 license blocked by user policy | Low (already flagged in defaults) | No property-based tests in layer 8 | Hand-write generators or pick a permissive alternative; flagged for decision at impl time |
-| `JavaSymbolSolver` panics on Java 25 idioms (records, sealed, pattern-match) | Medium | Resolver failure on modern Java | Per-file resolution failures are caught (§11); track upstream JavaParser issues; pin to latest JavaParser version |
-| Cross-class resolution still ambiguous with same-named symbols across modules | Medium | False matches even with resolver | Track via E2E quality numbers; flag for sub-project 1.5 (Maven/Gradle classpath JAR resolution) if material |
-
-## 15. Dependency decisions
-
-To be resolved at implementation time (NOT in this spec):
-
-1. **`javaparser-symbol-solver-core` exact version.** Resolve the latest stable version compatible with `javaparser-core` (currently 3.28.0 per CLAUDE.md). Use `context7` MCP first; fall back to Maven Central.
-2. **`net.jqwik:jqwik` license (EPL-2.0).** Per `~/.claude/rules/dependencies.md`: "Permissive licenses (MIT/Apache/BSD) preferred. GPL/AGPL flagged for approval." EPL-2.0 is not GPL/AGPL but is also not on the preferred list. Default plan: ask the user once at implementation time; if blocked, swap for hand-rolled generators or another permissive property-test framework. **Will not add jqwik silently.**
-3. **PIT mutation testing dep.** Apache-2.0; safe to add as a non-default Maven profile.
-
-## 16. Out of scope (cross-reference)
-
-- **TypeScript / JavaScript / Python / Go / Rust / C++ / C# resolution** — sub-projects 2–5. They will plug into the SPI defined here.
-- **Detect-by-resolved-type detector refactor** — sub-project 6. Migrated detectors here keep their current detection mechanism; only their *outgoing edges* benefit from resolution.
-- **Cross-framework false-positive harness** — sub-project 7.
-- **MCP HTTP-streamable hardening** — sub-project 8.
-- **Maven/Gradle classpath JAR resolution** — possible sub-project 1.5 if E2E quality numbers reveal a gap.
-
-## 17. Implementation sequencing (informational, plan owns the detail)
-
-The plan that follows this spec will sequence work as:
-1. Schema changes (`Confidence` enum, `CodeNode`/`CodeEdge` fields, Neo4j round-trip, `AnalysisCache` schema + version bump).
-2. SPI scaffolding (`SymbolResolver`, `Resolved`, `EmptyResolved`, `ResolverRegistry`).
-3. Java backend (`JavaSourceRootDiscovery`, `JavaSymbolResolver`, `JavaResolved`).
-4. Pipeline wiring (`Analyzer`, `IndexCommand`).
-5. Detector migration (one detector at a time, each with new + existing tests passing).
-6. Aggressive testing layers (1–9 in order, layers 8/9 may run in parallel with 5–7).
-7. Doc updates (`CHANGELOG`, `CLAUDE.md`, `PROJECT_SUMMARY.md`).
-8. PR ready for human review when all acceptance criteria green.
-
-## 18. References
-
-- [`PROJECT_SUMMARY.md`](../../PROJECT_SUMMARY.md) — repo-wide entry point.
-- [`CLAUDE.md`](../../CLAUDE.md) — canonical internals.
-- [`docs/project/architecture.md`](../project/architecture.md) — pipeline + components, including the package layering rule that detectors may not depend on `analyzer/`.
-- [`docs/project/data-model.md`](../project/data-model.md) — `NodeKind`, `EdgeKind`, Neo4j schema, H2 cache schema.
-- [`docs/project/conventions.md`](../project/conventions.md) — detector authoring, base classes, "don't refactor" rules.
-- [`docs/project/build-and-run.md`](../project/build-and-run.md) — Maven, ANTLR codegen, frontend bundling.
-- JavaParser symbol-solver documentation: resolve via `context7` MCP at implementation time.
-- Sourcegraph SCIP and GitHub Stack Graphs as comparable patterns (informational only — not adopted in sub-project 1).
diff --git a/docs/specs/2026-04-28-aks-read-only-deploy-design.md b/docs/specs/2026-04-28-aks-read-only-deploy-design.md
deleted file mode 100644
index 3cc4c4a7..00000000
--- a/docs/specs/2026-04-28-aks-read-only-deploy-design.md
+++ /dev/null
@@ -1,178 +0,0 @@
-# Sub-project 2 — AKS read-only deploy hardening
-
-> **Status:** Design ready for implementation. Owner: AI agent + Amit Kumar. Created 2026-04-28.
-
-## 1. Problem
-
-`codeiq serve` is meant to run inside an AKS pod with a **read-only root filesystem** (a hardening default for production Kubernetes). Only `/tmp` is writable. The graph bundle is built in CI via `index → enrich → bundle`, uploaded to a private Nexus registry, then pulled and mounted into the pod read-only at deploy time.
-
-Today's serve mode opens an embedded Neo4j data directory and a fat JAR. Both want to write under the directory they were pointed at:
-
-- **Neo4j Embedded** acquires a `store_lock` file in the DB directory at open, and writes transaction logs / counts / schema cache files even in nominally read-only modes.
-- **Spring Boot fat JAR loader** extracts nested JARs to `~/.m2/spring-boot-loader-tmp/` (or wherever `org.springframework.boot.loader.tmpDir` points) at startup.
-- **JVM** writes `hs_err_pid*.log` and heap dumps to the working directory by default on crash.
-
-Without a deploy-shape change, `serve` fails to boot under `--read-only` because every one of the above tries to write outside `/tmp`.
-
-## 2. Goal
-
-`codeiq serve` runs cleanly inside an AKS pod that has:
-
-- root filesystem mounted **read-only**,
-- `/tmp` mounted as a writable `emptyDir` or `tmpfs`,
-- the graph bundle pulled from Nexus and made available at a known mount path,
-
-with **zero source-code changes to the serve profile or the Neo4j wiring**. Everything is solved at the deployment layer plus a JVM-flag preset.
-
-## 3. Non-goals
-
-- **Not** rewriting the storage layer to a static read-only snapshot (e.g. JSON / Parquet at serve time replacing Neo4j). That's a separate, much larger sub-project. We address it only if the init-container copy approach proves operationally insufficient.
-- **Not** adding mutation endpoints or any write surface to serve mode. The serving layer remains strictly read-only per `CLAUDE.md` §"Read-Only Serving Layer".
-- **Not** changing the build-CI side of the bundle pipeline (`index`, `enrich`, `bundle`) — that runs on a writable build agent.
-- **Not** introducing a hosted backend or static-CDN frontend. The Maven Central + GitHub Releases distribution model from `engineering-standards.md` §7.1 is unchanged. AKS deploy is one of several runtime targets a downstream consumer might pick; the artifacts are the same JAR.
-
-## 4. Approach: init-container copy + JVM flag preset
-
-```
- ┌──────────────────────────────┐
- │ Build CI │
- │ index → enrich → bundle.zip │
- │ upload to Nexus │
- └───────────────┬──────────────┘
- │
- ▼
-┌─────────────────────────────────────────────────────────────────┐
-│ AKS pod (root FS = read-only, /tmp writable) │
-│ │
-│ ┌─────────────── init-container ───────────────┐ │
-│ │ curl --fail "$NEXUS_URL/$BUNDLE" -o /tmp/bundle.zip │ │
-│ │ unzip /tmp/bundle.zip -d /tmp/codeiq-data/ │ │
-│ └────────────────────┬─────────────────────────┘ │
-│ │ (volume share: /tmp via emptyDir) │
-│ ▼ │
-│ ┌─────────────── main container ───────────────┐ │
-│ │ scripts/aks-launch.sh /tmp/codeiq-data │ │
-│ │ → java [JVM flag preset] -jar code-iq.jar │ │
-│ │ serve /tmp/codeiq-data │ │
-│ └──────────────────────────────────────────────┘ │
-└─────────────────────────────────────────────────────────────────┘
-```
-
-The init-container is doing one thing: making the immutable bundle physically present under `/tmp/codeiq-data` so Neo4j can open it in normal (read+write-to-its-own-dir) mode. The main container then runs `serve` with the JVM flags below.
-
-### Why this over the alternatives
-
-| Approach | Verdict | Reasoning |
-|---|---|---|
-| **A. Init-container copy + JVM flags** *(chosen)* | ✅ | Minimal blast radius — zero source-code changes to serve / Neo4j wiring. Neo4j gets a writable directory under `/tmp`, the rest is JVM flags. Easy to test (`docker run --read-only --tmpfs /tmp ...`). |
-| B. Neo4j RO mode + writable temp dir redirects | ❌ | Embedded Neo4j 2026.04.0 still acquires `store_lock` at open. `dbms.directories.transaction.logs.root` redirect needs careful per-version validation. Neo4j's RO mode is more brittle than copying the dir. |
-| C. Bake bundle into container image | ❌ | Couples release cadence to image build; large image; container's writable upper layer is ALSO read-only when mounted `--read-only`, so Neo4j still fails. |
-| D. Replace Neo4j with static snapshot | ❌ | Throws away the entire read API surface (Cypher, indexes, full-text search). Massive scope. Reserved as the "if A doesn't hold" fallback. |
-
-## 5. JVM flag preset
-
-These flags compose at launch via `scripts/aks-launch.sh`. Every entry has a non-default behavior that without it would write outside `/tmp`.
-
-```bash
-JAVA_OPTS=(
- # Spring Boot fat JAR extracts nested JARs at startup. Default is
- # ~/.m2/spring-boot-loader-tmp/ which sits under HOME, outside /tmp.
- "-Dorg.springframework.boot.loader.tmpDir=/tmp/spring-boot-loader"
-
- # Java standard temp dir. Spring Boot's multipart upload temp area,
- # any Files.createTempFile call, JNA / Netty native lib extraction.
- "-Djava.io.tmpdir=/tmp"
-
- # JVM crash dump file (default: cwd/hs_err_pid.log).
- "-XX:ErrorFile=/tmp/hs_err_pid%p.log"
-
- # JVM heap dump on OOM (default: cwd).
- "-XX:HeapDumpPath=/tmp"
- "-XX:+HeapDumpOnOutOfMemoryError"
-
- # Diagnostic VM logs that some JDKs default into cwd.
- "-XX:NativeMemoryTracking=summary"
-)
-```
-
-The preset is **wrapper-script-encoded, not pom.xml**: pom.xml controls the build, not the runtime JVM. The script is the contract surface for AKS deploy.
-
-## 6. Audit findings
-
-| Surface | Default location | Conflict with RO root | Fix |
-|---|---|---|---|
-| Neo4j `store_lock` + tx logs + counts cache | `/.codeiq/graph/graph.db/` | 🚩 yes | Init-container copies bundle to `/tmp/codeiq-data`. No code change. |
-| Spring Boot fat JAR extraction | `~/.m2/spring-boot-loader-tmp/` | 🚩 yes | `-Dorg.springframework.boot.loader.tmpDir=/tmp/spring-boot-loader` |
-| Java standard temp | `java.io.tmpdir` (default `/tmp` on Linux but worth being explicit) | 🟡 environment-dependent | `-Djava.io.tmpdir=/tmp` |
-| JVM crash files (`hs_err_pid*.log`) | cwd | 🚩 yes | `-XX:ErrorFile=/tmp/hs_err_pid%p.log` |
-| JVM heap dumps on OOM | cwd | 🚩 yes | `-XX:HeapDumpPath=/tmp` |
-| Logback file appenders | none — `logback-spring.xml` is console-only | ✅ no | No change. Verified at `src/main/resources/logback-spring.xml`. |
-| H2 analysis cache | `/.codeiq/cache/` | ✅ no — index-time only | No change. |
-| React SPA static assets | classpath: `static/` | ✅ no | No change. |
-| Picocli + Spring AI MCP | in-memory + classpath | ✅ no | No change. |
-| Symbol resolver SPI (sub-project 1) | in-memory; index-time only | ✅ no | No change. |
-
-## 7. Test approach
-
-**Layer 1 — JVM-flag preset sentinel** (unit, fast, CI-gated)
-
-A unit test reads `scripts/aks-launch.sh` and asserts each required `-D` / `-XX:` flag is present and points at a `/tmp` path. Catches drift if someone trims the script. Cheap to keep green.
-
-**Layer 2 — Local docker smoke** (manual, runbook-described, not CI-gated)
-
-The runbook documents:
-
-```bash
-docker run --rm --read-only --tmpfs /tmp:rw,size=2g \
- -v /path/to/bundle:/mnt/bundle:ro \
- codeiq:latest \
- /usr/local/bin/aks-launch.sh /tmp/codeiq-data
-```
-
-The smoke is the *only* honest test of the RO-root assumption — JVM-level filesystem-write detection inside JUnit is environment-fragile (CI runners have different access patterns, no clean `chroot` API in Java). The runbook smoke is the SSoT for "did this actually work?".
-
-**Layer 3 — Integration smoke** (existing `IntegrationSmokeTest`)
-
-The existing `IntegrationSmokeTest` boots `serve` with a real Neo4j data dir from `INTEGRATION_TEST_DIR`. Once the runbook lands, follow-up: extend that test to assert no files appear in `Path.of(".").toAbsolutePath()` after startup. Tracked as a follow-up; not blocking for this sub-project.
-
-## 8. Backward compatibility
-
-- Existing `codeiq serve ` continues to work on a writable filesystem. The launch script is **optional** — a developer-machine launch keeps using `java -jar code-iq-*-cli.jar serve ` with no flags.
-- No new dependencies. No code changes outside the test surface and the script.
-- Not breaking the Maven Central + GitHub Releases distribution channel; consumers who pull the JAR and run it from a local CLI are unaffected.
-
-## 9. Risks
-
-| Risk | Mitigation |
-|---|---|
-| `/tmp` size cap on AKS too small for the graph bundle | Document `emptyDir.sizeLimit: 4Gi` (or larger per repo size) in the runbook init-container manifest. Pre-flight check in the script — fail fast if `/tmp` has < N MB free. |
-| Bundle download from Nexus fails — pod stuck in init | Init-container uses `curl --fail` so a 4xx/5xx aborts. Add a max-retry with backoff in the runbook init-container example. |
-| Init-container copy slow on first deploy (large DB) | Document the trade-off; for very large repos, consider Approach D (static snapshot) as a follow-up — out of scope here. |
-| Future Spring Boot release changes the loader temp-dir flag name | Sentinel test catches the flag presence; runbook lists the flag as Spring Boot 4.x — re-validate on Spring Boot 5.x upgrade. |
-| Neo4j version change introduces a new write target outside the data dir | Caught by the runbook docker smoke before merge of any Neo4j upgrade PR. Make the smoke part of the upgrade checklist in `release.md`. |
-
-## 10. Determinism + observability
-
-- Determinism is unaffected — this is a deploy-layer change. The graph itself is byte-identical for the same input regardless of where it's served from.
-- Add a `/api/diagnostics` (out of scope; tracked) that surfaces the JVM flag preset values for ops verification. Until then, ops can read the launch script directly inside the running container.
-
-## 11. Acceptance criteria
-
-1. **Spec** lands at `docs/specs/2026-04-28-aks-read-only-deploy-design.md`.
-2. **Plan** lands at `docs/plans/2026-04-28-sub-project-2-aks-read-only-deploy.md`.
-3. **Runbook** at `shared/runbooks/aks-read-only-deploy.md` covers: deploy shape, init-container manifest snippet, JVM flag preset, docker smoke, rollback.
-4. **Launch script** at `scripts/aks-launch.sh` composes the JVM flag preset and execs `java -jar`. Has `set -euo pipefail` and validates its single argument.
-5. **Sentinel test** at `src/test/java/.../deploy/AksLaunchScriptSentinelTest.java` asserts the script contains every required flag.
-6. **CHANGELOG.md** `[Unreleased] / Added` entry.
-7. **engineering-standards.md §7.1** cross-link to the new runbook.
-8. **`mvn test`** green.
-9. **PR** opened against `main`. Independent of sub-project 1 — separate base, separate review.
-
-## 12. References
-
-- `~/.claude/CLAUDE.md` — "Deployment assumption: solutions may run behind a corporate firewall / air-gapped"
-- `~/.claude/rules/build.md` — "Self-contained build", "No runtime network calls to the public internet"
-- `CLAUDE.md` (project) — "Read-Only Serving Layer", "Pipeline is index → enrich → serve"
-- `shared/runbooks/engineering-standards.md` §7.1 — "Deploy targets"
-- Spring Boot reference, "Loader" — `org.springframework.boot.loader.tmpDir` system property
-- Neo4j 2026.04.0 — embedded API; `store_lock` behavior
diff --git a/docs/superpowers/baselines/2026-04-17/BASELINE.md b/docs/superpowers/baselines/2026-04-17/BASELINE.md
deleted file mode 100644
index 8a8ecdfe..00000000
--- a/docs/superpowers/baselines/2026-04-17/BASELINE.md
+++ /dev/null
@@ -1,343 +0,0 @@
-# code-iq Baseline — 2026-04-17
-
-This file is generated by `scripts/baseline/consolidate.sh`. Re-run after
-updating any capture script. Raw artifacts under `raw/` are gitignored.
-
-## Toolchain
-
-- Java: openjdk version "25.0.2" 2026-01-20 LTS
-- Maven: [1mApache Maven 3.8.7[m
-- Node: v24.15.0
-- npm: 11.12.1
-
-## Maven build & tests
-
-```json
-{
- "tests": 3059,
- "failures": 0,
- "errors": 0,
- "skipped": 31
-}
-```
-
-## Coverage (JaCoCo)
-
-```json
-{
- "inst_covered": 82247,
- "inst_missed": 10270,
- "br_covered": 5931,
- "br_missed": 2388,
- "line_covered": 16515,
- "line_missed": 1990,
- "inst_pct": 88.9,
- "br_pct": 71.29,
- "line_pct": 89.25
-}
-```
-
-## Flaky tests
-
-```json
-{
- "runs": 3,
- "failures_per_run": [
- 0,
- 0,
- 0
- ],
- "always_failing": [],
- "flaky": []
-}
-```
-
-## SpotBugs
-
-```json
-{
- "total_bugs": 1492,
- "by_priority": {
- "2": 1484,
- "1": 8
- },
- "by_category": {
- "STYLE": 546,
- "MALICIOUS_CODE": 203,
- "I18N": 1,
- "BAD_PRACTICE": 736,
- "MT_CORRECTNESS": 1,
- "PERFORMANCE": 4,
- "CORRECTNESS": 1
- },
- "top_patterns": [
- [
- "NM_METHOD_NAMING_CONVENTION",
- 730
- ],
- [
- "SF_SWITCH_NO_DEFAULT",
- 448
- ],
- [
- "EI_EXPOSE_REP2",
- 77
- ],
- [
- "MS_PKGPROTECT",
- 60
- ],
- [
- "BC_UNCONFIRMED_CAST",
- 55
- ],
- [
- "EI_EXPOSE_REP",
- 46
- ],
- [
- "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE",
- 26
- ],
- [
- "MS_FINAL_PKGPROTECT",
- 20
- ],
- [
- "DLS_DEAD_LOCAL_STORE",
- 5
- ],
- [
- "SF_SWITCH_FALLTHROUGH",
- 4
- ],
- [
- "UC_USELESS_OBJECT",
- 3
- ],
- [
- "CT_CONSTRUCTOR_THROW",
- 2
- ],
- [
- "REC_CATCH_EXCEPTION",
- 2
- ],
- [
- "WMI_WRONG_MAP_ITERATOR",
- 2
- ],
- [
- "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE",
- 2
- ],
- [
- "ES_COMPARING_STRINGS_WITH_EQ",
- 2
- ],
- [
- "DB_DUPLICATE_BRANCHES",
- 1
- ],
- [
- "DM_DEFAULT_ENCODING",
- 1
- ],
- [
- "UL_UNRELEASED_LOCK_EXCEPTION_PATH",
- 1
- ],
- [
- "UPM_UNCALLED_PRIVATE_METHOD",
- 1
- ]
- ]
-}
-```
-
-## Vulnerability scan (OSV-Scanner + GitHub Dependabot)
-
-OWASP dependency-check was replaced with OSV-Scanner + Dependabot because NVD's unauthenticated API rate-limit made full NVD sync impractical (~4 hours to download 345k records at 3%/7min). Both alternatives are free, require no API key, and draw from broader vulnerability databases (OSV aggregates GHSA / RustSec / PyPA / Go vulndb / Maven Central advisories; Dependabot uses GHSA directly).
-
-```json
-{
- "status": "OK",
- "scanner": "osv-scanner 1.9.2 + github dependabot",
- "total": 12,
- "by_severity": {"CRITICAL": 0, "HIGH": 4, "MODERATE": 7, "LOW": 1}
-}
-```
-
-### Findings (sorted by severity)
-
-| Severity | Source | Package | Installed | Fix in | CVE | Summary |
-|---|---|---|---|---|---|---|
-| HIGH | pom.xml | `org.apache.tomcat.embed:tomcat-embed-core` | 11.0.20 | 11.0.21 | CVE-2026-34483 | Improper encoding/escaping in JsonAccessLogValve |
-| HIGH | pom.xml | `org.apache.tomcat.embed:tomcat-embed-core` | 11.0.20 | 11.0.21 | CVE-2026-34487 | Sensitive info insertion into log file |
-| HIGH | pom.xml | `tools.jackson.core:jackson-core` | 3.1.0 | 3.1.1 | (GHSA-2m67-wjpj-xhg9) | Document length constraint bypass in blocking/async/DataInput parsers |
-| HIGH | package-lock.json | `vite` (dev-only) | 6.4.1 | 6.4.2 | CVE-2026-39363 | Arbitrary file read via dev server WebSocket |
-| MODERATE | pom.xml | `io.modelcontextprotocol.sdk:mcp-core` | 1.1.0 | 1.1.1 | CVE-2026-34237 | Hardcoded wildcard CORS (`Access-Control-Allow-Origin: *`) |
-| MODERATE | pom.xml | `org.apache.logging.log4j:log4j-core` | 2.25.3 | 2.25.4 | CVE-2026-34477 | `verifyHostName` silently ignored in TLS config |
-| MODERATE | pom.xml | `org.apache.logging.log4j:log4j-core` | 2.25.3 | 2.25.4 | CVE-2026-34478 | Log injection in `Rfc5424Layout` |
-| MODERATE | pom.xml | `org.apache.logging.log4j:log4j-core` | 2.25.3 | 2.25.4 | CVE-2026-34480 | Silent log-event loss in XmlLayout (forbidden XML 1.0 chars) |
-| MODERATE | pom.xml | `org.apache.logging.log4j:log4j-layout-template-json` | 2.25.3 | 2.25.4 | CVE-2026-34481 | Improper serialization of non-finite floats in JsonTemplateLayout |
-| MODERATE | pom.xml | `org.apache.tomcat.embed:tomcat-embed-core` | 11.0.20 | 11.0.21 | CVE-2026-34500 | CLIENT_CERT auth does not fail as expected |
-| MODERATE | package-lock.json | `vite` (dev-only) | 6.4.1 | 6.4.2 | CVE-2026-39365 | Path traversal in optimized deps `.map` handling |
-| LOW | pom.xml | `org.apache.shiro:shiro-core` | 2.0.6 | 2.1.0 | CVE-2026-23901 | Observable timing discrepancy |
-
-### Remediation shape
-
-- **Spring Boot 4.0.6+ patch release** bumps tomcat-embed-core to 11.0.21 and typically log4j to 2.25.4 — addresses 7 of 12 (2 HIGH + 5 MODERATE).
-- **Explicit bump `tools.jackson.core:jackson-core` → 3.1.1** in pom.xml — addresses 1 HIGH.
-- **Explicit bump `io.modelcontextprotocol.sdk:mcp-core` → 1.1.1** — addresses 1 MODERATE (wildcard CORS on MCP).
-- **`npm install vite@6.4.2 -D`** in `src/main/frontend/` — addresses 2 (HIGH + MODERATE). Dev-only.
-- **Explicit bump `org.apache.shiro:shiro-core` → 2.1.0** — addresses 1 LOW (timing side-channel on password check).
-
-### Dependabot overlap
-
-GitHub Dependabot on `main` independently flagged **CVE-2026-39363** and **CVE-2026-39365** (both `vite`). OSV-Scanner caught those plus the 10 Maven findings Dependabot did not surface on this repo.
-
-## Frontend
-
-- Playwright:
-```json
-{
- "passed": 0,
- "failed": 575,
- "skipped": 0
-}
-```
-- Full logs: `raw/frontend/` (local only).
-
-## Pipeline on seed repos
-
-### spring-petclinic
-```json
-{
- "seed": "spring-petclinic",
- "timings": [
- "index duration=8s rc=0",
- "enrich duration=13s rc=0",
- "health=fail"
- ],
- "stats": null,
- "health_ok": false
-}
-```
-
-### realworld-express
-```json
-{
- "seed": "realworld-express",
- "timings": [
- "index duration=5s rc=0",
- "enrich duration=10s rc=0",
- "health=fail"
- ],
- "stats": null,
- "health_ok": false
-}
-```
-
-## Known gaps / issues
-
-Ordered by severity. Each item cites the raw artifact it was derived from.
-
-### Critical
-
-- **OWASP dependency-check failed.** NVD initial sync hit `UpdateException: Unable to obtain exclusive lock on H2 database` followed by `NoDataException: No documents exist`. Maven exit 1 after 40 min. No CVE inventory captured. Must re-run (see §Re-run instructions below) before any security posture claim.
- - Raw: `raw/depcheck.log`, `raw/depcheck-summary.json` (stub, `status=FAILED`).
- - **RESOLVED via alternative tooling (2026-04-17, branch `phase-a/vuln-scan`)**: replaced OWASP dep-check with OSV-Scanner 1.9.2 + GitHub Dependabot (neither needs an API key; both broader than NVD-direct). Real CVE inventory captured: **12 findings — 0 CRITICAL, 4 HIGH, 7 MODERATE, 1 LOW.** All have known fix versions (minor/patch bumps). HIGH items: 2× tomcat-embed-core→11.0.21, jackson-core→3.1.1, vite→6.4.2 (dev-only). MODERATE items: log4j-core×3, log4j-layout-template-json, mcp-core (hardcoded wildcard CORS), tomcat-embed-core (CLIENT_CERT bypass), vite. LOW: shiro-core timing side-channel. See §Vulnerability scan section above for the full table + remediation shape. Retrying OWASP dep-check would reproduce much the same set and is no longer a release blocker.
-
-- **Playwright E2E: 0 passed / 575 failed.** 100% failure rate. Almost certainly environment/config rather than regressions — the audit script runs `npm run test:e2e` without starting the backend (`java -jar ... serve`), so any test that hits `/api/*` will fail. Needs a harness that spins up the server (or mocks it) before running Playwright, or a `webServer` entry in `playwright.config.ts`.
- - Raw: `raw/frontend/playwright.log`, `raw/frontend/playwright-summary.json`.
- - **RESOLVED — config + system-deps + run (2026-04-17, branch `phase-a/fix-playwright-webserver`)**: added a `webServer` block to `src/main/frontend/playwright.config.ts` that boots `java -jar target/code-iq-*-cli.jar serve .seeds/spring-petclinic --port 8080` and gates readiness on `/api/stats` (HTTP 200). Installed system libs via `sudo npx playwright install-deps chromium`. Ran full chromium suite against the live backend (19.2 min wall time).
-
- **New chromium baseline**: `33 passed / 94 failed / 4 skipped` out of **131 tests**. Huge improvement from "0 passed / 575 failed" (the 575 was the sum across 7 Playwright projects — chromium/firefox/edge/webkit + 3 responsive breakpoints). The 94 chromium failures are **not environmental** — they're real test ↔ UI divergence. Signature histogram of the 22 detailed failure blocks analyzed:
-
- | Pattern | Interpretation |
- |---|---|
- | `expect(locator).toBeVisible() failed` / `element(s) not found` | Selectors targeting UI elements that no longer render (renamed / removed). |
- | `strict mode violation: getByText(/0/) resolved to 8 elements` | Tests assumed unique stats values; spring-petclinic's 691-node graph produces many zero-valued stat cards. |
- | `expect(received).toHaveLength(expected)` | List count mismatches (menu items, nav links, stats rows). |
- | `Error: Button ... has no aria-label` | Real a11y regressions in `accessibility.spec.ts`. |
- | `locator.focus: Test timeout 30000ms exceeded` | UI interactions hanging on elements that never appear. |
-
- Test-suite maintenance / fixture generation is a follow-up (not a Phase A blocker). Candidate approaches: (1) rebuild the Ant-Design selectors to match current UI taxonomy; (2) pin a synthetic fixture with known, deterministic stats counts; (3) relax strict-mode selectors to `.first()` where tests genuinely target "any such element". 22 of the 94 failures have full error blocks captured in `raw/frontend/playwright.log`.
-
-### High
-
-- **Pipeline serve-smoke failed on both seed repos** (`health=fail`, `stats=null`). `index` and `enrich` succeeded (petclinic 8+13s, express 5+10s) but the 8-second sleep between starting `serve` and `curl /actuator/health` is at the low end of the documented 8–16s Spring Boot + embedded Neo4j cold-start window (see CLAUDE.md §Gotchas). Fix in Phase F hardening: poll `/actuator/health` with a retry budget instead of a fixed sleep.
- - Raw: `raw/pipeline/spring-petclinic/`, `raw/pipeline/realworld-express/`.
- - **RESOLVED (2026-04-17, branch `phase-a/fixups-pipeline-smoke`)**: patched `run-pipeline.sh` to poll `/api/stats` (up to 60s at 2s interval) as the readiness probe and to capture `/actuator/health` only as a diagnostic. Root cause was *not* a too-short sleep — the server cold-starts in 10–11s on both seeds and `/api/stats` responds with real data, but `/actuator/health` returns HTTP **503 `OUT_OF_SERVICE`** because the `GraphHealthIndicator` reports OUT_OF_SERVICE even after the graph loads. Captured baseline numbers below.
-
- | Seed | index | enrich | ready (stats) | nodes | edges | files | languages | frameworks | health HTTP |
- |---|---:|---:|---:|---:|---:|---:|---|---|---:|
- | spring-petclinic | 4s | 11s | 11s | 691 | 1,836 | 67 | java 18 | spring_boot 24 | 503 |
- | realworld-express | 5s | 10s | 10s | 224 | 297 | 39 | typescript 6 | express 20, prisma 7 | 503 |
-
- Follow-up split out below.
-
-- **`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`.
- - **PARTIALLY RESOLVED — NP_NULL subset (2026-04-17, branch `phase-a/fix-np-null`)**: all 26 `NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE` findings fixed — count 26 → 0. All 26 shared one pattern: calling `.toString()` on `Path.getFileName()` (or once `Path.getParent()`) where the result can be null for root-like paths. Fix: replaced every site with `java.util.Objects.toString(path.getFileName(), fallback)` using sensible per-site fallbacks (`""` for filename comparisons, `"unknown"` / `"bundle"` / `"flow"` for human-facing project names, `PROP_ROOT` in Analyzer). One `AnalysisCache` constructor call (`Files.createDirectories(dbPath.getParent())`) rewritten as a null-guarded block. 12 files touched (Analyzer.java 5 sites inc. a triplicated block, plus FileDiscovery, FileClassifier, ServiceDetector, ConfigScanner, AnalysisCache, BundleCommand, EnrichCommand, FlowCommand, PluginsCommand, StatsCommand, TopologyCommand). Full `mvn test` still green (3,059 tests, 0 failures). The broader SpotBugs triage (noise exclude + priority-1 fixes) lives on `phase-a/fixups-spotbugs`.
-
-### Medium
-
-- **Branch coverage 71.3% is notably below instruction coverage 89.0%.** Expected for a detector-heavy codebase, but targeted branch coverage on the enrichment / linker / LayerClassifier paths (which drive deterministic output) is worth a focused improvement pass in Phase E.
- - Raw: `raw/coverage-summary.json`, `raw/jacoco.csv`.
-
-- **31 skipped tests.** Not investigated. Read surefire reports to confirm they're intentional (`@Disabled` / profile-gated) and not silently excluded.
- - Raw: `raw/surefire-reports.tar`.
-
-### Low / noise
-
-- `consolidate.sh` prints the Maven version with raw ANSI escape codes (`[1mApache Maven 3.8.7[m`). Strip with `sed 's/\x1b\[[0-9;]*m//g'` in a follow-up. Cosmetic only.
-
-### Green
-
-- **3,059 tests, 0 failures, 0 errors.** Clean.
-- **Flaky scan: 0 always-failing, 0 flaky across 3 runs.** Suite is stable.
-- **Instruction coverage 89.0%**, line coverage 89.25%. Strong baseline.
-- **`npm audit` + Vite build: no blocking issues** recorded in the capture.
-- **Pipeline `index` and `enrich` succeeded deterministically** on both seed repos.
-
-## Re-run instructions (for blocked captures)
-
-### OWASP dep-check
-```bash
-# 1. Stop any lingering dep-check processes
-pkill -f dependency-check 2>/dev/null
-# 2. Clear NVD locks (and optionally wipe the partial DB)
-rm -f ~/.m2/repository/org/owasp/dependency-check-data/11.0/*.lock
-# rm -rf ~/.m2/repository/org/owasp/dependency-check-data/11.0 # fallback if DB is corrupt
-# 3. Re-run
-./scripts/baseline/run-depcheck.sh
-```
-
-### Pipeline serve-smoke
-Patch `scripts/baseline/run-pipeline.sh` to replace the `sleep 8` with a poll loop:
-```bash
-for _ in $(seq 1 60); do
- if curl -sf "http://127.0.0.1:$PORT/actuator/health" > "$OUT/health.json"; then break; fi
- sleep 2
-done
-```
-Then re-run `./scripts/baseline/run-pipeline.sh spring-petclinic` and `realworld-express`.
-
-### Playwright E2E
-Add a `webServer` entry to `src/main/frontend/playwright.config.ts` that starts the code-iq server against a fixture repo, or supply a mock backend. Then re-run `./scripts/baseline/run-frontend-audit.sh`.
-
-## Handoff to subsequent phases
-
-- **Phase B (unified config)** — `codeiq.yml` smoke test against both seed repos; validation script gates CI.
-- **Phase D (MCP robustness)** — pipeline serve-smoke fix above is a prerequisite for any MCP contract test.
-- **Phase E (determinism)** — `index → enrich` reproducibility on the two seed repos above is the seed for graph-snapshot diffing; 31 skipped tests to triage.
-- **Phase F (ops & perf)** — Playwright harness + cold-start budgets.
diff --git a/docs/superpowers/baselines/2026-04-17/PHASE-B-EXIT-GATE.md b/docs/superpowers/baselines/2026-04-17/PHASE-B-EXIT-GATE.md
deleted file mode 100644
index 2acd2826..00000000
--- a/docs/superpowers/baselines/2026-04-17/PHASE-B-EXIT-GATE.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# Phase B Exit-Gate Verification — 2026-04-22
-
-**Branch:** `phase-b/unified-config`
-**Head commit:** `5356630 docs(config): document codeiq.yml, resolution order, and migration from .osscodeiq.yml`
-**Final test count:** **3275 pass / 0 fail / 0 errors / 31 skipped** (`mvn -B test`, BUILD SUCCESS)
-
-## Gate status
-
-| # | Gate | Status | Evidence |
-|---|---|---|---|
-| 1 | Single source of truth — `codeiq.yml` is authoritative; `application.yml` no longer duplicates migrated keys | PASS | `src/main/resources/application.yml` contains zero instances of `codeiq.root-path`, `codeiq.cache-dir`, `codeiq.graph.path`, `codeiq.max-depth`, `codeiq.max-radius`, `codeiq.batch-size`. Remaining `codeiq.*` keys are exactly: `codeiq.ui.enabled` (L33-35), `codeiq.neo4j.enabled` (L56-58 indexing profile, L94-96 serving profile). `codeiq.neo4j.bolt.port` and `codeiq.cors.allowed-origin-patterns` consume `@Value` defaults with no YAML override (documented at L25-32). |
-| 2 | Layered resolution — defaults → user-global → project → env → CLI | PASS | `src/main/java/io/github/randomcodespace/iq/config/unified/ConfigLayer.java` enumerates `BUILT_IN, USER_GLOBAL, PROJECT, ENV, CLI`. `ConfigResolver.resolve()` appends layers in that exact order into `ConfigMerger.Input` list; "last wins" semantics are documented in the class Javadoc. |
-| 3 | Provenance — `config explain` prints per-field source | PASS | `ConfigExplainSubcommand` row format `FIELD(40) LAYER(12) SOURCE(40) VALUE`. `ConfigExplainSubcommandTest.printsProvenanceForEachLeaf` asserts stdout contains `serving.port`, value `9000`, layer `PROJECT`, plus `ENV` layer for env-overridden `mcp.limits.per_tool_timeout_ms=30000`, and at least one `BUILT_IN` leaf. `cliOverlayWinsOverEnv` test asserts CLI > ENV precedence on the explain output. |
-| 4 | Validation — `config validate` returns exit 0/1 on valid/invalid | PASS | `ConfigValidateSubcommand` returns `1` on validation errors (sorted by `fieldPath`, written to stderr) or load failure. `ConfigValidateSubcommandTest` covers: `invalidFileReturnsOneAndListsErrorsOnStderr` (port 99999 → exit 1, stderr contains `serving.port`), `missingFileReturnsOneAndPrintsLoadErrorToStderr`, `malformedYamlReturnsOneAndReportsLoadError`, `emptyFileIsValidAndReturnsZero`. |
-| 5 | Env var overlay — `CODEIQ__` works across sections | PASS | `EnvVarOverlayTest` covers 6 cases: `readsServingPort`, `readsNestedMcpLimit` (`CODEIQ_MCP_LIMITS_PERTOOLTIMEOUTMS`), `parsesBooleansAndLists` (`CODEIQ_INDEXING_LANGUAGES=java,typescript,python`), `unknownVarIsIgnored`, `nonCodeiqVarsIgnored`, `malformedIntThrowsWithVarName`. |
-| 6 | Schema documented — `docs/codeiq.yml.example` exists, snake_case throughout | PASS | File exists with 6 sections matching `CodeIqUnifiedConfig` record: `project`, `indexing`, `serving`, `mcp`, `observability`, `detectors`. Only "camelCase" hits in real content are `SpringRestDetector`/`QuarkusRestDetector` — Java SimpleClassName keys under `detectors.overrides`, which is the documented convention, not config key casing. Header explicitly calls out camelCase as deprecated alias. |
-| 7 | `.osscodeiq.yml` deprecation — WARN once per path; legacy flat keys translated | PASS | `ProjectConfigLoader.loadFrom` uses `ConcurrentHashMap.newKeySet()` (`WARNED_PATHS`) at L70; WARN emitted only on first `add(canonical)`. `ProjectConfigLoaderTest` (14 tests) covers: `preferCodeiqYmlWhenBothPresent` (new file wins, no WARN), `fallsBackToOsscodeIqWithWarn`, `fallbackOsscodeiqWithFlatKeysTranslatesToUnifiedOverlay`, `fallbackOsscodeiqWithNewShapeStillWorks`, `mixedLegacyFlatAndNestedKeysPrefersLegacyPath`, `neitherFilePresentReturnsEmptyConfig`. Per-path dedupe test is **not explicitly covered** (see follow-ups). |
-| 8 | `CodeIqConfig` API unchanged | PASS | All legacy getters present with original signatures: `getRootPath`/`setRootPath` (L62-66), `getCacheDir` (L70), `getMaxDepth` (L78), `getMaxFiles` (L86), `getMaxRadius` (L94), `getBatchSize` (L102), `getServiceName` (L118), `getGraph` (L126), `getMaxSnippetLines` (L142). Inner `Graph.getPath`/`setPath` (L50-51). 27 source files still reference `CodeIqConfig`. |
-| 9 | Test count baseline — 3275+ tests, 0 failures | PASS | `mvn -B test` → `Tests run: 3275, Failures: 0, Errors: 0, Skipped: 31` — `BUILD SUCCESS`. |
-| 10 | No regressions — `.osscodeiq.yml` still loads for legacy users | PASS | Covered by `ProjectConfigLoaderTest.fallsBackToOsscodeIqWithWarn` and `fallbackOsscodeiqWithNewShapeStillWorks`. SpotBugs / frontend build not re-verified in this gate pass (neither is a §3.6 requirement; see follow-ups for any outstanding Phase-A items). |
-
-## Spec §3.6 acceptance criteria (direct mapping)
-
-| Spec criterion | Plan task | Verified via |
-|---|---|---|
-| One file controls pipeline end-to-end; no CLI flag for default run | Task 14 gate 1 | `CodeIqUnifiedConfig` + `UnifiedConfigBeans` wire the full tree; all previously-required CLI overrides now read from `codeiq.yml` via `ConfigResolver`. Full pipeline smoke (`java -jar ... index .`) deferred to release candidate (jar not built in this verification pass) — all unit + integration paths pass. |
-| `code-iq config explain` prints effective config + source per value | Task 14 gate 2 | `ConfigExplainSubcommand` + passing tests above. |
-| Deprecation warning fires when `.osscodeiq.yml` is used | Task 14 gate 3 | `ProjectConfigLoader.loadFrom` L107 `log.warn(...)`; `fallsBackToOsscodeIqWithWarn` test asserts `r.deprecationWarningEmitted() == true`. |
-| Invalid config yields a clear, file-anchored error | Task 14 gate 4 | `ConfigValidateSubcommand` sorts `ConfigError.fieldPath()` to stderr with `field.path: message` format; `invalidFileReturnsOneAndListsErrorsOnStderr` asserts `serving.port` appears in stderr for out-of-range port. |
-
-## Docs-vs-implementation sync
-
-- `README.md` §Configuration (L158-218) — documents `codeiq.yml` as single source, resolution order (5 layers, last wins), `config validate` / `config explain` commands, minimal example (snake_case), and the 4 Spring-owned keys. Matches implementation.
-- `CLAUDE.md` §Configuration (L368-409) — same structure, including `.osscodeiq.yml` deprecation section pointing to `ProjectConfigLoader`. Matches implementation.
-
-## Release blockers
-
-**None.** Phase B meets all §3.6 acceptance criteria. All code paths exercised by 3275 passing tests.
-
-## Post-release follow-ups
-
-Tracked issues (priority: post-release):
-
-- **#47** — Detector taxonomy refactor (post)
-- **#48** — SQL / migration detector (post)
-- **#49** — Freeze `CodeIqConfig` setters (post — setter mutability does not affect Phase B's contract; unified config is the write path)
-- **#50** — Slice `UnifiedConfigBeansTest` (post)
-- **#52** — Retire legacy `ProjectConfigLoader` static methods + migrate Analyzer/CliOutput (post — static methods are marked `@Deprecated since 0.2.0, for removal` in javadoc, and `loadFrom` is the new instance path; they can be removed once Analyzer/CliOutput are migrated in a follow-up, without breaking Phase B's single-source-of-truth gate)
-
-Minor gaps noted (not blockers):
-
-- `ProjectConfigLoaderTest` covers WARN emission via `LoadResult.deprecationWarningEmitted()` but has no explicit test that calling `loadFrom` twice against the same canonical path emits the WARN only once. The logic (`WARNED_PATHS.add(canonical)`) is correct; a unit test asserting dedupe would be a belt-and-braces addition. **Recommend: add as a trivial follow-up, not a release blocker.**
-- Frontend build and SpotBugs not re-executed in this verification pass — neither is a §3.6 criterion. Phase A baseline covered them; no Phase B changes touched frontend or triggered new SpotBugs findings.
-- End-to-end smoke test with packaged jar (`java -jar .../code-iq-*-cli.jar index .`) was not run because the packaged jar is not a §3.6 artifact — the four acceptance criteria are fully covered by the subcommand unit tests plus the unified-config loader/merger/resolver tests. Recommended as a final pre-tag sanity check when the release candidate is built.
-
-## Final verdict
-
-**APPROVED TO MERGE.** Phase B (Pillar 1 — Unified Config) has met every §3.6 acceptance criterion with passing, deterministic test coverage. Single source of truth (`codeiq.yml`) verified; 5-layer resolution (`BUILT_IN → USER_GLOBAL → PROJECT → ENV → CLI`) verified; provenance surfaced by `config explain`; validation errors are file-anchored and exit 1; `.osscodeiq.yml` deprecation shim translates legacy flat keys and emits a per-path WARN; legacy `CodeIqConfig` API surface preserved; `application.yml` reduced to Spring-framework-consumed keys only (all 4 documented). Full suite green: 3275/0/31.
diff --git a/docs/superpowers/plans/2026-05-13-enrich-oom-fix.md b/docs/superpowers/plans/2026-05-13-enrich-oom-fix.md
deleted file mode 100644
index 2c0a69fa..00000000
--- a/docs/superpowers/plans/2026-05-13-enrich-oom-fix.md
+++ /dev/null
@@ -1,829 +0,0 @@
-# Enrich Pipeline OOM Fix — Streaming Refactor
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Eliminate the `codeiq enrich` OOM on real-world polyglot codebases (~/projects/-scale: 49k files / 434k nodes) while keeping output bytes-identical to today's pipeline. The bar is "no OOM, ever, on any input that fits in disk."
-
-**Architecture:** Three coordinated refactors, sequenced low → high risk so each ships independently and yields measurable improvement on its own.
-
-1. **Phase A — Quick wins** (4 surgical fixes). Targets the actual pprof hotspots: parse-storm, unbounded goroutines, Kuzu buffer pool default, GraphBuilder dual-lifetime. Expected to drop peak RSS by 70-85% on its own.
-2. **Phase B — TreeCursor migration**. Replaces `parser.Walk`'s recursive `Node.Child()` traversal with tree-sitter's `TreeCursor`. Eliminates the largest single allocator (`cachedNode` — 91% of churn).
-3. **Phase C — Streaming three-pass enrich**. The architectural fix. Decouples enrich stages so the full graph is never materialised in Go memory. Memory-safe by construction; scales to 10M+ nodes.
-4. **Phase D — Verification harness**. Memory + wall-time benchmark suite that pins the regression bar in CI.
-
-**Tech Stack:** Go 1.25.10, Kuzu 0.7.1 (`github.com/kuzudb/go-kuzu`), tree-sitter (`github.com/smacker/go-tree-sitter`), SQLite (`mattn/go-sqlite3`). CGO required everywhere. Already in use — no new dependencies for Phases A + B; Phase C may add `lanrat/extsort` (28★, Apache-2) if disk-spill becomes necessary at extreme scale.
-
-**Execution mode:** ralph-loop with **no iteration limit**. The loop drives this plan to completion via the recipe in §"Ralph-loop execution recipe" below. No human gates within phases; humans approve PRs at phase boundaries.
-
----
-
-## Ralph-loop execution recipe
-
-This plan is the loop's source of truth. The loop's prompt should be:
-
-> `Read /home/dev/projects/codeiq/docs/superpowers/plans/2026-05-13-enrich-oom-fix.md and progress at /home/dev/projects/codeiq/.claude/oom-fix-progress.md. Execute the next undone Task per the iteration recipe in the plan. Update the progress file after every Step. Only exit when OOM FIXED on ~/projects/ is genuinely true per the acceptance criteria in §"Completion promise".`
-
-### Completion promise
-
-The loop MUST NOT emit `OOM FIXED on ~/projects/` until ALL of the following hold:
-
-1. All Phase A, B, C, D tasks are checked complete in the progress file.
-2. The 4 PRs (one per phase) are merged into `main`. Verified via `gh pr list --state merged --head --json mergedAt`.
-3. `/usr/bin/time -v codeiq enrich ~/projects/` runs to completion with exit 0 AND peak RSS < 4 GiB (extracted from `Maximum resident set size (kbytes):` line).
-4. `codeiq stats ~/projects/` returns a non-empty `graph.nodes` count (proves the Kuzu graph populated).
-5. `cd go && CGO_ENABLED=1 go test ./... -count=1 -race` is green on `main` HEAD.
-6. The perf-gate CI added in Task D1 is green on `main` HEAD.
-
-If any criterion is unmet, continue iterating. Do NOT emit a false promise.
-
-### Per-iteration recipe
-
-```dot
-digraph ralph_loop {
- start [shape=doublecircle, label="iteration start"];
- read_prog [shape=box, label="Read progress file\n.claude/oom-fix-progress.md"];
- find_next [shape=diamond, label="Find next undone\nTask in the plan"];
- all_done [shape=diamond, label="All Tasks done?"];
- accept [shape=diamond, label="Acceptance criteria\nmet (§ Completion promise)?"];
- promise [shape=doublecircle, label="OOM FIXED\nEXIT LOOP"];
- enter_wt [shape=box, label="Enter worktree for Task"];
- exec_steps [shape=box, label="Execute Task Steps\nin order, marking each\ncomplete in progress file"];
- tests [shape=diamond, label="All tests + benchmarks\npass for this Task?"];
- blocker [shape=box, label="Mark Task blocked\nin progress, document\nthe failure"];
- open_pr [shape=diamond, label="Last Task in Phase?"];
- pr_phase [shape=box, label="Open PR for the\nwhole Phase"];
- pr_wait [shape=diamond, label="Phase PR merged?"];
- next_phase [shape=box, label="Pull main, start\nnext Phase"];
- next_task [shape=box, label="Move to next\nTask in Phase"];
-
- start -> read_prog;
- read_prog -> all_done;
- all_done -> accept [label="yes"];
- all_done -> find_next [label="no"];
- accept -> promise [label="yes"];
- accept -> find_next [label="no"];
- find_next -> enter_wt;
- enter_wt -> exec_steps;
- exec_steps -> tests;
- tests -> blocker [label="no"];
- tests -> open_pr [label="yes"];
- blocker -> start [label="next iter"];
- open_pr -> pr_phase [label="yes"];
- open_pr -> next_task [label="no"];
- pr_phase -> pr_wait;
- pr_wait -> next_phase [label="yes"];
- pr_wait -> start [label="no, wait next iter"];
- next_phase -> start;
- next_task -> start;
-}
-```
-
-### Progress file format
-
-The loop maintains `/home/dev/projects/codeiq/.claude/oom-fix-progress.md` (gitignored; the `.claude/` directory is already in `.gitignore`):
-
-```markdown
-# OOM Fix Progress
-
-Started: 2026-05-13
-Plan: docs/superpowers/plans/2026-05-13-enrich-oom-fix.md
-
-## Phase A — Quick wins
-- [x] Task A1: Parse once per file in LanguageEnricher — PR #144 merged 2026-05-13
-- [ ] Task A2: Bounded goroutine pool — in_progress, branch perf/enricher-bounded-pool
-- [ ] Task A3: Cap Kuzu BufferPoolSize + CALL threads
-- [ ] Task A4: Free GraphBuilder maps after Snapshot
-
-## Phase B — TreeCursor migration
-- [ ] Task B1: Rewrite parser.Walk to use TreeCursor
-
-## Phase C — Streaming three-pass enrich
-- [ ] Task C1: Define streaming interfaces (lands with C2)
-- [ ] Task C2: Implement Pass 1 — Index build
-- [ ] Task C3: Pass 2 — Linkers against compact index
-- [ ] Task C4: Pass 3 — Streaming load with bounded-batch BulkLoad
-- [ ] Task C5: Cut over enrich.go to three-pass
-
-## Phase D — Verification harness
-- [ ] Task D1: Memory regression test in CI
-- [ ] Task D2: Real-world acceptance run
-
-## Blockers
-(none currently — update if a Step fails twice)
-
-## Acceptance checklist (§ Completion promise)
-- [ ] All Tasks complete
-- [ ] 4 phase PRs merged
-- [ ] /usr/bin/time -v codeiq enrich ~/projects/ < 4 GiB peak RSS
-- [ ] codeiq stats ~/projects/ returns non-empty graph
-- [ ] go test ./... -count=1 -race green on main
-- [ ] perf-gate CI green on main
-```
-
-### Inter-task / inter-phase semantics
-
-- **Inside a phase**: tasks land as separate commits on ONE phase branch. The loop iterates Steps within a Task to completion, then moves to the next Task on the same branch.
-- **At phase end**: open ONE PR per phase (4 PRs total: `perf/enrich-oom-phase-a`, `…-phase-b`, `…-phase-c`, `…-phase-d`). The PR body lists all Tasks landed and the measured improvement.
-- **Phase gating**: do not start Phase N+1 until Phase N's PR is merged. The loop polls `gh pr view --json state` between iterations; if state ≠ MERGED, the loop's next iteration just re-checks (idempotent). User merges at their cadence; loop does not auto-merge.
-- **Worktree isolation**: each phase runs in its own worktree (`EnterWorktree name=enrich-oom-phase-X`). Worktree torn down after the phase PR merges.
-
-### Failure / blocker handling
-
-- Steps must verify themselves (tests + smoke). If a Step fails twice in a row, the loop DOES NOT retry a third time. It marks the Task `blocked`, writes the diagnosis to `## Blockers` in the progress file, and moves to the next undone Task (cross-phase if needed). At loop wake-up, blockers surface for human review.
-- Some Tasks have hard dependencies (e.g. C3 depends on C2). If C2 is blocked, C3 stays pending; loop moves on to other phases' tasks.
-- No silent skips: every `blocked` is documented with file paths, error messages, and what was tried.
-
-### Stop conditions
-
-The loop stops ONLY when `OOM FIXED on ~/projects/` is emitted, which requires all 6 acceptance criteria in §"Completion promise" to hold. There is no iteration cap; the loop continues until done OR a hard blocker requires human intervention (in which case it surfaces the blocker via a clearly-marked progress-file entry and waits for the next wake-up).
-
----
-
-**Evidence base (research that informed this plan):**
-
-- Empirical pprof on airflow (9,151 Py files): 91% of all allocations come from `tree-sitter.(*Tree).cachedNode`, with peak RSS 3.8 GB driven by transient parse-storm churn, not retention. inuse_space post-GC is only 4.5 MB — Go GC keeps up at small scale but loses at ~/projects scale.
-- Trajectory: istio (5.2k files / 1.1 GB peak) → airflow (9.1k / 3.8 GB) → ~/projects extrapolation 9-15 GB → OOM on 15 GB host.
-- Code-walk: `enrich.go:68` never nils the `GraphBuilder`, so its dedup maps (~280 MB) coexist with the snapshot slices for the whole pipeline. `enrich.go:69-70` slices grow through every stage and are never chunked.
-- Kuzu: `SystemConfig.DefaultSystemConfig()` allocates 80% of system RAM as buffer pool. Kuzu has no streaming/Appender API in v0.7.1 through v0.11.3 (issue #2739 still open). String-PK hash index for COPY FROM is not buffer-pool tracked and cannot spill (issue #4937).
-- ETL patterns: ID-only dedup (~24 MB for 434k IDs) + compact linker side-table (drops Properties/Annotations, ~35 MB) is the surgical streaming pattern. `extsort` is the fallback for >5M-node scale.
-
----
-
-## Phase A — Quick wins
-
-Four independent fixes. Each is its own PR. Sequence: A1 → A2 → A3 → A4 (no inter-task dependencies, but the order minimises rebase churn since A1 + A2 both touch `extractor/enricher.go`).
-
-### Task A1: Parse once per file in LanguageEnricher
-
-**Context.** `internal/intelligence/extractor/enricher.go:100-130` spawns one goroutine per source file. Inside, each goroutine iterates the file's nodes and calls `t.ext.Extract(ctx, n)`. Every `Extract()` implementation (`java/extractor.go`, `python/extractor.go`, `typescript/extractor.go`, `golang/extractor.go`) calls `parser.ParseByName(lang, []byte(ctx.Content))` at its top — re-parsing the same file once per node. At ~13 nodes/file on Python this is a 13× over-parse and the dominant allocation driver.
-
-**Fix.** Hoist the parse out of `Extract`. The extractor goroutine parses the file once, then calls a new method `ext.ExtractFromTree(ctx, tree, nodes)` that walks the prebuilt tree to produce edges for all of the file's nodes.
-
-**Files:**
-- Modify: `go/internal/intelligence/extractor/extractor.go` — extend `LanguageExtractor` interface with `ExtractFromTree(ctx Context, tree *sitter.Tree, nodes []*model.CodeNode) []*model.CodeEdge`. Keep the existing `Extract` as a thin wrapper that calls parse + ExtractFromTree, for back-compat in tests.
-- Modify: `go/internal/intelligence/extractor/enricher.go` — in the per-file goroutine, parse once, call `ExtractFromTree` instead of looping `Extract` per node.
-- Modify: `go/internal/intelligence/extractor/{java,python,typescript,golang}/extractor.go` — implement `ExtractFromTree`; refactor `Extract` to wrap it.
-- Modify: `go/internal/intelligence/extractor/{java,python,typescript,golang}/*_test.go` — update tests if they test private parse paths; otherwise no-op.
-
-- [ ] **Step 1: Add `ExtractFromTree` to the interface**
-
-`go/internal/intelligence/extractor/extractor.go`:
-
-```go
-type LanguageExtractor interface {
- Language() string
- Extract(ctx Context, node *model.CodeNode) []*model.CodeEdge
- // ExtractFromTree walks a pre-parsed tree-sitter Tree once and emits
- // edges for every node in `nodes` belonging to the same file. This
- // replaces N per-node calls to Extract on the same file (each of which
- // re-parses) with one call that visits the AST a single time.
- ExtractFromTree(ctx Context, tree *sitter.Tree, nodes []*model.CodeNode) []*model.CodeEdge
-}
-```
-
-Run `go build ./...` — expect compile errors in the 4 extractor packages until they implement the new method.
-
-- [ ] **Step 2: Implement `ExtractFromTree` in `python/extractor.go`**
-
-Pull the current body of `Extract` (which calls `ParseByName` then walks the tree to find calls) into a helper `walkForCalls(tree, node) []CodeEdge`. New `ExtractFromTree` calls `walkForCalls(tree, n)` once per node, sharing the tree. `Extract` becomes:
-
-```go
-func (e *Extractor) Extract(ctx Context, n *model.CodeNode) []*model.CodeEdge {
- tree, err := parser.ParseByName("python", []byte(ctx.Content))
- if err != nil { return nil }
- defer tree.Close()
- return e.ExtractFromTree(ctx, tree, []*model.CodeNode{n})
-}
-```
-
-- [ ] **Step 3: Run python extractor tests**
-
-`cd go && CGO_ENABLED=1 go test ./internal/intelligence/extractor/python/... -count=1 -v`
-
-Tests must still pass with identical output.
-
-- [ ] **Step 4: Repeat Steps 2-3 for java, typescript, golang**
-
-- [ ] **Step 5: Update `enricher.go` to call `ExtractFromTree`**
-
-Replace the per-node loop in `enricher.go:97-130` with:
-
-```go
-go func(i int, t task) {
- defer wg.Done()
- raw, err := os.ReadFile(t.file)
- if err != nil { return }
- ctx := buildContext(t, raw)
- tree, err := parser.ParseByName(t.ext.Language(), raw)
- if err != nil { return }
- defer tree.Close()
- out[i] = t.ext.ExtractFromTree(ctx, tree, t.nodes)
-}(i, t)
-```
-
-- [ ] **Step 6: Full enrich-related test pass**
-
-```
-cd go && CGO_ENABLED=1 go test ./internal/intelligence/extractor/... ./internal/analyzer/... -count=1
-```
-
-- [ ] **Step 7: Benchmark before/after on fixture-multi-lang**
-
-Build, then time the enrich. Record peak RSS via `/usr/bin/time -v`. Expect: allocations down ~13×, peak RSS down meaningfully but smaller than the gain from Task A2.
-
-- [ ] **Step 8: Commit + PR**
-
-```
-git checkout -b perf/enrich-parse-once-per-file
-git commit -m "perf(enricher): parse tree-sitter tree once per file, not per node
-
-Each LanguageExtractor.Extract reparsed the source file at its top —
-on Python at ~13 nodes/file that meant 13x over-parse. pprof on
-airflow showed 91% of total allocations from tree-sitter.Tree.cachedNode.
-
-Adds ExtractFromTree(ctx, tree, nodes []*CodeNode) []*CodeEdge to the
-LanguageExtractor interface; enricher goroutines now parse once and
-walk the shared tree for every node in that file."
-```
-
----
-
-### Task A2: Bounded goroutine pool in LanguageEnricher
-
-**Context.** `enricher.go:97-130` spawns one goroutine per source file unbounded. On airflow's 7,456 Python files that's 7,456 concurrent live trees + file content strings. Peak RSS spikes when many of these are live simultaneously.
-
-**Fix.** Replace the bare fan-out with a semaphore-bounded pool sized to `2 * runtime.GOMAXPROCS(0)`. Preserves determinism because results are still written to `out[i]` indexed by task slot.
-
-**Files:**
-- Modify: `go/internal/intelligence/extractor/enricher.go` — add semaphore channel; acquire before `go func`, release at goroutine end.
-- Modify: `go/internal/intelligence/extractor/enricher_test.go` — add a test asserting concurrency cap (count concurrent goroutines via a runtime counter).
-
-- [ ] **Step 1: Write the test first**
-
-```go
-func TestEnricherBoundedConcurrency(t *testing.T) {
- var inFlight, maxInFlight int32
- // Drive enricher with N=200 fake tasks that each sleep so we can
- // observe peak concurrency. Assert max <= 2 * GOMAXPROCS.
- ...
- cap := int32(2 * runtime.GOMAXPROCS(0))
- if maxInFlight > cap {
- t.Fatalf("peak concurrent goroutines = %d, want <= %d", maxInFlight, cap)
- }
-}
-```
-
-- [ ] **Step 2: Run test — watch it fail**
-
-- [ ] **Step 3: Implement the semaphore**
-
-```go
-sem := make(chan struct{}, 2*runtime.GOMAXPROCS(0))
-for i, t := range tasks {
- wg.Add(1)
- sem <- struct{}{}
- go func(i int, t task) {
- defer wg.Done()
- defer func() { <-sem }()
- // existing body
- }(i, t)
-}
-```
-
-- [ ] **Step 4: Watch test pass**
-
-- [ ] **Step 5: Full extractor test pass**
-
-- [ ] **Step 6: Benchmark on airflow (or proxy)**
-
-Time + RSS before vs after. Expected: similar wall time (already bounded by CPU); peak RSS materially down because fewer trees live simultaneously.
-
-- [ ] **Step 7: Commit + PR**
-
-```
-perf(enricher): bound LanguageEnricher goroutine pool to 2 * GOMAXPROCS
-
-Previously the enricher spawned one goroutine per source file with no
-cap. On polyglot Python repos (airflow: 7,456 files) that produced
-7k+ concurrent live tree-sitter Trees + file content strings, driving
-the OOM-prone RSS spike. Bounded semaphore preserves determinism
-(results still indexed by task slot) at no measurable wall-time cost.
-```
-
----
-
-### Task A3: Cap Kuzu BufferPoolSize + CALL threads
-
-**Context.** `internal/graph/store.go` opens Kuzu with `kuzu.DefaultSystemConfig()` which allocates 80% of system RAM as the buffer pool. On a 15 GiB host that's ~12 GiB reserved by Kuzu before any Go enrich work starts. Plus: COPY FROM parallelism on string-keyed node tables is the worst case (issue #4937 — primary key hash index is not buffer-pool-tracked, cannot spill).
-
-**Fix.**
-1. Expose a `--max-buffer-pool` flag (and config field) that defaults to `min(2 GiB, 25% of system RAM)`. Pass it via `SystemConfig.BufferPoolSize` when opening Kuzu.
-2. Before the first `BulkLoadNodes` COPY, issue `CALL threads = N` where N defaults to `min(4, GOMAXPROCS)`. Lowers Kuzu's COPY parallelism, capping its working set proportionally.
-
-**Files:**
-- Modify: `go/internal/graph/store.go` — change `Open()` and `OpenReadOnly()` to accept a `StoreOptions` struct (or extend the existing one) with `BufferPoolBytes int64`. Default if unset: 2 GiB.
-- Modify: `go/internal/cli/enrich.go` (or root.go) — add a `--max-buffer-pool` flag. Wire through to store.Open.
-- Modify: `go/internal/graph/bulk.go` — before the first COPY, issue `CALL threads = ?` if configured.
-- Modify: `codeiq.yml` example + `internal/config/*.go` — surface the option.
-
-- [ ] **Step 1: Add BufferPoolBytes to store options**
-
-```go
-type StoreOptions struct {
- Path string
- BufferPoolBytes int64 // 0 = use default 2 GiB
- CopyThreads int // 0 = use default min(4, GOMAXPROCS)
-}
-```
-
-- [ ] **Step 2: Apply in graph.Open()**
-
-```go
-cfg := kuzu.DefaultSystemConfig()
-if opts.BufferPoolBytes > 0 {
- cfg.BufferPoolSize = uint64(opts.BufferPoolBytes)
-} else {
- cfg.BufferPoolSize = 2 << 30 // 2 GiB
-}
-```
-
-- [ ] **Step 3: Apply CALL threads in BulkLoadNodes** (`bulk.go`)
-
-Before the first COPY:
-
-```go
-if s.copyThreads > 0 {
- if _, err := s.Cypher(fmt.Sprintf("CALL threads = %d", s.copyThreads)); err != nil {
- return fmt.Errorf("graph: set copy threads: %w", err)
- }
-}
-```
-
-- [ ] **Step 4: Wire CLI flag**
-
-In `enrich.go` cobra command:
-
-```go
-cmd.Flags().Int64Var(&maxBufferPool, "max-buffer-pool", 0, "Max Kuzu buffer pool in bytes (default: 2 GiB).")
-cmd.Flags().IntVar(©Threads, "copy-threads", 0, "Threads for Kuzu COPY FROM (default: min(4, GOMAXPROCS)).")
-```
-
-- [ ] **Step 5: Test**
-
-```
-cd go && CGO_ENABLED=1 go test ./internal/graph/... ./internal/cli/... -count=1
-```
-
-- [ ] **Step 6: Smoke with explicit cap**
-
-```
-/tmp/codeiq enrich /tmp/bench-fixture --max-buffer-pool=$((512*1024*1024)) --copy-threads=2
-```
-
-Expected: stats output identical to default.
-
-- [ ] **Step 7: Commit + PR**
-
-```
-perf(graph): cap Kuzu BufferPoolSize and COPY threads
-
-kuzu.DefaultSystemConfig() allocates 80% of system RAM as buffer pool
-(~12 GiB on a 15 GiB host) before any enrich work runs, leaving
-insufficient headroom for Go-side enrichment. Cap at 2 GiB by default;
-expose --max-buffer-pool and --copy-threads CLI flags for tuning.
-```
-
----
-
-### Task A4: Free GraphBuilder maps after Snapshot
-
-**Context.** `enrich.go:68` calls `snap := builder.Snapshot()` but never releases `builder`. The two dedup maps (`builder.nodes`, `builder.edges`) hold ~280 MB of references to the same `*CodeNode` / `*CodeEdge` objects that the Snapshot slices now hold. They coexist for the entire pipeline lifespan.
-
-**Fix.** Set `builder = nil` immediately after Snapshot returns. Optionally, modify `GraphBuilder.Snapshot()` to clear its internal maps so the builder is reusable but doesn't retain references.
-
-**Files:**
-- Modify: `go/internal/analyzer/graph_builder.go` — `Snapshot()` clears `b.nodes = nil; b.edges = nil` at the end (after copying out into slices).
-- Modify: `go/internal/analyzer/graph_builder_test.go` — add a determinism test that exercises Snapshot twice; document that the second call returns an empty snapshot now (or change the semantics to error on reuse).
-
-Decision point: do we want `Snapshot` to be idempotent (return same snapshot on repeated calls) or single-shot (clear after extraction)? Single-shot is simpler. Tests should confirm the new semantics.
-
-- [ ] **Step 1: Change Snapshot to clear**
-
-```go
-func (b *GraphBuilder) Snapshot() *Snapshot {
- snap := &Snapshot{
- Nodes: sortedNodesByID(b.nodes),
- Edges: sortedEdgesByID(b.edges),
- Stats: b.Stats(),
- }
- // Release dedup maps so the holders can be GC'd before the
- // downstream enricher pipeline runs.
- b.nodes = nil
- b.edges = nil
- return snap
-}
-```
-
-- [ ] **Step 2: Run GraphBuilder tests**
-
-`cd go && CGO_ENABLED=1 go test ./internal/analyzer/... -count=1`
-
-Add a test:
-
-```go
-func TestSnapshotReleasesMaps(t *testing.T) {
- b := NewGraphBuilder()
- b.Add(&detector.Result{Nodes: []*model.CodeNode{{ID: "x"}}})
- _ = b.Snapshot()
- if b.nodes != nil || b.edges != nil {
- t.Fatal("Snapshot must nil maps to allow GC")
- }
-}
-```
-
-- [ ] **Step 3: Commit + PR**
-
-```
-perf(graph_builder): release dedup maps after Snapshot
-
-GraphBuilder.Snapshot extracts deduped nodes/edges into sorted slices
-but the internal map[string]*CodeNode / map[edgeKey]*CodeEdge held
-references to the same objects, doubling peak retained memory across
-the enrich pipeline (~280 MB on ~/projects scale).
-
-Clear the maps inside Snapshot so the next allocation can collect
-them. Snapshot is now single-shot; documented in code.
-```
-
----
-
-## Phase A success criterion
-
-After A1-A4 merged, re-run the airflow enrich:
-
-```
-/usr/bin/time -v codeiq enrich ~/projects/polyglot-bench/airflow
-```
-
-Expected: peak RSS drops from 3.8 GB to ~600-800 MB. ~/projects-scale extrapolation drops from 9-15 GB to ~2-4 GB. If this is enough to clear ~/projects without OOM, we can ship Phases B + C as polish; if not, they're load-bearing.
-
----
-
-## Phase B — TreeCursor migration
-
-### Task B1: Rewrite parser.Walk to use TreeCursor
-
-**Context.** `parser/walk.go:22-32` does `n.Child(i)` recursion. Each `Child()` call routes through `(*Tree).cachedNode` which heap-allocates a `*Node` on first visit and caches it. With ~12 nodes/file and 7k+ files, that's ~84k+ live `*Node` allocations per parse. pprof: 91% of all allocations in the airflow run.
-
-`tree-sitter`'s `TreeCursor` (`bindings.go:602`) traverses without per-node `*Node` allocation. The cursor itself is the only allocation; it reuses a single internal `Node` view across traversal.
-
-**Fix.** Rewrite `parser.Walk` to use TreeCursor. Public function signature stays identical; callers don't change.
-
-**Files:**
-- Modify: `go/internal/parser/walk.go` — rewrite `Walk(node *sitter.Node, fn func(*sitter.Node) bool)`.
-- Verify: every caller in `internal/intelligence/extractor/*` still works.
-
-- [ ] **Step 1: Read tree-sitter cursor API in `~/go/pkg/mod/github.com/smacker/go-tree-sitter@*/bindings.go`**
-
-Methods: `NewTreeCursor(node) *TreeCursor`, `GotoFirstChild() bool`, `GotoNextSibling() bool`, `GotoParent() bool`, `CurrentNode() *Node`, `CurrentFieldName() string`, `Close()`.
-
-- [ ] **Step 2: Rewrite Walk**
-
-```go
-// Walk visits every descendant of `root` in pre-order DFS using a
-// TreeCursor — no per-node *Node heap allocation. Visitor returns
-// false to skip descending into a node's children.
-func Walk(root *sitter.Node, fn func(*sitter.Node) bool) {
- if root == nil { return }
- cur := sitter.NewTreeCursor(root)
- defer cur.Close()
- descend := fn(cur.CurrentNode())
- for {
- if descend && cur.GoToFirstChild() {
- descend = fn(cur.CurrentNode())
- continue
- }
- for {
- if cur.GoToNextSibling() {
- descend = fn(cur.CurrentNode())
- break
- }
- if !cur.GoToParent() { return }
- }
- }
-}
-```
-
-- [ ] **Step 3: Run parser tests + every extractor test**
-
-```
-cd go && CGO_ENABLED=1 go test ./internal/parser/... ./internal/intelligence/extractor/... -count=1
-```
-
-Tests must pass without modification — the public Walk API is unchanged.
-
-- [ ] **Step 4: Determinism check**
-
-Run the analyzer twice on fixture-minimal; assert byte-identical Kuzu output.
-
-- [ ] **Step 5: Benchmark**
-
-Allocations should drop 90%+ on airflow.
-
-- [ ] **Step 6: Commit + PR**
-
-```
-perf(parser): use TreeCursor instead of recursive Node.Child traversal
-
-(*Tree).cachedNode was responsible for 91% of total allocations
-during enrich on a polyglot Python repo (airflow run, pprof
-alloc_space top). Each Node.Child(i) call heap-allocated a *Node and
-cached it on the Tree. TreeCursor traverses the same tree without
-per-node *Node allocation.
-
-Public parser.Walk signature unchanged; callers are unmodified.
-```
-
----
-
-## Phase C — Streaming three-pass enrich
-
-This is the architectural fix. After it lands, the enrich pipeline never materialises the full graph in Go memory. Memory is bounded by configurable batch size + a compact ID/metadata index, both well under 100 MB at ~/projects scale and gracefully scalable to 10M+ nodes.
-
-### Task C1: Define the streaming interfaces
-
-**Context.** Today the enrich pipeline passes `nodes []*model.CodeNode` and `edges []*model.CodeEdge` slices between stages. To stream, we need an iterator/channel-based contract.
-
-**Files:**
-- Create: `go/internal/analyzer/stream.go` — defines `NodeStream`, `EdgeStream`, `NodeIndex` types.
-
-```go
-// NodeStream emits nodes in the order they were ingested from the
-// SQLite cache; deduplicated by ID via a Set held by the
-// stream's source. Implementations must be safe for one consumer.
-type NodeStream interface {
- Next() (*model.CodeNode, error) // io.EOF when exhausted
- Close() error
-}
-
-// EdgeStream — symmetric, for edges.
-type EdgeStream interface {
- Next() (*model.CodeEdge, error)
- Close() error
-}
-
-// NodeIndex is a compact in-memory index of every node's id + the
-// small set of fields cross-stage enrichers actually need: Kind,
-// Label, FQN, FilePath, Module. Properties and Annotations are
-// omitted. Memory cost at 434k nodes: ~35 MB.
-type NodeIndex interface {
- Lookup(id string) (CompactNode, bool)
- LookupByFQN(fqn string) (CompactNode, bool)
- Len() int
- Range(fn func(CompactNode) bool) // visit in stable order
-}
-
-type CompactNode struct {
- ID string
- Kind model.NodeKind
- Label string
- FQN string
- FilePath string
- Module string
- Layer model.Layer
-}
-```
-
-- [ ] **Step 1: Write the type definitions**
-
-- [ ] **Step 2: Add unit tests for the basic shape**
-
-Mock implementations + a smoke test that round-trips a small slice through a `sliceNodeStream`.
-
-- [ ] **Step 3: Commit (no PR yet — this is groundwork for C2)**
-
-This task lands together with C2 in a single PR for review coherence.
-
-### Task C2: Implement Pass 1 — Index build from cache
-
-**Goal.** Stream the SQLite cache once, building a `NodeIndex` (compact) and computing per-node Layer (since LayerClassifier is stateless and can run during the index pass). Memory budget: ~35 MB for the index + one cache row at a time.
-
-**Files:**
-- Modify: `go/internal/analyzer/enrich.go` — split `Enrich()` into `Enrich(opts)` orchestrating three passes.
-- Create: `go/internal/analyzer/pass1_index.go` — `BuildIndex(c *cache.Cache, root string) (NodeIndex, error)`.
-
-Pass 1 logic:
-1. Iterate cache entries via `c.IterateAll` — one entry at a time.
-2. For each cached node: compute Layer (call LayerClassifier inline), build `CompactNode`, insert into the index.
-3. Detect duplicates by ID via the index's internal map (acts as the ID set).
-4. Return the populated index. The cache stays on disk; the full node payloads are NOT held in memory.
-
-- [ ] **Step 1: Implement BuildIndex** with TDD
-
-Test on fixture-minimal: builds an index with the expected node count + compact-field contents.
-
-- [ ] **Step 2: Wire it into enrich.go**
-
-Replace `builder := NewGraphBuilder(); ...; snap := builder.Snapshot()` with `index, err := BuildIndex(c, root)`. The old `nodes, edges` locals are gone.
-
-- [ ] **Step 3: Run all enrich tests — they will fail**
-
-That's expected. The next tasks rewire each stage.
-
-### Task C3: Pass 2 — Linkers against the compact index
-
-**Goal.** Reformat the three linkers to operate on `NodeIndex` and emit new nodes/edges to a channel/buffer instead of mutating slices.
-
-**Files:**
-- Modify: `go/internal/analyzer/linker/topic_linker.go` — `Link(idx NodeIndex, emit func(detector.Result))`.
-- Modify: `go/internal/analyzer/linker/entity_linker.go` — same shape.
-- Modify: `go/internal/analyzer/linker/module_containment_linker.go` — same.
-
-Each linker internally:
-- Iterates `idx.Range(...)` for whatever cross-referencing it does.
-- Emits new `CodeNode`/`CodeEdge` records by calling `emit(...)`.
-- Holds only its own small internal state (e.g. `byModule map[string][]CompactNode`).
-
-- [ ] **Step 1-3 per linker** — TDD-style, write the new signature, port the body, update tests.
-
-- [ ] **Step 4: Smoke against fixture-multi-lang** — linker output diff vs baseline must be zero.
-
-### Task C4: Pass 3 — Streaming load with bounded-batch BulkLoad
-
-**Goal.** Stream the SQLite cache a SECOND time. For each batch of `batchSize` nodes (default 5000):
-1. Read the full payload from cache.
-2. Apply LexicalEnricher (per-file, releases file content after each).
-3. Apply LanguageEnricher (with parse-once-per-file from A1 + bounded pool from A2).
-4. Append linker output appropriate to nodes in this batch.
-5. Hand off to a write goroutine via a bounded channel; the write goroutine calls `BulkLoadNodes(batch)` (already chunked in PR #143).
-6. Release the batch slice — let GC collect.
-
-Memory budget: `batchSize` nodes (~2.5 MB at 5000 nodes × 500 bytes) + the NodeIndex (~35 MB) + write-channel buffer.
-
-**Files:**
-- Create: `go/internal/analyzer/pass3_load.go` — `StreamLoad(c *cache.Cache, idx NodeIndex, linkerOutputs []detector.Result, store *graph.Store, opts) error`.
-- Modify: `go/internal/intelligence/lexical/enricher.go` — operate on per-batch nodes; don't require full-set.
-- Modify: `go/internal/intelligence/extractor/enricher.go` — operate on per-batch nodes.
-
-The ServiceDetector is tricky because it currently walks the filesystem AND stamps every node. Two options:
-- (a) Run it in Pass 1 (before the index is finalised) so it appears in the index and downstream tasks see service mappings.
-- (b) Run it in Pass 3 batch by batch using the index for cross-references.
-
-Recommendation: (a). ServiceDetector's filesystem walk is fast (it doesn't iterate nodes — it walks for build files) and the per-node stamping is fast given the index is in memory.
-
-- [ ] **Step 1: ServiceDetector refactor to run in Pass 1**
-
-- [ ] **Step 2: LexicalEnricher per-batch refactor**
-
-- [ ] **Step 3: LanguageEnricher per-batch refactor** (depends on A1's `ExtractFromTree`)
-
-- [ ] **Step 4: Implement StreamLoad** with bounded write channel
-
-- [ ] **Step 5: Determinism test** — run enrich twice on fixture-multi-lang, diff Kuzu output (use the kuzu_dump utility in parity/).
-
-- [ ] **Step 6: Wall-time + memory benchmark on airflow**
-
-### Task C5: Cut over `enrich.go` to the three-pass pipeline
-
-Replace the old `Enrich()` body with:
-
-```go
-func Enrich(root string, c *cache.Cache, opts EnrichOptions) (EnrichSummary, error) {
- // Pass 1: build compact index + run ServiceDetector + LayerClassifier
- index, services, err := BuildIndex(c, root)
- if err != nil { return EnrichSummary{}, fmt.Errorf("pass1: %w", err) }
-
- // Pass 2: linkers emit new edges/nodes against the compact index
- linkerOut, err := RunLinkers(index)
- if err != nil { return EnrichSummary{}, fmt.Errorf("pass2: %w", err) }
-
- // Pass 3: stream cache through enrichers in batches, bulk-load to Kuzu
- store, err := graph.Open(opts.GraphDir, opts.StoreOptions)
- if err != nil { return EnrichSummary{}, fmt.Errorf("open graph: %w", err) }
- defer store.Close()
- if err := store.ApplySchema(); err != nil { return EnrichSummary{}, err }
-
- summary, err := StreamLoad(c, index, services, linkerOut, store, opts)
- if err != nil { return EnrichSummary{}, fmt.Errorf("pass3: %w", err) }
-
- if err := store.CreateIndexes(); err != nil { return EnrichSummary{}, err }
- return summary, nil
-}
-```
-
-- [ ] **Step 1: Replace enrich.go body**
-
-- [ ] **Step 2: Full test suite**
-
-`cd go && CGO_ENABLED=1 go test ./... -count=1 -race`
-
-- [ ] **Step 3: Determinism diff against pre-cutover output**
-
-Index + enrich fixture-multi-lang on both branches; diff Kuzu graph contents via `parity/kuzu_dump`. Output must be byte-identical.
-
-- [ ] **Step 4: Commit + PR — single big PR for Phase C**
-
-The streaming refactor lands as one PR because the moving parts (index, linkers, load) are interlocked. Reviewers see the full picture once.
-
-```
-perf(enrich): streaming three-pass pipeline for memory-bounded enrich
-
-Pass 1 (index): stream SQLite cache, build compact NodeIndex (~35 MB
-at 434k-node scale; drops Properties + Annotations), run
-ServiceDetector + LayerClassifier in-pass.
-
-Pass 2 (linkers): TopicLinker, EntityLinker, ModuleContainmentLinker
-operate on NodeIndex; emit new nodes/edges to a buffer.
-
-Pass 3 (load): stream cache a second time in batches of 5000 nodes.
-LexicalEnricher + LanguageEnricher apply per batch. Each batch is
-BulkLoaded to Kuzu and released before the next starts.
-
-Memory profile: NodeIndex (35 MB) + one batch (2.5 MB) + linker
-output buffer + Kuzu buffer pool cap (2 GiB from task A3). Total
-peak ~2.1 GiB regardless of input size.
-
-Determinism: Kuzu output byte-identical to pre-cutover on
-fixture-multi-lang (verified via parity/kuzu_dump).
-```
-
----
-
-## Phase D — Verification harness
-
-### Task D1: Memory regression test in CI
-
-**Goal.** Lock in the gain. Add a CI check that runs `enrich` on a representative target and asserts peak RSS < threshold.
-
-**Files:**
-- Create: `go/internal/analyzer/bench/memory_test.go` (build tag `bench`).
-- Modify: `.github/workflows/perf-gate.yml` — invoke the bench, parse output, fail if RSS > 1 GiB on the test fixture.
-
-The test:
-1. Builds the binary
-2. Runs `index` then `enrich` on a fixture
-3. Captures peak RSS via `/usr/bin/time -v` parsing OR `golang.org/x/sys/unix.Rusage`
-4. Asserts peak < threshold
-
-- [ ] **Step 1: Pick a fixture** — fixture-multi-lang is too small (peak ~50 MB). We need something Python-heavy to exercise the parse-storm path. Add a `fixture-python-heavy/` to testdata: ~500 synthetic Python files generated programmatically. ~5 MB on disk, ~10k AST nodes total.
-
-- [ ] **Step 2: Memory bench harness**
-
-- [ ] **Step 3: CI integration**
-
-Add to perf-gate.yml; threshold: peak RSS < 200 MB on the new fixture. (Tunable; pick after baselining.)
-
-### Task D2: Real-world acceptance run
-
-**Goal.** End-to-end confirmation on ~/projects.
-
-- [ ] **Step 1: Index + enrich + stats on ~/projects/polyglot-bench (~22k files)**
-
-```
-codeiq index ~/projects/polyglot-bench
-codeiq enrich ~/projects/polyglot-bench
-codeiq stats ~/projects/polyglot-bench
-```
-
-Capture peak RSS via `/usr/bin/time -v`. Target: < 2.5 GB.
-
-- [ ] **Step 2: Index + enrich on ~/projects (49k files)**
-
-Same dance. Target: < 4 GB peak. No exit 137. Stats output usable.
-
-- [ ] **Step 3: Document the result**
-
-Add a section to PROJECT_SUMMARY.md or CLAUDE.md noting the new scale-tested target.
-
----
-
-## Out of scope
-
-- **Duplicate-PK service IDs** (`service:checkbox`, `service:src`) — distinct bug. `service_detector.go:168` builds ID as `"service:" + name`; needs path-qualification. Separate PR.
-- **CSV escape bug in BulkLoadEdges** — JSON properties with commas break Kuzu COPY FROM. Fix candidate: switch the delimiter to `\x1F` (unit separator) or pre-escape. Separate PR.
-- **Kuzu version upgrade** — v0.7.1 → v0.11.x. Worth doing for unrelated reasons but doesn't fix this OOM.
-- **Distributed enrich** — splitting enrich across processes. Premature; revisit at 100M-node scale.
-
----
-
-## Risk register
-
-| Risk | Mitigation |
-|---|---|
-| Determinism drift between pre/post streaming refactor | After every Phase C task, run `parity/kuzu_dump` diff against baseline on fixture-multi-lang. Block the merge if any diff. |
-| TreeCursor semantics differ subtly from recursive Walk (e.g. order of visitation) | Phase B test pass against every extractor must show identical edge emission. Add a property-based test that compares Walk-old vs Walk-new on synthetic ASTs. |
-| Phase A3 cap of 2 GiB buffer pool starves Kuzu on large reads downstream | The cap is configurable (`--max-buffer-pool`). Default sized for typical workstation; users with more RAM raise the cap. |
-| ServiceDetector run in Pass 1 emits before all nodes seen | ServiceDetector's filesystem walk is independent of node iteration; the per-node stamping happens *after* the walk completes and the full module map is built. Verify with an audit during Step C4.1. |
-| Phase C is one big PR; review burden high | Split into stacked PRs: C1+C2 (index), C3 (linkers), C4 (load), C5 (cutover). Each is independently reviewable. |
-
----
-
-## Verification checklist (run before declaring the OOM bar met)
-
-- [ ] All Phase A PRs merged. `go test ./... -count=1` green on main.
-- [ ] Phase B PR merged. parser.Walk uses TreeCursor; determinism diff zero on fixture-multi-lang.
-- [ ] Phase C PRs merged. enrich.go is the three-pass orchestrator. `go test ./... -count=1 -race` green.
-- [ ] Phase D bench test added to CI; passes on every PR.
-- [ ] **Real-world acceptance**: `codeiq enrich ~/projects/` completes successfully with peak RSS < 4 GiB. No exit 137. Stats output usable.
-- [ ] CLAUDE.md / PROJECT_SUMMARY.md updated noting the new memory profile + scale ceiling.
-- [ ] Kuzu graph output byte-identical (or documented-different) between pre-refactor and post-refactor on fixture-multi-lang.
diff --git a/shared/runbooks/aks-oom-quick-fix.md b/shared/runbooks/aks-oom-quick-fix.md
deleted file mode 100644
index 920a2ce6..00000000
--- a/shared/runbooks/aks-oom-quick-fix.md
+++ /dev/null
@@ -1,162 +0,0 @@
-# Runbook: AKS OOM quick fix for `codeiq serve`
-
-> **Audience:** ops engineers seeing `codeiq serve` pods crash, restart-loop, or feel sluggish on AKS (or any Kubernetes cluster) at the typical ~200 K-node graph scale.
->
-> **Symptom:** pod is `OOMKilled`, or `kubectl top pod` shows steady-state RSS climbing toward the cgroup limit, or readiness probe flaps under load.
->
-> **Companion:** [`aks-read-only-deploy.md`](aks-read-only-deploy.md) covers the read-only-rootfs deploy shape; this runbook covers the memory tuning that should pair with it.
-
-## TL;DR
-
-```yaml
-resources:
- requests: { memory: "3Gi", cpu: "500m" }
- limits: { memory: "4Gi", cpu: "2" }
-env:
- - name: JAVA_TOOL_OPTIONS
- value: >-
- -XX:MaxRAMPercentage=50
- -XX:InitialRAMPercentage=25
- -XX:+UseG1GC
- -XX:+ExitOnOutOfMemoryError
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=/tmp/codeiq-oom.hprof
-readinessProbe:
- httpGet: { path: /actuator/health/readiness, port: 8080 }
- initialDelaySeconds: 60
- periodSeconds: 30
- timeoutSeconds: 10
- failureThreshold: 3
-livenessProbe:
- httpGet: { path: /actuator/health/liveness, port: 8080 }
- initialDelaySeconds: 90
- periodSeconds: 30
- failureThreshold: 6
-```
-
-If you only do one thing, **set `MaxRAMPercentage=50` and `limits.memory: 4Gi`** — that alone resolves most OOMKilled crashes on the current architecture.
-
-## 1. Why a graph this small OOMs
-
-On a typical workload (~200 K nodes, ~320 K edges) the raw graph is ~150–200 MiB. The pod still OOMs because three independent memory-consumers fight for the same cgroup limit:
-
-| Consumer | Default behaviour (untuned) | After the v0.2.1 quick-win PR |
-|---|---|---|
-| JVM heap | `-XX:MaxRAMPercentage=75` (JDK 25 default in containers) → ~3 GiB on a 4 GiB pod | Capped at 50% via `aks-launch.sh` |
-| Neo4j page cache | Auto-grabs ~50% of *free* RAM at startup (off-heap, additive) | Capped at 256 MiB in `Neo4jConfig.java` |
-| Spring `@Cacheable` regions | `ConcurrentMapCacheManager` — unbounded, no TTL, no eviction | Caffeine `maximumSize=1000, expireAfterWrite=5m` |
-| Topology snapshot | Two independent `AtomicReference>` (one in `McpTools`, one in `TopologyController`) | One shared `TopologySnapshotProvider`, 60 s TTL |
-
-The first two cumulatively exceed `limits.memory` because nothing tells either side it has to share. The next two leak slowly under normal traffic until the heap fills.
-
-## 2. Diagnostic — what's actually broken
-
-Run these inside the cluster before applying the patch. They tell you whether the failure mode is **kernel OOM** (cgroup limit) or **JVM heap thrash** (probes timing out under GC pauses) — different fixes apply.
-
-```bash
-NS=
-POD=$(kubectl -n $NS get pod -l app=codeiq -o jsonpath='{.items[0].metadata.name}')
-
-# 1. Are pods being kernel-OOM-killed?
-kubectl -n $NS get events --sort-by='.lastTimestamp' | grep -iE "oom|kill|evict"
-kubectl -n $NS describe pod $POD | grep -A2 "Last State"
-
-# 2. Pod resource limits + actual usage
-kubectl -n $NS get pod $POD -o jsonpath='{.spec.containers[0].resources}'; echo
-kubectl -n $NS top pod $POD
-
-# 3. JVM-effective heap settings + current heap
-kubectl -n $NS exec $POD -- jcmd 1 VM.flags | tr ' ' '\n' | grep -E "MaxHeapSize|MaxRAMPercentage"
-kubectl -n $NS exec $POD -- jcmd 1 GC.heap_info
-```
-
-### Decision tree
-
-- **`Last State: Terminated Reason: OOMKilled`** → pod hit the cgroup limit. Apply the full TL;DR patch above.
-- **No `OOMKilled`, but readiness flaps (`Reason: Unhealthy` events for `/actuator/health/readiness`)** → JVM is in GC thrash. The Caffeine + topology-snapshot fixes in v0.2.1 + bumping `failureThreshold: 6` resolve this without changing pod size.
-- **Steady-state RSS keeps climbing for hours** → unbounded Spring cache. Confirm the pod image includes the Caffeine fix (v0.2.1+) by checking `kubectl exec $POD -- jcmd 1 VM.classloader_stats | grep -i caffeine`.
-
-## 3. Apply the Deployment patch
-
-```yaml
-# Deployment.spec.template.spec.containers[0]
-resources:
- # request = guaranteed-not-evicted floor; limit = hard cgroup ceiling.
- # 200 K-node graphs comfortably fit in 4 GiB total once the v0.2.1
- # quick-win lands. Bump the limit (not the request) if your store grows
- # past ~500 MB on disk.
- requests:
- memory: "3Gi"
- cpu: "500m"
- limits:
- memory: "4Gi"
- cpu: "2"
-env:
- - name: JAVA_TOOL_OPTIONS
- # JAVA_TOOL_OPTIONS is picked up by every JVM invocation and prepended
- # to argv. Useful here because aks-launch.sh already sets the same
- # flags at exec time — the env var is a belt-and-braces fallback if
- # ops bypass the launch wrapper (e.g. kubectl exec'ing into the pod).
- value: >-
- -XX:MaxRAMPercentage=50
- -XX:InitialRAMPercentage=25
- -XX:+UseG1GC
- -XX:+ExitOnOutOfMemoryError
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=/tmp/codeiq-oom.hprof
-readinessProbe:
- # Spring + Neo4j cold start is 10–16s. initialDelaySeconds of 60 gives
- # Spring's lazy beans + the first Neo4j page-cache page-in headroom
- # before the first probe failure can mark the pod NotReady.
- httpGet: { path: /actuator/health/readiness, port: 8080 }
- initialDelaySeconds: 60
- periodSeconds: 30
- timeoutSeconds: 10
- failureThreshold: 3
-livenessProbe:
- # failureThreshold: 6 over periodSeconds: 30 = 3 minutes of tolerated
- # unresponsiveness before SIGKILL. Critical because GraphHealthIndicator
- # runs against Neo4j and a flushing page cache can stall it briefly
- # under burst traffic. Liveness must never flap on transient slowness;
- # only on actual JVM-dead.
- httpGet: { path: /actuator/health/liveness, port: 8080 }
- initialDelaySeconds: 90
- periodSeconds: 30
- failureThreshold: 6
-```
-
-After `kubectl apply`, watch the rollout:
-
-```bash
-kubectl -n $NS rollout status deployment/codeiq --timeout=5m
-kubectl -n $NS top pod -l app=codeiq # RSS should land near the requested 3Gi, not the 4Gi limit
-kubectl -n $NS logs -l app=codeiq --tail=200 | grep -iE "oom|outofmemory|gc"
-```
-
-## 4. What this does NOT fix
-
-- **5 M+ node graphs.** At that scale the topology snapshot is multi-GB regardless of TTL. The bounded-Cypher refactor is needed (tracked as the topology-deep-refactor follow-up).
-- **runCypher misuse.** Operators or LLM agents can still run unbounded ad-hoc Cypher and OOM the pod. Limit the `runCypher` MCP tool's `maxResults` via `codeiq.yml` if you expose it externally.
-- **Heap dump capture under cgroup pressure.** `HeapDumpPath=/tmp` is fine on tmpfs-backed `/tmp` (the read-only deploy uses `emptyDir: { medium: Memory }`), but if `/tmp` is also at the limit when the OOM fires, the dump won't write. For long-term diagnosis attach an `emptyDir` volume sized at `1.5 × heap` and point `HeapDumpPath` at it.
-
-## 5. Horizontal scaling
-
-The image-bundled read-only graph means each pod is fully stateless — `replicas: N` is safe. The only per-pod state worth knowing about is the in-process rate-limit `ConcurrentHashMap` in `RateLimitFilter`; token buckets reset per-replica, which is correct for per-key throttling but means a global rate limit across replicas isn't enforced. Most workloads don't need that.
-
-```yaml
-spec:
- replicas: 3
- strategy:
- type: RollingUpdate
- rollingUpdate:
- maxUnavailable: 0
- maxSurge: 1
-```
-
-A 3-replica deploy at `4Gi × 3 = 12Gi` total cluster cost gives ~3× the request capacity of a single 8Gi pod with zero crash risk.
-
-## 6. Cross-references
-
-- Code changes that landed alongside this runbook: `config/Neo4jConfig.java` (page-cache cap), `query/TopologySnapshotProvider.java` (shared snapshot), `application.yml` (Caffeine cache type), `scripts/aks-launch.sh` (JVM flag preset).
-- Related runbook: [`aks-read-only-deploy.md`](aks-read-only-deploy.md) — the deploy shape this OOM patch sits inside.
-- Architecture rationale: [`shared/runbooks/engineering-standards.md`](engineering-standards.md) §4 (resource sizing) and the OOM review thread in the project history.
diff --git a/shared/runbooks/aks-read-only-deploy.md b/shared/runbooks/aks-read-only-deploy.md
deleted file mode 100644
index b113e734..00000000
--- a/shared/runbooks/aks-read-only-deploy.md
+++ /dev/null
@@ -1,225 +0,0 @@
-# Runbook: AKS read-only deploy
-
-> **Audience:** ops engineers deploying `codeiq serve` to an AKS cluster (or any Kubernetes cluster with `securityContext.readOnlyRootFilesystem: true`).
->
-> **Spec:** [`docs/specs/2026-04-28-aks-read-only-deploy-design.md`](../../docs/specs/2026-04-28-aks-read-only-deploy-design.md). Full architecture rationale lives there; this runbook is the operational checklist.
-
-## 1. Overview
-
-`codeiq serve` runs inside an AKS pod with the root filesystem mounted read-only and `/tmp` mounted writable. The graph bundle is built in CI (`index → enrich → bundle`), uploaded to Nexus, then pulled at deploy time by an init-container into `/tmp/codeiq-data`. The main container runs the launch wrapper at `scripts/aks-launch.sh` which composes the JVM flag preset and execs `java -jar code-iq.jar serve /tmp/codeiq-data`.
-
-Three deployment-layer pieces enable this with **zero source-code changes** to the serve profile:
-
-1. The graph bundle physically lives under `/tmp/codeiq-data` so embedded Neo4j has a writable directory for its `store_lock`, transaction logs, and counts cache.
-2. JVM flags redirect Spring-Boot-loader extraction, crash dumps, and heap dumps to `/tmp`.
-3. The launch wrapper enforces the flag preset in one place.
-
-## 2. Deploy shape
-
-```
-Build CI (any writable agent — GitHub Actions, GitLab, etc.)
- └─ codeiq index $REPO
- └─ codeiq enrich $REPO ──▶ $REPO/.codeiq/graph/graph.db/
- └─ codeiq bundle $REPO ──▶ bundle.zip (graph + manifest)
- └─ curl -u $NEXUS_USER:$NEXUS_PASS \
- --upload-file bundle.zip \
- "$NEXUS_URL/repository/codeiq-bundles/$BUNDLE_VERSION/bundle.zip"
-
-AKS deploy (one Pod per service)
- init-container "fetch-bundle" download from Nexus → /tmp/codeiq-data/
- main container "codeiq-serve" /usr/local/bin/aks-launch.sh /tmp/codeiq-data
- listens on :8080 (configurable)
-```
-
-## 3. Init-container Kubernetes manifest
-
-Drop the snippet below into your Pod spec. The init-container shares an `emptyDir` mount with the main container so the unzipped bundle is visible at the same path in both.
-
-```yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: codeiq
- namespace: codeiq
-spec:
- replicas: 1
- selector: { matchLabels: { app: codeiq } }
- template:
- metadata: { labels: { app: codeiq } }
- spec:
- securityContext:
- runAsNonRoot: true
- runAsUser: 65532
- fsGroup: 65532
- volumes:
- - name: tmp
- emptyDir:
- medium: Memory # tmpfs — fastest; switch to "" for disk-backed
- sizeLimit: 4Gi # tune to bundle size + Neo4j tx-log headroom
- initContainers:
- - name: fetch-bundle
- image: alpine:3.20
- command: [sh, -c]
- args:
- - |
- set -euo pipefail
- apk add --no-cache curl unzip > /dev/null
- curl --fail --silent --show-error \
- -u "$NEXUS_USER:$NEXUS_PASS" \
- "$NEXUS_URL/repository/codeiq-bundles/$BUNDLE_VERSION/bundle.zip" \
- -o /tmp/bundle.zip
- mkdir -p /tmp/codeiq-data
- unzip -q /tmp/bundle.zip -d /tmp/codeiq-data
- rm -f /tmp/bundle.zip
- env:
- - name: NEXUS_URL
- valueFrom: { secretKeyRef: { name: codeiq-nexus, key: url } }
- - name: NEXUS_USER
- valueFrom: { secretKeyRef: { name: codeiq-nexus, key: user } }
- - name: NEXUS_PASS
- valueFrom: { secretKeyRef: { name: codeiq-nexus, key: pass } }
- - name: BUNDLE_VERSION
- value: "0.1.0" # bumped per release
- volumeMounts:
- - name: tmp
- mountPath: /tmp
- securityContext:
- readOnlyRootFilesystem: true
- allowPrivilegeEscalation: false
- capabilities: { drop: [ALL] }
- containers:
- - name: codeiq-serve
- image: ghcr.io/randomcodespace/codeiq:0.1.0
- command: [/usr/local/bin/aks-launch.sh, /tmp/codeiq-data]
- ports:
- - { name: http, containerPort: 8080 }
- readinessProbe:
- httpGet: { path: /actuator/health/readiness, port: http }
- initialDelaySeconds: 20
- periodSeconds: 5
- livenessProbe:
- httpGet: { path: /actuator/health/liveness, port: http }
- initialDelaySeconds: 60
- periodSeconds: 10
- resources:
- requests: { cpu: 500m, memory: 1Gi }
- limits: { cpu: 2, memory: 4Gi }
- volumeMounts:
- - name: tmp
- mountPath: /tmp
- securityContext:
- readOnlyRootFilesystem: true # enforces the model
- allowPrivilegeEscalation: false
- runAsNonRoot: true
- capabilities: { drop: [ALL] }
- seccompProfile: { type: RuntimeDefault }
-```
-
-**Volume sizing:** the `emptyDir.sizeLimit: 4Gi` covers a typical mid-size repo's graph + Neo4j transaction-log headroom + Spring Boot loader extraction (~50 MB) + JVM heap-dump headroom. Bump for very large bundles. The pre-flight check in `aks-launch.sh` aborts startup if `/tmp` has < 1 GB free, which is the absolute floor.
-
-**Image:** the container image must install the launch script at `/usr/local/bin/aks-launch.sh` and the JAR at `/app/code-iq.jar` (or set `CODEIQ_JAR=...`). Reference Dockerfile:
-
-```dockerfile
-FROM eclipse-temurin:25-jre-alpine
-RUN apk add --no-cache bash
-WORKDIR /app
-COPY code-iq-*-cli.jar /app/code-iq.jar
-COPY scripts/aks-launch.sh /usr/local/bin/aks-launch.sh
-RUN chmod +x /usr/local/bin/aks-launch.sh
-USER 65532:65532
-ENTRYPOINT ["/usr/local/bin/aks-launch.sh"]
-```
-
-## 4. JVM flag preset (canonical reference)
-
-Encoded in `scripts/aks-launch.sh`. Updating the preset means updating the script (and the sentinel test catches the drift). Every flag has a non-default behavior that without it would write outside `/tmp`.
-
-| Flag | Default | Why required |
-|---|---|---|
-| `-Dorg.springframework.boot.loader.tmpDir=/tmp/spring-boot-loader` | `~/.m2/spring-boot-loader-tmp` | Spring Boot fat JAR extracts nested JARs to `$HOME` by default — outside `/tmp`. |
-| `-Djava.io.tmpdir=/tmp` | OS-default (`/tmp` on Linux) | Explicit so multipart uploads, JNA / Netty native lib extraction land where we expect across base images. |
-| `-XX:ErrorFile=/tmp/hs_err_pid%p.log` | cwd | JVM crash dump default is the working directory. |
-| `-XX:HeapDumpPath=/tmp` | cwd | Heap dump on OOM default is cwd. |
-| `-XX:+HeapDumpOnOutOfMemoryError` | off | Without this the path flag never fires. |
-
-## 5. Verification
-
-### 5.1 Local docker smoke (the gate)
-
-This is the **single source of truth** for "did the deploy assumption actually hold." JVM-level write detection inside JUnit is environment-fragile; running the actual binary inside the actual constraint shape is the only honest test.
-
-```bash
-# Build the image once.
-docker build -t codeiq:smoke .
-
-# Run with --read-only and a tmpfs /tmp, mount a known-good bundle as RO.
-docker run --rm \
- --read-only \
- --tmpfs /tmp:rw,size=2g,mode=1777 \
- -v "$PWD/test-bundle:/mnt/bundle:ro" \
- -p 8080:8080 \
- --entrypoint sh \
- codeiq:smoke \
- -c '
- cp -r /mnt/bundle/. /tmp/codeiq-data &&
- /usr/local/bin/aks-launch.sh /tmp/codeiq-data
- '
-
-# In another terminal:
-curl -fsS http://localhost:8080/api/stats > /tmp/stats.json
-jq '.graph.nodes' /tmp/stats.json # > 0 confirms the graph loaded
-```
-
-If the container exits non-zero with `Read-only file system` or `Permission denied`, **do not paper over with `--read-only=false`**. Investigate which path the new code is trying to write to, and either fix the code or extend the JVM flag preset.
-
-### 5.2 Sentinel test (drift catcher)
-
-```bash
-mvn test -Dtest=AksLaunchScriptSentinelTest
-```
-
-Asserts every required flag is in `scripts/aks-launch.sh`. CI-gated. Catches accidental flag removal.
-
-### 5.3 In-cluster smoke (post-deploy)
-
-```bash
-kubectl -n codeiq port-forward deploy/codeiq 8080:8080 &
-curl -fsS http://localhost:8080/actuator/health
-curl -fsS http://localhost:8080/api/stats | jq '.graph.nodes'
-```
-
-## 6. Rollback
-
-The deploy artifact is the immutable bundle in Nexus + the immutable container image. Rollback is "redeploy the previous bundle version."
-
-```bash
-# Bundle rollback — re-tag the previous bundle version, redeploy.
-kubectl -n codeiq set env deploy/codeiq \
- --containers='codeiq-serve' BUNDLE_VERSION=0.0.49
-
-# Image rollback (CVE patch / launcher fix).
-kubectl -n codeiq set image deploy/codeiq \
- codeiq-serve=ghcr.io/randomcodespace/codeiq:0.0.49
-```
-
-For full release / rollback policy see [`shared/runbooks/release.md`](release.md) and [`shared/runbooks/rollback.md`](rollback.md). This runbook covers the AKS-specific bits only.
-
-## 7. Troubleshooting
-
-| Symptom | Likely cause | Fix |
-|---|---|---|
-| `Read-only file system` at startup | A new code path is writing outside `/tmp`. | Run the docker smoke (§5.1) — the stack trace points at the path. Either redirect via the JVM flag preset (extend §4) or fix the code. |
-| `lock acquired by another process` from Neo4j | Two pods sharing the same `/tmp` volume — only legal in single-replica mode. | Set `replicas: 1`, or split each replica's `emptyDir` (default — they're per-pod). |
-| `out of disk space` during init-container | `emptyDir.sizeLimit` too small for the bundle. | Bump `sizeLimit` in the manifest. |
-| `BUNDLE_VERSION` not found at Nexus | Stale tag, or release never landed. | Verify the upload step in build CI; check Nexus repository UI. |
-| Pod restart loop after a clean start | Likely a heap dump filling `/tmp` — `--tmpfs` size cap reached. | Bump `sizeLimit`; investigate the OOM root cause via the heap dump pulled out of the previous pod. |
-
-## 8. Cross-references
-
-- Spec: [`docs/specs/2026-04-28-aks-read-only-deploy-design.md`](../../docs/specs/2026-04-28-aks-read-only-deploy-design.md)
-- Plan: [`docs/plans/2026-04-28-sub-project-2-aks-read-only-deploy.md`](../../docs/plans/2026-04-28-sub-project-2-aks-read-only-deploy.md)
-- Engineering standards: [`engineering-standards.md`](engineering-standards.md) §7.1 Deploy targets
-- Release process: [`release.md`](release.md)
-- Rollback: [`rollback.md`](rollback.md)
-- Launch script: [`scripts/aks-launch.sh`](../../scripts/aks-launch.sh)
-- Sentinel test: [`src/test/java/io/github/randomcodespace/iq/deploy/AksLaunchScriptSentinelTest.java`](../../src/test/java/io/github/randomcodespace/iq/deploy/AksLaunchScriptSentinelTest.java)
diff --git a/shared/runbooks/engineering-standards.md b/shared/runbooks/engineering-standards.md
deleted file mode 100644
index 03dbff9e..00000000
--- a/shared/runbooks/engineering-standards.md
+++ /dev/null
@@ -1,171 +0,0 @@
-# Engineering Standards — codeiq
-
-> **SSoT** for the cross-cutting engineering rules that every contributor — human or agent — must follow on this repo. Per-issue specifics live in the issue thread; per-component conventions live in [`/CLAUDE.md`](../../CLAUDE.md). This document is what the runbooks ([`release.md`](release.md), [`rollback.md`](rollback.md), [`first-time-setup.md`](first-time-setup.md)) reference for "what counts as done."
-
-The rule of last resort: **`/home/dev/.claude/rules/*.md` wins.** This file does not contradict it; it specialises it for codeiq.
-
----
-
-## 1. Quality gates (hard / non-negotiable)
-
-| Gate | Threshold | Where it runs | Failure action |
-|---|---|---|---|
-| Unit + integration tests | All pass | `mvn verify` (CI + local) | Block merge |
-| JaCoCo coverage | ≥ 85% line (project-wide, post-exclusions) | `jacoco-maven-plugin` rule in `pom.xml` | Block merge |
-| SpotBugs (Java lint) | Zero High/Critical findings; `spotbugs-exclude.xml` justified per-entry | `mvn spotbugs:check` (bound to `verify`) | Block merge |
-| OSV-Scanner (SCA, npm lockfile) | Zero High/Critical CVEs in npm dependency tree | `.github/workflows/security.yml` | Block merge |
-| Trivy (filesystem + container scan, covers Maven + OS) | Zero High/Critical findings (`severity: HIGH,CRITICAL`, `exit-code: 1`) | `.github/workflows/security.yml` | Block merge |
-| Dependabot (cross-ecosystem) | Surfaces advisories on `pom.xml` + `package-lock.json` | `.github/dependabot.yml` + GitHub Security tab | Surface; auto-PRs gated by separate review |
-| Semgrep (SAST) | Zero ERROR-level findings on `p/security-audit` + `p/owasp-top-ten` + `p/java` | `.github/workflows/security.yml` | Block merge |
-| Gitleaks (secret scan) | Zero findings | `.github/workflows/security.yml` | Block merge |
-| jscpd (duplication) | < 3% on touched code, languages: Java + JS + TS | `.github/workflows/security.yml` | Block merge |
-| SBOM (SPDX + CycloneDX) | Generated and uploaded as build artifact (`anchore/sbom-action`) | `.github/workflows/security.yml` | Surface as artifact; do **not** gate merge |
-| OpenSSF Scorecard | Best-effort; no hard score floor; `Pinned-Dependencies` is a soft target | `scorecard.yml` (push to `main` + weekly) | Surface in security tab; do **not** gate merge |
-| Signed commits | Every commit on `main` must verify | Branch protection + `gh api ... /commits/{sha}/check-runs` | Block merge |
-
-Coverage exclusions are enumerated in `pom.xml` `` config — only generated ANTLR sources, the `application/` Spring Boot main, and pure data records are excluded. Adding to that list requires TechLead sign-off.
-
-**Stack: OSS-CLI only.** Per RAN-46 board ruling (path B): no Sonar, no CodeQL, no NVD-direct tools (OWASP Dependency-Check). The OSS-CLI stack covers SCA (OSV-Scanner against the npm lockfile via OSV.dev = GHSA + ecosystem feeds; Trivy + Dependabot cover Maven and the rest of the filesystem — osv-scanner v2's Maven plugin depends on a `deps.dev` gRPC service that is intermittently unavailable in CI, so SCA on Java is delegated to Trivy), filesystem + container scan (Trivy), SAST (Semgrep), secret detection (Gitleaks), duplication (jscpd, `--min-tokens 200` to filter Java header boilerplate that 97 detector files share by template-method conformance), and SBOM emission (`anchore/sbom-action` SPDX + CycloneDX). Cost: $0 — entire stack is OSS-CLI in GitHub Actions, free for public OSS.
-
----
-
-## 2. Code style
-
-- **Java 25** — virtual threads, records, sealed types, pattern matching are first-class. Do not write defensive `instanceof` chains where pattern matching applies.
-- **No mutable state in detectors.** `@Component` beans are singletons; per-call state lives on method locals.
-- **UTF-8 everywhere** (`StandardCharsets.UTF_8` explicit, never the default).
-- **Determinism is enforced.** No `Set` iteration without `TreeSet`/`stream().sorted()`. No reliance on thread completion order. Every detector must have a determinism test (run twice → identical bytes).
-- **Property-key constants** — extract any string literal that appears in the same file 3+ times.
-- **Exception hygiene** — never catch `Exception` to hide it; always either rethrow with context or log at the right level.
-
-Formatting is handled by editor defaults (no auto-formatter committed). Reviewers will not nit on whitespace; they will block on convention drift.
-
----
-
-## 3. Branch, commit, PR rules
-
-- Branch off `main`. Conventional-commit subjects (`feat:`, `fix:`, `chore:`, `refactor:`, `test:`, `docs:`, `perf:`).
-- One logical change per commit. Squash-merge is the only path into `main`.
-- Every commit ssh-signed (RAN-46 AC #2). Branch protection rejects unsigned commits.
-- PR title is a conventional-commit subject. Body contains:
- - Linked Paperclip issue (e.g., `Closes RAN-42`).
- - "Why" in 1–2 sentences (the diff covers "what").
- - Tests / coverage notes if non-obvious.
- - Rollback note if the change is risky.
-- No force-push to `main` ever. Force-push to feature branches is fine until the PR is open; once it is open, prefer additive commits so review threads stay anchored.
-
----
-
-## 4. Testing tiers
-
-| Tier | What it tests | Where it lives | Speed budget |
-|---|---|---|---|
-| Unit | Pure logic, no I/O | `src/test/java/.../` next to the SUT | < 10 ms each |
-| Integration | Real H2 / real Neo4j (Embedded) / real filesystem | `src/test/java/.../analyzer/`, `.../graph/`, `.../e2e/` | < 5 s each |
-| E2E quality | Full pipeline against a real repo (Spring PetClinic, etc.) | `E2EQualityTest`, ground-truth files under `src/test/resources/e2e/` | Run on demand + nightly |
-
-Ground rules:
-- Test behaviour at the boundary; do not test private internals.
-- Every detector ships with: a positive case, a discriminator-guard negative case, a determinism case.
-- Flaky test = broken test. Fix in the same PR, quarantine with a tracked Paperclip issue, or delete.
-
----
-
-## 5. Security
-
-### 5.1 Tooling stack — OSS-CLI ONLY (board ruling, RAN-46 path B)
-
-| Concern | Tool | Where |
-|---|---|---|
-| SCA (npm) | **OSV-Scanner** against `src/main/frontend/package-lock.json` (OSV.dev / GHSA / ecosystem feeds; **not NVD**) | `.github/workflows/security.yml` |
-| SCA (Maven + OS) + filesystem + container scan | **Trivy** filesystem scan (covers `pom.xml` transitive resolution via Trivy's own DB, plus OS packages and any future container layers); Dependabot also surfaces Maven advisories via the GitHub Security tab | `.github/workflows/security.yml` + `.github/dependabot.yml` |
-| SAST | **Semgrep** (`p/security-audit`, `p/owasp-top-ten`, `p/java`) | `.github/workflows/security.yml` |
-| Secret scan | **Gitleaks** (full git history) | `.github/workflows/security.yml` |
-| Duplication | **jscpd** (Java + JS + TS, threshold < 3%) | `.github/workflows/security.yml` |
-| SBOM | **`anchore/sbom-action`** (SPDX + CycloneDX) | `.github/workflows/security.yml` |
-| Java lint | **SpotBugs** (bound to `mvn verify`) | `pom.xml` |
-
-**Not used (do not re-introduce without an explicit board reversal of the RAN-46 path B ruling):** SonarCloud / SonarQube, CodeQL (default-setup or workflow-driven), OWASP Dependency-Check (or any NVD-direct tool). Rationale: NVD has analysis-backlog and rate-limit reliability problems; OSV / GHSA cover the same ground without those issues. CodeQL is GHAS-paid for non-public repos; we standardise on Semgrep across all repos for consistency.
-
-### 5.2 Code hygiene
-
-- **Inputs** — every public-facing endpoint validates input at the boundary; parameterised queries only; output encoded by default.
-- **Path traversal** — anything that takes a user path goes through the canonical-path check pattern used by `/api/file` (see RAN-8 fix).
-- **Secrets** — never in code, config, or commit history. CI secrets are repo-level; rotation cadence is annual or on suspected exposure.
-- **CVE policy** — High/Critical → block; Medium → fix if a patched version exists, else document non-exploitability with TechLead sign-off; Low → tracked in the next dependency bump cycle.
-- **Vulnerability reporting** — see [`/SECURITY.md`](../../SECURITY.md). Private disclosure only.
-
----
-
-## 6. Performance
-
-- Default to streaming and bounded concurrency. No unbounded queues, buffers, or virtual-thread fan-outs.
-- Every external call has a timeout and a cancellation path.
-- Performance-sensitive paths (`Analyzer`, `GraphStore.bulkSave`, `LayerClassifier`) have a microbenchmark or a regression-detection test before they ship.
-- "Make it correct, then make it fast" — but data-structure and query-shape decisions must be performance-aware up front (see `/home/dev/.claude/rules/performance.md`).
-
----
-
-## 7. Build & distribution
-
-- Vendor what you can; deterministic builds (`mvn -B -ntp clean verify`) are the contract.
-- No public-CDN runtime fetches, no auto-update phone-home, no telemetry default-on.
-- GitHub Actions are pinned by commit SHA in every workflow. Rationale: OpenSSF Scorecard `Pinned-Dependencies` and supply-chain integrity.
-- Container artifacts (when added) build from a minimal/distroless base, are pushed to GHCR with provenance attestations, and are pinned by digest at consumer sites.
-
-### 7.1 Deploy targets (RAN-46 AC #10 ruling — option a)
-
-codeiq's deploy surface is the **Maven Central + GitHub Releases** pipeline. There is intentionally no static-CDN frontend or always-on hosted backend.
-
-Rationale (per @CEO's RAN-46 ruling):
-
-- codeiq ships as a single Java JAR (`code-iq-*-cli.jar`). The React UI is bundled **inside** that JAR and served via Spring Boot's static resource handler — there is no separately deployable frontend.
-- The intended runtime is the developer's own machine (`codeiq index → enrich → serve`). There is no shared SaaS surface that needs a VPS — codeiq's value is scanning *your* code on *your* machine.
-- The literal AC #10 wording ("frontend → static host / CDN; backend → container registry + VPS rollout") was written for a generic product template that does not match codeiq's shape; the deploy surface is the existing release pipeline instead.
-
-The pipeline:
-
-| Cadence | Workflow | Trigger | Artifact destination |
-|---|---|---|---|
-| Beta | `.github/workflows/beta-java.yml` | `workflow_dispatch` (manual) | Sonatype Central beta + GitHub pre-release |
-| GA | `.github/workflows/release-java.yml` | `workflow_dispatch` (manual, with `version` input) | Sonatype Central GA + GitHub Release |
-
-Both workflows are `workflow_dispatch`-only — there is no tag-push trigger and no automatic release on merge. A GA cut: the workflow builds a GPG-signed release commit on a detached HEAD, deploys from that exact tree, then creates and pushes a GPG-signed annotated `vX.Y.Z` tag pointing at the release commit plus a GitHub Release. Tags are an *output* of the GA workflow, not a trigger. See [`release.md`](release.md) §3 for the full sequence.
-
-Hello-world / pipeline proof: `git tag -l 'v0.0.1-beta.*' | wc -l` is non-zero (47+ beta tags as of the AC #10 ruling) and `gh release list` shows the corresponding GitHub pre-releases. AC #10 is satisfied by the existing pipeline; no new deploy scaffold is required.
-
-If the product later needs a hosted demo or container surface, that is a **new RAN-* issue**, not a re-open of RAN-46.
-
-#### 7.1.1 Read-only deploy targets (sub-project 2)
-
-When a downstream consumer wants to run `codeiq serve` inside a hardened container runtime — Kubernetes / AKS / OpenShift with `securityContext.readOnlyRootFilesystem=true`, or any environment where the root filesystem is mounted read-only and only `/tmp` is writable — the canonical pattern is:
-
-1. CI builds the bundle (`index → enrich → bundle`) and uploads the zip to a private artifact registry (e.g. Nexus).
-2. An **init-container** copies the bundle into a writable `/tmp/codeiq-data` (`emptyDir` with `medium: Memory` for tmpfs, or default for disk-backed).
-3. The main container runs [`scripts/aks-launch.sh`](../../scripts/aks-launch.sh) which composes the JVM flag preset (Spring-Boot-loader tmpDir, `java.io.tmpdir`, `-XX:ErrorFile`, `-XX:HeapDumpPath`) and exec's `java -jar code-iq.jar serve /tmp/codeiq-data`.
-
-Zero source-code changes to the serve profile or Neo4j wiring — solved at the deployment layer plus the JVM-flag-preset launcher. Drift caught by `AksLaunchScriptSentinelTest`. Full deploy / verify / rollback steps in [`shared/runbooks/aks-read-only-deploy.md`](aks-read-only-deploy.md). Architecture rationale in [`docs/specs/2026-04-28-aks-read-only-deploy-design.md`](../../docs/specs/2026-04-28-aks-read-only-deploy-design.md).
-
----
-
-## 8. Documentation
-
-- `/CLAUDE.md` is the architecture + conventions SSoT for code-touching changes.
-- `/AGENTS.md` (repo root) is the entry-point for agent collaborators.
-- Every runbook lives under `shared/runbooks/`. Adding a new runbook requires updating this file's References section.
-- ADRs (`docs/adr/NNN-title.md`) for any decision that changes a contract: persistence, public API surface, deployment shape.
-
----
-
-## 9. References
-
-- `/CLAUDE.md` — architecture and conventions.
-- `/SECURITY.md` — disclosure policy.
-- `shared/runbooks/release.md`, `rollback.md`, `first-time-setup.md`.
-- `/home/dev/.claude/rules/*.md` — global engineering rules (parent SSoT).
-- `pom.xml` — quality-gate plugin wiring (`jacoco`, `spotbugs`, `central-publishing`).
-- `.github/workflows/` — CI / release / security automations:
- - `ci-java.yml` — `mvn verify` (tests, JaCoCo 85%, SpotBugs).
- - `security.yml` — OSS-CLI security stack (OSV-Scanner, Trivy, Semgrep, Gitleaks, jscpd, SBOM).
- - `scorecard.yml` — OpenSSF Scorecard (push + weekly cron, non-gating).
- - `beta-java.yml`, `release-java.yml` — Maven Central publishing (manual `workflow_dispatch`).
diff --git a/shared/runbooks/first-time-setup.md b/shared/runbooks/first-time-setup.md
deleted file mode 100644
index 194f49f0..00000000
--- a/shared/runbooks/first-time-setup.md
+++ /dev/null
@@ -1,170 +0,0 @@
-# First-time Setup — codeiq
-
-> Get a fresh contributor (human or agent) from a clean machine to a green local build, signed-commit-ready, in one pass. Pairs with [`release.md`](release.md), [`rollback.md`](rollback.md), and [`engineering-standards.md`](engineering-standards.md).
-
----
-
-## 0. What you'll have at the end
-
-- Repo cloned at `~/projects/codeiq` with the offline build path verified.
-- Java 25 + Maven 3.9 on PATH.
-- A signed-commit configuration (ssh) bound to your `id_ed25519` key.
-- `mvn verify` exits 0 on `main`.
-- `codeiq` CLI runs end-to-end against this repo as a smoke target.
-
-If any step fails, stop and follow the troubleshooting note inline — do not "fix forward" against a partially-set-up machine.
-
----
-
-## 1. System prerequisites
-
-| Tool | Min version | Notes |
-|---|---|---|
-| Java | 25 | Required by `pom.xml` `maven-enforcer-plugin` (`[25,)`). Use Adoptium / Temurin. |
-| Maven | 3.9.x | Newer minor versions are fine; do not use 4.x snapshots. |
-| Git | 2.34+ | Required for commit signing in any of the supported formats (`gpg.format` = `ssh`, `openpgp` / `gpg`, or `x509`). |
-| OpenSSH | 8.0+ | Required only for `gpg.format=ssh` (the default; bundles `ssh-keygen -Y verify`). Skip if you sign with OpenPGP or x509. |
-| GnuPG | 2.2+ | Required only for `gpg.format=openpgp` / `gpg`. Skip if you sign with SSH or x509. |
-| Node.js | 20.x LTS | Only needed for the bundled React UI — `mvn package` shells out to it via the frontend Maven plugin. |
-| `gh` CLI | 2.40+ | For PR/release plumbing. |
-
-Verify in one shot:
-
-```bash
-java --version | head -1 # openjdk 25 ...
-mvn -v | head -1 # Apache Maven 3.9.x
-git --version # git version 2.x
-ssh -V # OpenSSH_8.x or newer
-node --version # v20.x
-gh --version | head -1 # gh version 2.x
-```
-
----
-
-## 2. Clone and configure
-
-```bash
-git clone git@github.com:RandomCodeSpace/codeiq.git ~/projects/codeiq
-cd ~/projects/codeiq
-```
-
-Apply the repo-local signed-commit config (this is what RAN-46 AC #2 codifies):
-
-```bash
-./scripts/setup-git-signed.sh
-```
-
-That script is idempotent and is the single SSoT for the per-repo `git config --local` block. It writes `user.name`, `user.email`, `user.signingkey`, `gpg.format`, `commit.gpgsign=true`, `tag.gpgsign=true`, then verifies the configured key resolves in your keychain.
-
-The script picks up your preferred signing format from (in order) the `GIT_GPG_FORMAT` env var, your global `git config gpg.format`, or the `ssh` default. Per-format expectations:
-
-- **`ssh` (default)** — `user.signingkey` must be a path on disk to your **public** key (typically `~/.ssh/id_ed25519.pub`). If you do not have an ed25519 keypair, generate one (`ssh-keygen -t ed25519 -C "you@example.com"`) and upload the public key to your GitHub account under `Settings → SSH and GPG keys → New SSH key → Key type: Signing Key` before re-running.
-- **`openpgp` / `gpg`** — `user.signingkey` must be a key id or fingerprint that `gpg --list-secret-keys` knows about. Generate / import the key first (`gpg --full-generate-key`), then `git config --global user.signingkey ` and `git config --global gpg.format openpgp` before running this script.
-- **`x509`** — `user.signingkey` must be a key id or fingerprint that `gpgsm --list-secret-keys` knows about. Configure x509 signing keys in `gpgsm` first.
-
-Sanity-check the config:
-
-```bash
-git config --local --get user.signingkey # ssh: a .pub path; openpgp/x509: a key id or fingerprint
-git config --local --get gpg.format # ssh | openpgp | gpg | x509
-git config --local --get commit.gpgsign # should print "true"
-
-# Produce a throwaway signed commit object (no refs touched) and verify it.
-sig_commit=$(echo "verify-signing" | git commit-tree HEAD^{tree} -S)
-git verify-commit "$sig_commit" # expect "Good ... signature"
-git log -1 --pretty=%G? "$sig_commit" # expect: G
-```
-
-`git verify-commit` operates on a commit object id, not stdin — capturing the
-output of `git commit-tree -S` first and then verifying that id is the right
-shape. If the verification line errors with "no principal matched", point git
-at an `allowed_signers` file: see `scripts/setup-git-signed.sh` output for the
-canonical template.
-
----
-
-## 3. Build, test, run
-
-The standard offline path is:
-
-```bash
-mvn -B -ntp -DskipTests=false clean verify
-```
-
-This runs the full pipeline: unit tests, integration tests, jacoco coverage gate (≥85%), SpotBugs, OWASP Dependency-Check, and the executable CLI JAR build. Expect ~2-3 min on a warm cache.
-
-For a faster inner loop while iterating:
-
-```bash
-mvn -B -ntp test \
- -Dspotbugs.skip=true -Ddependency-check.skip=true # unit tests only (Surefire), no static analysis / CVE plugins
-mvn -B -ntp -Dtest=SomeDetectorTest test # single unit test class
-mvn -B -ntp -DskipTests=true package # JAR only, no tests
-mvn -B -ntp verify \
- -Dspotbugs.skip=true -Ddependency-check.skip=true # unit + integration tests (Surefire + Failsafe), no static analysis / CVE plugins
-```
-
-The first command **does run tests** — earlier drafts incorrectly passed `-DskipTests` here, which would have skipped them. Reviewer finding cf64b44d (RAN-47, R5-3): Maven's `test` phase only runs Surefire (unit tests). This repo's integration tests are wired through Failsafe at the `integration-test` / `verify` phases — use the fourth form above when you need both unit + integration in the inner loop. Use `-Dspotbugs.skip` / `-Ddependency-check.skip` to keep things fast without dropping test coverage.
-
-Smoke-test the CLI end-to-end against this repo:
-
-```bash
-java -jar target/code-iq-*-cli.jar version
-java -jar target/code-iq-*-cli.jar index .
-java -jar target/code-iq-*-cli.jar enrich .
-java -jar target/code-iq-*-cli.jar serve . & # opens http://localhost:8080
-curl -fsS http://localhost:8080/api/stats > /dev/null && echo OK
-kill %1
-```
-
----
-
-## 4. Optional: full quality gate locally
-
-If you want to mirror the CI gate before pushing:
-
-```bash
-mvn -B -ntp clean verify # tests + jacoco
-mvn -B -ntp spotbugs:check # static analysis gate
-mvn -B -ntp dependency-check:check -DfailBuildOnCVSS=7 # CVE gate
-```
-
-Sonar runs only in CI (the token is not on local machines by design).
-
----
-
-## 5. Branch / PR workflow
-
-Create branches off `main`:
-
-```bash
-git fetch origin main
-git switch -c / origin/main
-```
-
-Conventional-commit subjects (`feat:`, `fix:`, `chore:`, `refactor:`, `test:`, `docs:`). One logical change per commit. Sign every commit (the local config makes this automatic). Push and open a PR:
-
-```bash
-git push -u origin HEAD
-gh pr create --fill --base main
-```
-
-Branch protection on `main` requires:
-- A Codex review approval from TechLead (or delegate).
-- CI green on the PR: `ci-java.yml` (build + jacoco 85% + dependency-check + Sonar), the repo-level CodeQL default-setup checks (`Analyze (java-kotlin)`, `Analyze (javascript-typescript)`, `Analyze (actions)`), Socket Security, SonarCloud Code Analysis.
-- All commits in the PR signed (branch protection rejects unsigned commits — there is no separate "signed-commits" status check).
-- OpenSSF Scorecard runs on push-to-`main` and a weekly cron, **not** on PRs, and is intentionally non-gating per [`engineering-standards.md`](engineering-standards.md) §1.
-
-Force-push to `main` is disabled. Direct pushes are disabled. Squash-merge is the default and only path.
-
----
-
-## 6. Where to look next
-
-- Architecture, layout, and convention SSoT: [`/CLAUDE.md`](../../CLAUDE.md).
-- Coverage / Sonar / CVE policy: [`engineering-standards.md`](engineering-standards.md).
-- Releasing: [`release.md`](release.md).
-- Rolling back a bad release: [`rollback.md`](rollback.md).
-- Security reporting: [`/SECURITY.md`](../../SECURITY.md).
-
-If you hit anything that looks like a runtime gap (missing tool, broken hook, weird auth), open a Paperclip issue against `type:devx` rather than working around it locally — the bootstrap is meant to be reproducible.
diff --git a/shared/runbooks/release-go.md b/shared/runbooks/release-go.md
deleted file mode 100644
index 66c940aa..00000000
--- a/shared/runbooks/release-go.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# Releasing the Go binary
-
-The Java side has its own `release.md` runbook; this one covers the Go
-single-binary release that ships from Phase 5 of the port onward.
-
-The pipeline is **tag-triggered, fully automated, and keyless-signed**:
-
-1. Push a semver tag matching `v*.*.*`.
-2. `.github/workflows/release-go.yml` cross-builds for linux/amd64,
- linux/arm64, darwin/arm64 (CGO + native kuzudb/sqlite forces
- per-target runners).
-3. Goreleaser packages binaries with `LICENSE`, `README.md`,
- `CHANGELOG.md` into `codeiq___.tar.gz`.
-4. Syft generates an SPDX SBOM per archive.
-5. Cosign keyless-signs `checksums.sha256` via GitHub OIDC (no
- long-lived key on the runner; signature transparency entry lands in
- the public Rekor log).
-6. GitHub release is created as a **draft** with the verification
- recipe embedded in the release notes header.
-
-## Cutting a release
-
-```bash
-# From the repo root, on main, with a clean working tree:
-git checkout main
-git pull --ff-only
-
-# Update CHANGELOG.md [Unreleased] → [vX.Y.Z] - YYYY-MM-DD. Commit.
-$EDITOR CHANGELOG.md
-git add CHANGELOG.md
-git commit -m "chore(release): vX.Y.Z"
-
-# Tag (signed) and push the tag.
-git tag -s vX.Y.Z -m "vX.Y.Z"
-git push origin vX.Y.Z
-```
-
-Within ~5 minutes:
-
-- `release-go` workflow finishes and creates a **draft** Release.
-- Sigstore transparency log records the signature.
-
-Review the draft release on GitHub — verify artifact list, checksums,
-SBOM presence, release notes — then click **Publish release**.
-
-## Verifying a downloaded artifact
-
-End-users should verify both checksum AND signature:
-
-```bash
-# Checksum
-sha256sum -c checksums.sha256
-
-# Signature (Sigstore keyless, bundle format — no key material needed locally)
-cosign verify-blob \
- --bundle checksums.sha256.cosign.bundle \
- --certificate-identity-regexp 'https://github.com/RandomCodeSpace/codeiq/.github/workflows/release-go.yml@.*' \
- --certificate-oidc-issuer https://token.actions.githubusercontent.com \
- checksums.sha256
-```
-
-A successful `cosign verify-blob` proves:
-
-- The binary was built by the release workflow in this repo (not a
- fork, not a manually-uploaded artifact).
-- The build ran on a GitHub-hosted runner under GitHub's OIDC token.
-- The signature was logged to the Rekor public transparency log.
-
-## Local dry run
-
-To validate `.goreleaser.yml` without cutting a release:
-
-```bash
-# Dry-run (builds + packages but doesn't publish).
-goreleaser release --snapshot --clean
-ls dist/
-```
-
-The `--snapshot` flag forces a fake version `-next` and
-disables publish steps (no GitHub upload, no signing). CGO is needed
-locally — `CGO_ENABLED=1` is set in `.goreleaser.yml/env`.
-
-## Failure recovery
-
-- **Tag points at a broken commit** — delete the tag locally and
- remotely (`git tag -d vX.Y.Z && git push --delete origin vX.Y.Z`),
- fix, retag. The draft release will be replaced on retag because
- `mode: replace` is set.
-- **Signing failure (OIDC token)** — usually transient. Re-run the
- workflow. The OIDC permissions in `release-go.yml` are correct;
- GitHub occasionally has Sigstore connectivity issues.
-
-## What this does NOT do
-
-- Does not push to package registries (npm, PyPI, Cargo) — codeiq is
- a single binary, not a library.
-- Does not run a smoke test of the published artifact post-release.
- Add this once we have a canary user.
-- Does not auto-bump the version. Versioning is human decision.
diff --git a/shared/runbooks/release.md b/shared/runbooks/release.md
deleted file mode 100644
index ed0cda3e..00000000
--- a/shared/runbooks/release.md
+++ /dev/null
@@ -1,163 +0,0 @@
-# Release Runbook — codeiq
-
-> **SSoT for shipping codeiq.** Owner: TechLead (until bootstrap completes); thereafter the engineer who owns the change. This runbook is the gate referenced by the bootstrap precondition (`RAN-46`): it MUST exist on `main` before any product `RAN-*` issue can leave `backlog`.
-
----
-
-## 1. Release surfaces
-
-> **AC #10 ruling (RAN-46, @CEO).** Maven Central + GitHub Releases **is** the codeiq deploy surface. There is no separate static-CDN frontend (the React UI is bundled inside the JAR) and no always-on hosted backend (codeiq runs on the developer's machine). See [`engineering-standards.md`](engineering-standards.md) §7.1 for the full rationale.
-
-codeiq ships in three forms. A "release" updates **all three** in lockstep:
-
-| Surface | Artifact | Distribution |
-|---|---|---|
-| Library JAR | `io.github.randomcodespace.iq:code-iq` (POM packaging) | Maven Central via Sonatype Central Portal |
-| Executable CLI JAR | `target/code-iq--cli.jar` | GitHub Release asset |
-| Source tag | `v` (annotated, GPG/OpenPGP-signed by `release-java.yml`) | Git tag pushed to `RandomCodeSpace/codeiq` |
-
-Versioning is [SemVer](https://semver.org/). Pre-`1.0.0` releases use `0.MINOR.PATCH`; breaking changes bump MINOR.
-
-Snapshot artifacts (`-SNAPSHOT`) are published from `main` by `beta-java.yml` to OSSRH snapshots; consumers must opt into the snapshot repo. Snapshots are **not** releases.
-
----
-
-## 2. Pre-release checklist
-
-Run BEFORE creating the tag:
-
-1. `main` is green: `gh run list --branch main --workflow ci-java.yml --limit 1` → `success`.
-2. SonarCloud Quality Gate: `gh api /repos/RandomCodeSpace/codeiq/actions/runs?branch=main --jq '.workflow_runs[0].conclusion'` and SonarCloud project page both green.
-3. Coverage ≥ 85% (jacoco rule + Sonar new-code ≥ 80% — see [`engineering-standards.md`](engineering-standards.md)).
-4. Dependency audit clean: `mvn -B -ntp clean verify` exits 0 (the OWASP `dependency-check:check` goal is bound to `verify` and fails the build on CVSS ≥ 7 — see `pom.xml`). Cross-check with the Dependabot security tab for any open advisories.
-5. SpotBugs clean: `mvn spotbugs:check` exits 0.
-6. CHANGELOG entry drafted under `[Unreleased]` and ready to promote.
-7. Working copy of `main` is clean (`git status --porcelain` empty).
-8. GPG release-signing secrets present in repo settings: `MAVEN_GPG_PRIVATE_KEY` and `MAVEN_GPG_PASSPHRASE` (verify via `gh secret list`). The workflow signs both the release commit and the annotated tag with the imported OpenPGP key — no local SSH or GPG key is required on the maintainer's machine for the GA path (Reviewer finding fd559a54, R5-7).
-
----
-
-## 3. Cut a release (canonical path)
-
-Driven by `release-java.yml` via **manual `workflow_dispatch`** with a `version` input. The workflow does **everything**: it creates a release commit (signed) on a detached HEAD with the bumped version, deploys to Maven Central from that exact source tree, and then creates a GPG-signed annotated tag pointing at that release commit. The tag is the only persistent reference to the release commit — `main` is never directly pushed by the workflow, so branch protection stays clean.
-
-```bash
-# 1. Promote CHANGELOG on main (PR + merge per branch protection)
-$EDITOR CHANGELOG.md # move [Unreleased] → [X.Y.Z] - YYYY-MM-DD
-gh pr create --fill --base main
-gh pr merge --squash --auto
-
-# 2. Trigger the release workflow with the target version
-gh workflow run release-java.yml --ref main -f version=X.Y.Z
-
-# 3. Watch it run
-gh run watch $(gh run list --workflow release-java.yml --limit 1 --json databaseId --jq '.[0].databaseId')
-```
-
-`release-java.yml` then, in order:
-1. Configures git identity (`github-actions[bot]`) and binds it to the imported `MAVEN_GPG_PRIVATE_KEY` for both commit and tag signing — same trust path as the published artifact.
-2. Runs `mvn versions:set -DnewVersion=X.Y.Z` and creates a **GPG-signed release commit** on a detached HEAD capturing that tree.
-3. Runs `mvn -P release clean deploy` from that release commit's tree (full quality gate runs along the way: jacoco 85%, SpotBugs, OWASP Dependency-Check).
-4. Creates a **GPG-signed annotated tag `vX.Y.Z`** pointing at the release commit.
-5. Pushes only the tag (`git push origin refs/tags/vX.Y.Z`). The release commit lives only as a tag-reachable object — no `main` update.
-6. Cuts a GitHub Release from the tag and uploads `code-iq-X.Y.Z-cli.jar` with auto-generated release notes.
-
-The tag therefore points at the **exact source** that produced the artifact (no divergence between source tag and released artifact), and is annotated and GPG-signed — verifiable with `git tag --verify vX.Y.Z` provided the maintainer's public GPG key is trusted locally.
-
-Manual cuts on a fork or downstream consumer follow the same flow: trigger the workflow with the target version. Direct `git tag && git push origin vX.Y.Z` from a developer machine does **not** publish.
-
----
-
-## 4. Post-release verification
-
-Within 30 minutes of the release workflow finishing:
-
-1. **Maven Central index**: `curl -fsS "https://repo.maven.apache.org/maven2/io/github/randomcodespace/iq/code-iq/X.Y.Z/code-iq-X.Y.Z.pom" | head -20`.
-2. **Smoke install**: in a clean directory, `mvn dependency:get -Dartifact=io.github.randomcodespace.iq:code-iq:X.Y.Z` succeeds.
-3. **CLI smoke**: download the GH Release JAR, run `java -jar code-iq-X.Y.Z-cli.jar version` and `java -jar code-iq-X.Y.Z-cli.jar analyze --help` — both exit 0.
-4. **GitHub Release** is marked `Latest` and links the changelog section.
-5. **codebase.repoUrl** in paperclip Project still resolves (`git ls-remote git@github.com:RandomCodeSpace/codeiq.git HEAD`).
-
-If any of (1)–(4) fails, [`rollback.md`](rollback.md) applies.
-
-### 4a. Consumer-side bundle integrity (`codeiq bundle` artifacts)
-
-When operators receive a `*-bundle.zip` produced by `codeiq bundle`, they
-**must** verify integrity before launching the bundled `serve.sh` /
-`serve.bat`. The bundle ships a `checksums.sha256` entry in standard GNU
-coreutils format, generated as the last step of bundling
-(`BundleCommand#writeChecksumsManifest`).
-
-```bash
-# 1. Unzip into a clean directory.
-unzip myrepo-v1.0-bundle.zip -d myrepo-bundle/
-cd myrepo-bundle
-
-# 2. Verify every file. Exits non-zero if any entry is missing or modified;
-# `checksums.sha256` itself is intentionally not listed (would be circular).
-sha256sum -c --quiet checksums.sha256
-
-# 3. (Optional) Skip via env var only when the bundle is trusted source-internal:
-# CODEIQ_SKIP_VERIFY=1 ./serve.sh
-./serve.sh
-```
-
-`serve.sh` runs the same `sha256sum -c` automatically when the binary is
-on `PATH`. **Do not set `CODEIQ_SKIP_VERIFY=1` in production**: it
-disables the only consumer-side integrity gate when the bundle was
-delivered out-of-band (USB, internal mirror, AKS sidecar artifact). For
-verifying `checksums.sha256` itself against tampering, sign the
-bundle.zip out-of-band (Sigstore, GPG, or compare to the GitHub Release
-SHA-256 if the bundle was published to a release).
-
-If the consumer environment does not provide `sha256sum` (Windows without
-WSL, locked-down build agents), distribute the bundle via Sigstore-signed
-release and rely on the Sigstore client for integrity. `serve.bat`
-intentionally does **not** include a Windows-native verification step
-yet — tracked under follow-up.
-
----
-
-## 5. Hot-fix patch release (`X.Y.Z+1`)
-
-1. Branch from the release tag: `git switch -c hotfix/X.Y.Z+1 vX.Y.Z`.
-2. Apply the minimal fix; add a regression test.
-3. Open PR against `main`; merge with squash.
-4. Rebase the hotfix branch onto the post-merge `main` if needed; cut the tag from `main` per §3.
-
-Hotfixes do **not** skip the pre-release checklist.
-
----
-
-## 6. Required GitHub secrets (org/repo level)
-
-| Secret | Used by | Owner |
-|---|---|---|
-| `SONAR_TOKEN` | `ci-java.yml` (Sonar gate) | TechLead |
-| `OSS_NEXUS_USER` / `OSS_NEXUS_PASS` | `release-java.yml` (Sonatype Central) | TechLead |
-| `MAVEN_GPG_KEY_ID` / `MAVEN_GPG_PASSPHRASE` / `MAVEN_GPG_PRIVATE_KEY` | `release-java.yml` (artifact signing) | TechLead |
-| `CODEQL_*` | n/a — uses `GITHUB_TOKEN` | n/a |
-
-Rotation policy: any compromise → rotate immediately + audit recent `release-java.yml` runs. Routine rotation: annually, recorded in [`engineering-standards.md`](engineering-standards.md).
-
----
-
-## 7. Auth-blocked steps (escalation path)
-
-If a release step requires auth the runtime cannot satisfy, do **not** improvise:
-
-- Sonatype Central Portal namespace re-claim → block on board (human OAuth).
-- GitHub org admin escalation (e.g., re-enable a disabled secret) → block on `aksOps` GitHub owner.
-- OpenSSF Best Practices badge updates (if the badge is required for a Release announcement) → block on board to log into bestpractices.dev.
-
-In all cases: PATCH the relevant Paperclip issue to `blocked` with the exact ask, and `@`-mention the board.
-
----
-
-## 8. References
-
-- [`rollback.md`](rollback.md) — what to do when a release goes bad.
-- [`first-time-setup.md`](first-time-setup.md) — how a new contributor builds and tests locally.
-- [`engineering-standards.md`](engineering-standards.md) — coverage/Sonar/CVE policy SSoT.
-- `pom.xml` — version, plugins, signing wiring.
-- `.github/workflows/release-java.yml` — the actual release pipeline.
diff --git a/shared/runbooks/rollback.md b/shared/runbooks/rollback.md
deleted file mode 100644
index 2ecd3329..00000000
--- a/shared/runbooks/rollback.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# Rollback Runbook — codeiq
-
-> **Purpose:** restore a known-good state when a `main` push, a release, or a CI/security change has broken the project. Owner: the engineer who shipped the change; TechLead is escalation. Pair this with [`release.md`](release.md).
-
-The rule of thumb: **revert first, root-cause second.** Users on Maven Central cannot un-install a bad version, so the cheap path is always to publish a clean follow-up rather than try to "fix forward" under pressure.
-
----
-
-## 1. Decide the scope
-
-| Symptom | Section |
-|---|---|
-| `main` is broken (CI red, build won't compile) | §2 — revert merge |
-| A release is bad on Maven Central / GH Releases | §3 — release rollback |
-| Branch protection / CI / security workflow change broke things | §4 — config rollback |
-| Embedded Neo4j cache or `.codeiq/graph` corrupted on a user box | §5 — data rollback |
-| A secret was exposed | §6 — secret rotation |
-
-If you are not sure, default to §2 and revert.
-
----
-
-## 2. Revert a bad merge to `main`
-
-```bash
-git fetch origin main
-git switch -c revert/ origin/main
-
-# Squash-merged PRs land as a single commit with one parent. Plain `git revert`
-# applies — do NOT pass `-m`, which only makes sense for true (multi-parent)
-# merge commits. Use `-m 1` only if `main` ever carries an actual merge commit
-# (which the squash-merge-only branch protection should prevent).
-git revert
-
-git push -u origin revert/
-gh pr create --base main --fill --title "revert: " --label "type:revert"
-gh pr merge --squash --auto
-```
-
-Then watch `ci-java.yml` go green and confirm SonarCloud + downstream consumers recover.
-
-**Never** force-push `main`; the bootstrap branch protection forbids it and history rewrites would break every consumer's `git pull`.
-
----
-
-## 3. Roll back a release
-
-You cannot delete or overwrite a Maven Central artifact (Sonatype policy). The only valid rollback is a **new patch release** that returns the behaviour to the prior version.
-
-1. **Block downloads of the bad version** *(best-effort)*:
- - GitHub Release: `gh release edit vX.Y.Z --prerelease --notes "Marked pre-release: see vX.Y.(Z+1) for fix"`.
- - SECURITY.md / README badges: add a one-line advisory linking the follow-up version.
-2. **Cut a hotfix patch release** per [`release.md`](release.md) §5. The hotfix MUST contain only the minimum changes needed to restore the prior behaviour, plus a regression test.
-3. **GHSA advisory** if the bad version contains a security regression: `gh api -X POST repos/RandomCodeSpace/codeiq/security-advisories -f severity=high -f summary="..." -f description="..."`. Coordinate with the OpenSSF Scorecard advisory feed.
-4. **Update CHANGELOG.md** under `[X.Y.(Z+1)]` with `### Fixed (rollback)` and link the bad-version commit + the GHSA.
-
-Do NOT delete the bad git tag. Yanking tags after they have been seen by consumers breaks `git fetch --tags` and reproducible builds.
-
----
-
-## 4. Roll back CI / branch protection / security config
-
-These are driven by `gh api` calls (see RAN-46 inventory). They are not in version control by themselves, so rollback is by re-running the prior call.
-
-- **Branch protection**: snapshot before any change with `gh api /repos/RandomCodeSpace/codeiq/branches/main/protection > /tmp/bp-before.json`. The GET payload is a denormalized view that GitHub's PUT endpoint does **not** accept verbatim (PUT flattens the nested objects: `enforce_admins.enabled` → bare boolean, `required_status_checks.checks[].context` strings → flat `contexts[]`, `*.url` fields are rejected). Reshape with the jq filter below before piping into PUT (Reviewer finding fd559a54, R5-5):
-
- ```bash
- jq '{
- required_status_checks: (
- if .required_status_checks == null then null
- else {
- strict: .required_status_checks.strict,
- contexts: ([.required_status_checks.checks[]?.context] // [])
- }
- end
- ),
- enforce_admins: (.enforce_admins.enabled // false),
- required_pull_request_reviews: (
- if .required_pull_request_reviews == null then null
- else {
- dismiss_stale_reviews: (.required_pull_request_reviews.dismiss_stale_reviews // false),
- require_code_owner_reviews: (.required_pull_request_reviews.require_code_owner_reviews // false),
- required_approving_review_count: (.required_pull_request_reviews.required_approving_review_count // 1)
- }
- end
- ),
- restrictions: null,
- required_linear_history: (.required_linear_history.enabled // false),
- allow_force_pushes: (.allow_force_pushes.enabled // false),
- allow_deletions: (.allow_deletions.enabled // false),
- block_creations: (.block_creations.enabled // false),
- required_conversation_resolution: (.required_conversation_resolution.enabled // false),
- lock_branch: (.lock_branch.enabled // false),
- allow_fork_syncing: (.allow_fork_syncing.enabled // false)
- }' /tmp/bp-before.json \
- | gh api -X PUT /repos/RandomCodeSpace/codeiq/branches/main/protection --input -
- ```
-
- The transform unwraps the `{enabled: bool}` envelopes, projects `checks[].context` strings out into the flat `contexts[]` PUT expects, drops `*.url` fields, and forces `restrictions: null` (apps/teams/users restrictions are out of scope for this repo). If you need to *change* a field instead of rolling back, edit the transformed payload before piping.
-- **CodeQL default setup**: re-toggle via Repository Settings → Code security → Code scanning. The disabled state is the safe default.
-- **Dependabot security updates**: `gh api -X PUT /repos/RandomCodeSpace/codeiq/automated-security-fixes` to enable, `-X DELETE` to disable.
-- **Workflow files** (`.github/workflows/*.yml`): revert via §2 — they are version-controlled.
-
-If a change to branch protection prevents you from merging the rollback PR (e.g., you made the wrong status check required and it never passes), `aksOps` is the only account with bypass; coordinate over the board channel before bypassing — every bypass is logged.
-
----
-
-## 5. User-side data rollback (analysis cache / Neo4j store)
-
-For users who hit a bad cache shape after upgrading:
-
-```bash
-codeiq cache clear # drops .codeiq/cache/*.h2.db
-rm -rf .codeiq/graph/graph.db # drops the enriched Neo4j store
-codeiq index # rebuild
-codeiq enrich
-```
-
-The `AnalysisCache.CACHE_VERSION` constant must be bumped in any release that changes the cache schema. If a release fails to bump it, rollback per §3 and ship a follow-up that bumps it.
-
----
-
-## 6. Secret rotation
-
-If a CI secret leaked (push protection alert, secret scanning hit, or external report):
-
-1. Rotate at the source: GitHub Settings → Secrets, regenerate at the upstream provider (Sonatype, Sonar, etc.).
-2. Re-run the latest failing workflows after rotation: `gh run rerun `.
-3. Force a new release if the leaked secret signed an artifact (re-cut the release with a fresh GPG key).
-4. Open a tracking issue (`type:security`, `priority:high`) with the rotation timestamp, scope of exposure, and affected runs.
-
----
-
-## 7. After every rollback
-
-- Add a one-line entry to `CHANGELOG.md` describing what was rolled back and why.
-- Open a follow-up Paperclip issue tagged `type:postmortem` referencing the rolled-back PR/release. The postmortem MUST land before the next release.
-- Update [`engineering-standards.md`](engineering-standards.md) if the rollback exposed a missing CI gate or test layer.
diff --git a/shared/runbooks/test-strategy.md b/shared/runbooks/test-strategy.md
deleted file mode 100644
index c1371ea8..00000000
--- a/shared/runbooks/test-strategy.md
+++ /dev/null
@@ -1,90 +0,0 @@
-# Test Strategy — codeiq
-
-> **SSoT for testing policy: layers, coverage targets, flake handling, regression scope.** Owner: QA (until codeiq grows a dedicated QA hire; until then, the engineer who owns the change owns its tests). Pairs with [`engineering-standards.md`](engineering-standards.md) §1 (quality gates) and §4 (testing tiers) — this file is the operational expansion of those two sections.
-
-If a rule here conflicts with `engineering-standards.md`, the standards file wins. This runbook is **how**; the standards file is **what**.
-
----
-
-## 1. Test layers (what runs where)
-
-codeiq runs three test tiers. Every change picks the lightest tier that gives useful signal.
-
-| Layer | Definition | Where | Runs in CI | Wall-clock target |
-|---|---|---|---|---|
-| **Unit** | Pure logic; no I/O; no Spring context; no Neo4j; no filesystem beyond `@TempDir`. The bulk of tests. | `src/test/java/...//*Test.java` (Surefire) | Every PR + push (`mvn test`) | < 10 ms / test, < 60 s suite |
-| **Integration** | Real H2 cache, real Neo4j Embedded, real `@TempDir` filesystem, real ANTLR/JavaParser. Spring context allowed when needed. | `src/test/java/.../analyzer/`, `.../graph/`, `.../intelligence/`, `.../e2e/` (Failsafe — `*IT.java` or `@IntegrationTest`) | Every PR + push (`mvn verify`) | < 5 s / test, < 5 min suite |
-| **E2E quality** | Full pipeline (`index → enrich → serve`) against a real cloned external repo (Spring PetClinic, etc.); endpoint responses validated against Context7-sourced ground-truth JSON. | `E2EQualityTest`, ground-truth at `src/test/resources/e2e/ground-truth-*.json` | On demand + nightly cron | < 10 min / repo |
-
-**Discriminator:** if a test starts an `ApplicationContext`, touches Neo4j, or reads the filesystem outside `@TempDir`, it is integration, not unit. Move it to `src/test/java/.../analyzer/` or `.../e2e/`. Keep `src/test/java/.../detector/` unit-only — detectors are stateless beans, their tests should never need Spring.
-
-**Spring profile rule:** any `@SpringBootTest` MUST have `@ActiveProfiles("test")` so Neo4j embedded does not start during unit-context runs. This is a real-bug-causing gotcha — see `CLAUDE.md` § "Gotchas".
-
----
-
-## 2. Coverage targets
-
-| Scope | Target | Floor (build fails below) | Tool |
-|---|---|---|---|
-| Project-wide line | ≥ 90% | **85%** (JaCoCo BUNDLE LINE COVEREDRATIO; `pom.xml` rule) | `jacoco-maven-plugin` |
-| New code (per PR) | ≥ 90% | **80%** | SonarCloud "new code" gate (active per `engineering-standards.md` §1) |
-| Critical paths (auth, path-traversal, max-bytes, deserialization, anything in `api/` security checks) | 100% line + branch | 100% — no merge with gaps | JaCoCo + manual review |
-| Detectors | Positive match + negative match + determinism (run twice, assert identical output) | All three present | Test convention (per `CLAUDE.md`) |
-
-Coverage exclusions live in `pom.xml` `` config. Adding to that list requires TechLead sign-off and a one-line justification per entry. Generated ANTLR sources, the Spring `application/` main, and pure data records are pre-excluded.
-
-**Coverage is a signal, not a target.** 100% coverage with assertion-free tests is worse than 60% with meaningful ones. Don't chase the number; chase the failure modes the code can actually have.
-
----
-
-## 3. What every new detector test must include
-
-Per `CLAUDE.md` § "Adding a New Detector":
-
-1. **Positive match** — at least one synthetic input that should produce the expected node/edge.
-2. **Negative match** — an input that *looks* close but should NOT match (regression guard against the framework-false-positive class — e.g., generic `router.get` patterns wrongly attributed to Quarkus).
-3. **Determinism** — run `detect()` twice on the same input, assert byte-identical `DetectorResult`. This catches `Set` iteration leaks, mutable static state, and race conditions in shared helpers.
-
-Discriminator-guard detectors (Quarkus, Fastify, Micronaut, NestJS, etc.) need at minimum **two** negative cases: (a) framework-not-imported, (b) different-framework-imported.
-
----
-
-## 4. Flake policy — flaky test = broken test
-
-A flaky test is broken. Same PR resolution; do not merge code that makes flake worse.
-
-| State | Resolution |
-|---|---|
-| Flake reproduced locally | Fix the timing / order assumption, re-run 50× before declaring solved |
-| Flake in CI only, can't reproduce | Add deterministic seeding, isolate from shared state, retry **once** to gather a second log; if still flaky, quarantine |
-| Quarantine | `@Disabled("flaky — RAN-XXX")` with a tracked Paperclip issue; never silently deleted, never `@RepeatedTest`-looped past in CI |
-| Three quarantines on the same suite | The suite is unsound; rewrite or delete. Don't accumulate `@Disabled` debt |
-
-**Never** retry-loop in CI to mask a flake. That hides real concurrency / timing bugs (and codeiq runs heavily on virtual threads — exactly the place those bugs hide).
-
----
-
-## 5. Regression suite
-
-The regression suite is **everything** in Surefire + Failsafe. There is no separate "regression" phase — `mvn verify` is it. Total wall-clock target: < 7 min on CI's `ubuntu-latest`.
-
-E2E quality tests are **not** part of the per-PR gate (too slow + require external repo clone). They run nightly and on-demand via `E2E_PETCLINIC_DIR=... mvn -Dtest=E2EQualityTest test`. A red E2E nightly opens a `RAN-*` issue with the diff against ground truth attached.
-
-Ground-truth files (`src/test/resources/e2e/ground-truth-*.json`) are versioned. Updating one requires either: (a) the underlying upstream repo legitimately changed (link the upstream PR), or (b) a bug fix landed and the prior ground truth was wrong (link the codeiq PR).
-
----
-
-## 6. What we do NOT test (out of scope here)
-
-- **No live network.** Tests must work behind a corporate firewall / air-gapped (per `~/.claude/rules/build.md`). Anything that needs `https://` goes in `E2EQualityTest` with explicit external-repo opt-in via env var.
-- **No browser / E2E UI.** The React UI is bundled in the JAR; smoke-testing the SPA boot is done in `serve`-command CLI smoke (per `first-time-setup.md` §3), not in the test suite.
-- **No load / stress testing.** Performance work uses `pom.xml` JMH harnesses where they exist. Microbenchmarks are not regression tests; they are decision-support for `~/.claude/rules/performance.md`.
-
----
-
-## 7. References
-
-- [`engineering-standards.md`](engineering-standards.md) §1 (quality gates), §4 (test tiers), §6 (style)
-- [`first-time-setup.md`](first-time-setup.md) §3 (build, test, run loops)
-- [`/CLAUDE.md`](../../CLAUDE.md) "Testing" + "Adding a New Detector"
-- `pom.xml` — `jacoco-maven-plugin` rules, `surefire`/`failsafe` config