Skip to content

Commit 92c1813

Browse files
committed
Fix evidence lookup and deterministic graph metadata
1 parent d24836c commit 92c1813

14 files changed

Lines changed: 292 additions & 108 deletions

File tree

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

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.springframework.stereotype.Service;
88

99
import java.io.IOException;
10+
import java.nio.charset.StandardCharsets;
1011
import java.nio.file.FileVisitResult;
1112
import java.nio.file.Files;
1213
import java.nio.file.Path;
@@ -122,38 +123,17 @@ private List<DiscoveredFile> discoverViaGit(Path root) {
122123
.directory(root.toFile())
123124
.start();
124125

125-
String output;
126-
try (var is = process.getInputStream()) {
127-
output = new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
128-
}
129-
process.waitFor();
130-
131126
List<DiscoveredFile> result = new ArrayList<>();
132-
for (String line : output.split("\n")) {
133-
String trimmed = line.trim();
134-
if (trimmed.isEmpty()) continue;
135-
136-
Path relPath = Path.of(trimmed);
137-
Path absPath = root.resolve(relPath);
138-
139-
if (!Files.isRegularFile(absPath)) continue;
140-
if (isExcluded(relPath)) continue;
141-
if (isExcludedFilename(relPath)) continue;
142-
143-
String language = DetectorUtils.deriveLanguage(trimmed);
144-
if (language == null) continue;
145-
146-
long size;
147-
try {
148-
size = Files.size(absPath);
149-
} catch (IOException e) {
150-
continue;
127+
try (var reader = process.inputReader(StandardCharsets.UTF_8)) {
128+
String line;
129+
while ((line = reader.readLine()) != null) {
130+
addGitDiscoveredFile(root, result, line);
151131
}
152-
long maxSize = CONFIG_LANGUAGES.contains(language)
153-
? CONFIG_MAX_FILE_SIZE : DEFAULT_MAX_FILE_SIZE;
154-
if (size > maxSize) continue;
155-
156-
result.add(new DiscoveredFile(relPath, language, size));
132+
}
133+
int exitCode = process.waitFor();
134+
if (exitCode != 0) {
135+
log.warn("git ls-files exited with code {} -- falling back to filesystem walk", exitCode);
136+
return discoverViaWalk(root);
157137
}
158138
return result;
159139

@@ -167,6 +147,33 @@ private List<DiscoveredFile> discoverViaGit(Path root) {
167147
}
168148
}
169149

150+
private void addGitDiscoveredFile(Path root, List<DiscoveredFile> result, String line) {
151+
String trimmed = line.trim();
152+
if (trimmed.isEmpty()) return;
153+
154+
Path relPath = Path.of(trimmed);
155+
Path absPath = root.resolve(relPath);
156+
157+
if (!Files.isRegularFile(absPath)) return;
158+
if (isExcluded(relPath)) return;
159+
if (isExcludedFilename(relPath)) return;
160+
161+
String language = DetectorUtils.deriveLanguage(trimmed);
162+
if (language == null) return;
163+
164+
long size;
165+
try {
166+
size = Files.size(absPath);
167+
} catch (IOException e) {
168+
return;
169+
}
170+
long maxSize = CONFIG_LANGUAGES.contains(language)
171+
? CONFIG_MAX_FILE_SIZE : DEFAULT_MAX_FILE_SIZE;
172+
if (size > maxSize) return;
173+
174+
result.add(new DiscoveredFile(relPath, language, size));
175+
}
176+
170177
// ------------------------------------------------------------------
171178
// Filesystem walk fallback
172179
// ------------------------------------------------------------------

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.github.randomcodespace.iq.intelligence.evidence.EvidencePackAssembler;
66
import io.github.randomcodespace.iq.intelligence.evidence.EvidencePackRequest;
77
import io.github.randomcodespace.iq.intelligence.provenance.ArtifactMetadata;
8+
import io.github.randomcodespace.iq.intelligence.provenance.ArtifactMetadataProvider;
89
import io.github.randomcodespace.iq.intelligence.query.CapabilityMatrix;
910
import org.springframework.context.annotation.Profile;
1011
import org.springframework.http.HttpStatus;
@@ -26,17 +27,17 @@
2627
public class IntelligenceController {
2728

2829
private final EvidencePackAssembler assembler;
29-
private final ArtifactMetadata artifactMetadata;
30+
private final ArtifactMetadataProvider artifactMetadataProvider;
3031
private final CodeIqConfig config;
3132

3233
public IntelligenceController(
3334
@org.springframework.beans.factory.annotation.Autowired(required = false)
3435
EvidencePackAssembler assembler,
3536
@org.springframework.beans.factory.annotation.Autowired(required = false)
36-
ArtifactMetadata artifactMetadata,
37+
ArtifactMetadataProvider artifactMetadataProvider,
3738
CodeIqConfig config) {
3839
this.assembler = assembler;
39-
this.artifactMetadata = artifactMetadata;
40+
this.artifactMetadataProvider = artifactMetadataProvider;
4041
this.config = config;
4142
}
4243

@@ -79,19 +80,19 @@ public EvidencePack getEvidence(
7980
}
8081

8182
EvidencePackRequest request = new EvidencePackRequest(symbol, file, maxSnippetLines, includeRefs);
82-
return assembler.assemble(request, artifactMetadata);
83+
return assembler.assemble(request, currentArtifactMetadata());
8384
}
8485

8586
/**
8687
* Returns the artifact metadata loaded at serve startup.
8788
*/
8889
@GetMapping("/manifest")
8990
public ArtifactMetadata getManifest() {
90-
if (artifactMetadata == null) {
91+
if (artifactMetadataProvider == null) {
9192
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
9293
"Artifact metadata unavailable. Run 'enrich' first.");
9394
}
94-
return artifactMetadata;
95+
return artifactMetadataProvider.current();
9596
}
9697

9798
/**
@@ -124,4 +125,8 @@ private void requireAssembler() {
124125
"Intelligence service unavailable. Run 'enrich' first.");
125126
}
126127
}
128+
129+
private ArtifactMetadata currentArtifactMetadata() {
130+
return artifactMetadataProvider != null ? artifactMetadataProvider.current() : null;
131+
}
127132
}

src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java

Lines changed: 6 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
11
package io.github.randomcodespace.iq.config;
22

3-
import io.github.randomcodespace.iq.intelligence.RepositoryIdentity;
43
import io.github.randomcodespace.iq.graph.GraphStore;
5-
import io.github.randomcodespace.iq.intelligence.ArtifactManifest;
6-
import io.github.randomcodespace.iq.intelligence.CapabilityLevel;
7-
import io.github.randomcodespace.iq.intelligence.Provenance;
8-
import io.github.randomcodespace.iq.intelligence.provenance.ArtifactMetadata;
9-
import io.github.randomcodespace.iq.intelligence.query.CapabilityMatrix;
4+
import io.github.randomcodespace.iq.intelligence.provenance.ArtifactMetadataProvider;
105
import org.springframework.beans.factory.annotation.Autowired;
116
import org.springframework.boot.context.properties.ConfigurationProperties;
127
import org.springframework.context.annotation.Bean;
138
import org.springframework.context.annotation.Configuration;
149
import org.springframework.context.annotation.Profile;
1510

1611
import java.nio.file.Path;
17-
import java.util.Collections;
18-
import java.util.LinkedHashMap;
19-
import java.util.Map;
2012

2113
/**
2214
* Configuration properties for Code IQ, bound to the "codeiq" prefix.
@@ -145,50 +137,16 @@ public void setMaxSnippetLines(int maxSnippetLines) {
145137
}
146138

147139
/**
148-
* Provides {@link ArtifactMetadata} as a Spring bean in the {@code serving} profile.
140+
* Provides on-demand artifact metadata in the {@code serving} profile.
149141
*
150-
* <p>Metadata is derived at serve-startup from the analysed repository and the
151-
* populated Neo4j graph. {@code graphStore} is optional so serve can start even
152-
* when the graph has not been populated yet (the manifest endpoint returns 503 in
153-
* that case, handled by {@code IntelligenceController}).
142+
* <p>Graph-derived fields are resolved lazily so H2-to-Neo4j bootstrap can complete
143+
* before clients fetch manifest data.
154144
*/
155145
@Bean
156146
@Profile("serving")
157-
public ArtifactMetadata artifactMetadata(
147+
public ArtifactMetadataProvider artifactMetadataProvider(
158148
@Autowired(required = false) GraphStore graphStore) {
159149
Path root = Path.of(rootPath).toAbsolutePath().normalize();
160-
RepositoryIdentity identity = RepositoryIdentity.resolve(root);
161-
162-
long nodeCount = 0L;
163-
long edgeCount = 0L;
164-
if (graphStore != null) {
165-
try {
166-
nodeCount = graphStore.count();
167-
edgeCount = graphStore.countEdges();
168-
} catch (Exception ignored) {
169-
// Graph not yet populated — counts stay zero
170-
}
171-
}
172-
173-
String integrityHash = ArtifactMetadata.computeIntegrityHash(
174-
nodeCount, edgeCount, identity.commitSha());
175-
176-
Map<String, Map<String, CapabilityLevel>> langCaps = new LinkedHashMap<>();
177-
CapabilityMatrix.asSerializableMap().forEach((lang, dims) -> {
178-
Map<String, CapabilityLevel> dimMap = new LinkedHashMap<>();
179-
dims.forEach((dim, level) -> dimMap.put(dim, CapabilityLevel.valueOf(level)));
180-
langCaps.put(lang, Collections.unmodifiableMap(dimMap));
181-
});
182-
183-
return new ArtifactMetadata(
184-
identity.repoUrl() != null ? identity.repoUrl() : root.toString(),
185-
identity.commitSha(),
186-
identity.buildTimestamp(),
187-
String.valueOf(Provenance.CURRENT_SCHEMA_VERSION),
188-
String.valueOf(ArtifactManifest.BUNDLE_FORMAT_VERSION),
189-
Map.of("code-iq", "phase-4"),
190-
Collections.unmodifiableMap(langCaps),
191-
integrityHash
192-
);
150+
return new ArtifactMetadataProvider(root, graphStore);
193151
}
194152
}

src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -463,13 +463,13 @@ public List<CodeNode> findDependents(String moduleId) {
463463

464464
public List<CodeNode> findByKindPaginated(String kind, int offset, int limit) {
465465
return queryNodes(
466-
"MATCH (n:CodeNode) WHERE n.kind = $kind RETURN n SKIP $offset LIMIT $limit",
466+
"MATCH (n:CodeNode) WHERE n.kind = $kind RETURN n ORDER BY n.id SKIP $offset LIMIT $limit",
467467
Map.of(PROP_KIND, kind, PROP_OFFSET, offset, PROP_LIMIT, limit));
468468
}
469469

470470
public List<CodeNode> findAllPaginated(int offset, int limit) {
471471
return queryNodes(
472-
"MATCH (n:CodeNode) RETURN n SKIP $offset LIMIT $limit",
472+
"MATCH (n:CodeNode) RETURN n ORDER BY n.id SKIP $offset LIMIT $limit",
473473
Map.of(PROP_OFFSET, offset, PROP_LIMIT, limit));
474474
}
475475

@@ -568,7 +568,7 @@ public List<Map<String, Object>> findEdgesPaginated(int offset, int limit) {
568568
var result = tx.execute(
569569
"MATCH (s:CodeNode)-[r:RELATES_TO]->(t:CodeNode) "
570570
+ "RETURN r.id AS id, r.kind AS kind, s.id AS sourceId, t.id AS targetId "
571-
+ "SKIP $offset LIMIT $limit",
571+
+ "ORDER BY r.id SKIP $offset LIMIT $limit",
572572
Map.of(PROP_OFFSET, offset, PROP_LIMIT, limit));
573573
while (result.hasNext()) {
574574
var row = result.next();
@@ -589,7 +589,7 @@ public List<Map<String, Object>> findEdgesByKindPaginated(String kind, int offse
589589
var result = tx.execute(
590590
"MATCH (s:CodeNode)-[r:RELATES_TO]->(t:CodeNode) WHERE r.kind = $kind "
591591
+ "RETURN r.id AS id, r.kind AS kind, s.id AS sourceId, t.id AS targetId "
592-
+ "SKIP $offset LIMIT $limit",
592+
+ "ORDER BY r.id SKIP $offset LIMIT $limit",
593593
Map.of(PROP_KIND, kind, PROP_OFFSET, offset, PROP_LIMIT, limit));
594594
while (result.hasNext()) {
595595
var row = result.next();

src/main/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssembler.java

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Map;
2525
import java.util.Optional;
2626
import java.util.Set;
27+
import java.util.Comparator;
2728

2829
/**
2930
* Assembles {@link EvidencePack} instances from query plans and lexical results.
@@ -68,26 +69,30 @@ public EvidencePackAssembler(LexicalQueryService lexicalQueryService,
6869
public EvidencePack assemble(EvidencePackRequest request, ArtifactMetadata artifactMetadata) {
6970
int maxLines = resolveMaxLines(request.maxSnippetLines());
7071
Path rootPath = Path.of(config.getRootPath()).toAbsolutePath().normalize();
71-
72-
// Resolve query subject — prefer symbol, fall back to filePath
73-
String subject = request.symbol() != null && !request.symbol().isBlank()
72+
String symbol = request.symbol() != null && !request.symbol().isBlank()
7473
? request.symbol().strip()
75-
: (request.filePath() != null ? request.filePath().strip() : null);
74+
: null;
75+
String filePath = request.filePath() != null && !request.filePath().isBlank()
76+
? request.filePath().strip()
77+
: null;
7678

79+
// Resolve query subject — prefer symbol, fall back to filePath
80+
String subject = symbol != null ? symbol : filePath;
7781
if (subject == null) {
7882
return EvidencePack.empty(artifactMetadata, "No symbol or file path provided.");
7983
}
8084

8185
// Determine language from filePath when available (for query planner)
82-
String language = request.filePath() != null
83-
? inferLanguage(request.filePath())
86+
String language = filePath != null
87+
? inferLanguage(filePath)
8488
: PROP_UNKNOWN;
8589

8690
// Plan the query
8791
QueryPlan plan = queryPlanner.plan(QueryType.FIND_SYMBOL, language);
8892

89-
// Execute lexical lookup
90-
List<LexicalResult> lexResults = lexicalQueryService.findByIdentifier(subject);
93+
List<LexicalResult> lexResults = symbol != null
94+
? lexicalQueryService.findByIdentifier(symbol)
95+
: findByFilePath(filePath);
9196

9297
if (lexResults.isEmpty()) {
9398
String degradationNote = buildEmptyNote(subject, plan);
@@ -156,6 +161,13 @@ public EvidencePack assemble(EvidencePackRequest request, ArtifactMetadata artif
156161
);
157162
}
158163

164+
private List<LexicalResult> findByFilePath(String filePath) {
165+
return graphStore.findByFilePath(filePath).stream()
166+
.sorted(Comparator.comparing(CodeNode::getId, Comparator.nullsLast(String::compareTo)))
167+
.map(node -> LexicalResult.of(node, 1.0f, "file_path"))
168+
.toList();
169+
}
170+
159171
// ------------------------------------------------------------------
160172
// Helpers
161173
// ------------------------------------------------------------------
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.github.randomcodespace.iq.intelligence.provenance;
2+
3+
import io.github.randomcodespace.iq.graph.GraphStore;
4+
import io.github.randomcodespace.iq.intelligence.ArtifactManifest;
5+
import io.github.randomcodespace.iq.intelligence.CapabilityLevel;
6+
import io.github.randomcodespace.iq.intelligence.Provenance;
7+
import io.github.randomcodespace.iq.intelligence.RepositoryIdentity;
8+
import io.github.randomcodespace.iq.intelligence.query.CapabilityMatrix;
9+
10+
import java.nio.file.Path;
11+
import java.time.Instant;
12+
import java.util.Collections;
13+
import java.util.LinkedHashMap;
14+
import java.util.Map;
15+
16+
/**
17+
* Computes runtime artifact metadata on demand so graph-derived fields reflect the
18+
* currently loaded graph instead of the graph state at bean construction time.
19+
*/
20+
public class ArtifactMetadataProvider {
21+
private final String repositoryIdentity;
22+
private final String commitSha;
23+
private final Instant buildTimestamp;
24+
private final Map<String, String> extractorVersions;
25+
private final Map<String, Map<String, CapabilityLevel>> languageCapabilities;
26+
private final GraphStore graphStore;
27+
28+
public ArtifactMetadataProvider(Path root, GraphStore graphStore) {
29+
RepositoryIdentity identity = RepositoryIdentity.resolve(root.toAbsolutePath().normalize());
30+
this.repositoryIdentity = identity.repoUrl() != null ? identity.repoUrl() : root.toAbsolutePath().normalize().toString();
31+
this.commitSha = identity.commitSha();
32+
this.buildTimestamp = identity.buildTimestamp();
33+
this.extractorVersions = Map.of("code-iq", "phase-4");
34+
this.languageCapabilities = buildLanguageCapabilities();
35+
this.graphStore = graphStore;
36+
}
37+
38+
public ArtifactMetadata current() {
39+
long nodeCount = 0L;
40+
long edgeCount = 0L;
41+
if (graphStore != null) {
42+
try {
43+
nodeCount = graphStore.count();
44+
edgeCount = graphStore.countEdges();
45+
} catch (Exception ignored) {
46+
// Graph may still be empty or unavailable during startup.
47+
}
48+
}
49+
50+
return new ArtifactMetadata(
51+
repositoryIdentity,
52+
commitSha,
53+
buildTimestamp,
54+
String.valueOf(Provenance.CURRENT_SCHEMA_VERSION),
55+
String.valueOf(ArtifactManifest.BUNDLE_FORMAT_VERSION),
56+
extractorVersions,
57+
languageCapabilities,
58+
ArtifactMetadata.computeIntegrityHash(nodeCount, edgeCount, commitSha)
59+
);
60+
}
61+
62+
private static Map<String, Map<String, CapabilityLevel>> buildLanguageCapabilities() {
63+
Map<String, Map<String, CapabilityLevel>> langCaps = new LinkedHashMap<>();
64+
CapabilityMatrix.asSerializableMap().forEach((lang, dims) -> {
65+
Map<String, CapabilityLevel> dimMap = new LinkedHashMap<>();
66+
dims.forEach((dim, level) -> dimMap.put(dim, CapabilityLevel.valueOf(level)));
67+
langCaps.put(lang, Collections.unmodifiableMap(dimMap));
68+
});
69+
return Collections.unmodifiableMap(langCaps);
70+
}
71+
}

0 commit comments

Comments
 (0)