|
| 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 | +} |
0 commit comments