Skip to content

Commit 6003cb8

Browse files
merge: sync feature/ran-162-language-extractors with main
Resolves add/add conflicts: - CapabilityMatrix.java/Test: kept main's version (cpp support, RAN-172) Resolves content conflicts: - EnrichCommand.java/Test: kept feature branch (LanguageEnricher, Phase 5) Co-Authored-By: Paperclip <noreply@paperclip.ing>
2 parents 00b62f5 + f06ff60 commit 6003cb8

14 files changed

Lines changed: 944 additions & 5 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package io.github.randomcodespace.iq.api;
2+
3+
import io.github.randomcodespace.iq.config.CodeIqConfig;
4+
import io.github.randomcodespace.iq.intelligence.evidence.EvidencePack;
5+
import io.github.randomcodespace.iq.intelligence.evidence.EvidencePackAssembler;
6+
import io.github.randomcodespace.iq.intelligence.evidence.EvidencePackRequest;
7+
import io.github.randomcodespace.iq.intelligence.provenance.ArtifactMetadata;
8+
import io.github.randomcodespace.iq.intelligence.query.CapabilityMatrix;
9+
import org.springframework.context.annotation.Profile;
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RequestParam;
14+
import org.springframework.web.bind.annotation.RestController;
15+
import org.springframework.web.server.ResponseStatusException;
16+
17+
import java.util.Map;
18+
19+
/**
20+
* Intelligence REST API — evidence packs, artifact metadata, and capability matrix.
21+
* Read-only. Active only in the {@code serving} profile.
22+
*/
23+
@RestController
24+
@RequestMapping("/api/intelligence")
25+
@Profile("serving")
26+
public class IntelligenceController {
27+
28+
private final EvidencePackAssembler assembler;
29+
private final ArtifactMetadata artifactMetadata;
30+
private final CodeIqConfig config;
31+
32+
public IntelligenceController(
33+
@org.springframework.beans.factory.annotation.Autowired(required = false)
34+
EvidencePackAssembler assembler,
35+
@org.springframework.beans.factory.annotation.Autowired(required = false)
36+
ArtifactMetadata artifactMetadata,
37+
CodeIqConfig config) {
38+
this.assembler = assembler;
39+
this.artifactMetadata = artifactMetadata;
40+
this.config = config;
41+
}
42+
43+
/**
44+
* Assemble an evidence pack for a symbol or file path.
45+
*
46+
* <p>At least one of {@code symbol} or {@code file} must be provided.
47+
* The {@code file} parameter is path-traversal guarded.
48+
*
49+
* @param symbol symbol name to look up
50+
* @param file file path relative to repo root (path traversal guarded)
51+
* @param maxSnippetLines max lines per snippet (optional, capped at config limit)
52+
* @param includeRefs whether to include cross-reference nodes (default false)
53+
* @return assembled evidence pack
54+
*/
55+
@GetMapping("/evidence")
56+
public EvidencePack getEvidence(
57+
@RequestParam(required = false) String symbol,
58+
@RequestParam(required = false) String file,
59+
@RequestParam(required = false) Integer maxSnippetLines,
60+
@RequestParam(defaultValue = "false") boolean includeRefs) {
61+
62+
requireAssembler();
63+
64+
// 400 when both are absent
65+
if ((symbol == null || symbol.isBlank()) && (file == null || file.isBlank())) {
66+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
67+
"At least one of 'symbol' or 'file' must be provided.");
68+
}
69+
70+
// Path traversal guard on file param
71+
if (file != null && !file.isBlank()) {
72+
java.nio.file.Path root = java.nio.file.Path.of(config.getRootPath())
73+
.toAbsolutePath().normalize();
74+
java.nio.file.Path resolved = root.resolve(file).normalize();
75+
if (!resolved.startsWith(root)) {
76+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
77+
"Invalid file path: path traversal detected.");
78+
}
79+
}
80+
81+
EvidencePackRequest request = new EvidencePackRequest(symbol, file, maxSnippetLines, includeRefs);
82+
return assembler.assemble(request, artifactMetadata);
83+
}
84+
85+
/**
86+
* Returns the artifact metadata loaded at serve startup.
87+
*/
88+
@GetMapping("/manifest")
89+
public ArtifactMetadata getManifest() {
90+
if (artifactMetadata == null) {
91+
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
92+
"Artifact metadata unavailable. Run 'enrich' first.");
93+
}
94+
return artifactMetadata;
95+
}
96+
97+
/**
98+
* Returns the full capability matrix as a JSON object.
99+
* Optionally filter by language.
100+
*
101+
* @param language optional language filter (e.g. "java", "python")
102+
*/
103+
@GetMapping("/capabilities")
104+
public Map<String, Object> getCapabilities(
105+
@RequestParam(required = false) String language) {
106+
Map<String, Object> result = new java.util.LinkedHashMap<>();
107+
if (language != null && !language.isBlank()) {
108+
result.put("language", language.strip().toLowerCase());
109+
result.put("capabilities", CapabilityMatrix.forLanguage(language).entrySet().stream()
110+
.collect(java.util.stream.Collectors.toMap(
111+
e -> e.getKey().name().toLowerCase(),
112+
e -> e.getValue().name(),
113+
(a, b) -> a,
114+
java.util.TreeMap::new)));
115+
} else {
116+
result.put("matrix", CapabilityMatrix.asSerializableMap());
117+
}
118+
return result;
119+
}
120+
121+
private void requireAssembler() {
122+
if (assembler == null) {
123+
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
124+
"Intelligence service unavailable. Run 'enrich' first.");
125+
}
126+
}
127+
}

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

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

3+
import io.github.randomcodespace.iq.intelligence.RepositoryIdentity;
4+
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;
10+
import org.springframework.beans.factory.annotation.Autowired;
311
import org.springframework.boot.context.properties.ConfigurationProperties;
12+
import org.springframework.context.annotation.Bean;
413
import org.springframework.context.annotation.Configuration;
14+
import org.springframework.context.annotation.Profile;
15+
16+
import java.nio.file.Path;
17+
import java.util.Collections;
18+
import java.util.LinkedHashMap;
19+
import java.util.Map;
520

621
/**
722
* Configuration properties for OSSCodeIQ, bound to the "codeiq" prefix.
@@ -34,6 +49,9 @@ public class CodeIqConfig {
3449
/** Whether to serve the React web UI. Set to false via --no-ui flag. */
3550
private boolean uiEnabled = true;
3651

52+
/** Maximum lines per snippet returned in evidence packs (default 50). */
53+
private int maxSnippetLines = 50;
54+
3755
public static class Graph {
3856
private String path = ".osscodeiq/graph.db";
3957

@@ -117,4 +135,60 @@ public boolean isUiEnabled() {
117135
public void setUiEnabled(boolean uiEnabled) {
118136
this.uiEnabled = uiEnabled;
119137
}
138+
139+
public int getMaxSnippetLines() {
140+
return maxSnippetLines;
141+
}
142+
143+
public void setMaxSnippetLines(int maxSnippetLines) {
144+
this.maxSnippetLines = Math.max(1, maxSnippetLines);
145+
}
146+
147+
/**
148+
* Provides {@link ArtifactMetadata} as a Spring bean in the {@code serving} profile.
149+
*
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}).
154+
*/
155+
@Bean
156+
@Profile("serving")
157+
public ArtifactMetadata artifactMetadata(
158+
@Autowired(required = false) GraphStore graphStore) {
159+
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+
);
193+
}
120194
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.github.randomcodespace.iq.intelligence.evidence;
2+
3+
import io.github.randomcodespace.iq.intelligence.CapabilityLevel;
4+
import io.github.randomcodespace.iq.intelligence.lexical.CodeSnippet;
5+
import io.github.randomcodespace.iq.intelligence.provenance.ArtifactMetadata;
6+
import io.github.randomcodespace.iq.model.CodeNode;
7+
8+
import java.util.List;
9+
10+
/**
11+
* Runtime-facing evidence pack: everything the caller needs to understand a symbol or file.
12+
*
13+
* @param matchedSymbols Nodes whose name matched the requested symbol or file.
14+
* @param relatedFiles File paths of related nodes discovered via cross-references.
15+
* @param references Related nodes discovered via cross-reference traversal (non-empty when
16+
* {@link EvidencePackRequest#includeReferences()} is true).
17+
* @param snippets Bounded source snippets extracted for matched symbols.
18+
* @param provenance Provenance maps (one per matched node; may be null entries).
19+
* @param degradationNotes Human-readable notes explaining capability gaps; empty list when fully capable.
20+
* @param artifactMetadata Runtime projection of the artifact manifest.
21+
* @param capabilityLevel Overall capability level for the primary language of the matched symbols.
22+
*/
23+
public record EvidencePack(
24+
List<CodeNode> matchedSymbols,
25+
List<String> relatedFiles,
26+
List<CodeNode> references,
27+
List<CodeSnippet> snippets,
28+
List<java.util.Map<String, Object>> provenance,
29+
List<String> degradationNotes,
30+
ArtifactMetadata artifactMetadata,
31+
CapabilityLevel capabilityLevel
32+
) {
33+
/** Returns an empty evidence pack — used when no symbols are found. */
34+
public static EvidencePack empty(ArtifactMetadata artifactMetadata, String degradationNote) {
35+
List<String> notes = degradationNote != null ? List.of(degradationNote) : List.of();
36+
return new EvidencePack(
37+
List.of(), List.of(), List.of(), List.of(), List.of(),
38+
notes, artifactMetadata, CapabilityLevel.UNSUPPORTED
39+
);
40+
}
41+
}

0 commit comments

Comments
 (0)