| 1 | package org.egothor.methodatlas.ai; | |
| 2 | ||
| 3 | import java.io.IOException; | |
| 4 | import java.nio.charset.StandardCharsets; | |
| 5 | import java.nio.file.Files; | |
| 6 | import java.nio.file.Path; | |
| 7 | import java.util.List; | |
| 8 | ||
| 9 | import com.fasterxml.jackson.databind.DeserializationFeature; | |
| 10 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 11 | ||
| 12 | /** | |
| 13 | * Handles the consume phase of the manual AI workflow. | |
| 14 | * | |
| 15 | * <p> | |
| 16 | * For each test class this engine looks for a response file | |
| 17 | * {@code <fqcn>.response.txt} in the configured response directory. If the file | |
| 18 | * is present its content is parsed as an AI classification result and the | |
| 19 | * extracted suggestions are returned normally. If the file is absent an empty | |
| 20 | * suggestion is returned, which results in blank AI columns for that class in | |
| 21 | * the final CSV. | |
| 22 | * </p> | |
| 23 | * | |
| 24 | * <p> | |
| 25 | * This engine implements {@link AiSuggestionEngine} so it can be used as a | |
| 26 | * drop-in replacement for network-based engines in the standard scan loop. The | |
| 27 | * {@code classSource} parameter passed to {@link #suggestForClass} is ignored | |
| 28 | * because the AI has already processed the source during the prepare phase. | |
| 29 | * </p> | |
| 30 | * | |
| 31 | * <h2>Response file format</h2> | |
| 32 | * | |
| 33 | * <p> | |
| 34 | * The response file may contain free-form text (for example the operator may | |
| 35 | * have copied the AI response verbatim from the chat window). The engine | |
| 36 | * extracts the first JSON object found in the file using | |
| 37 | * {@link JsonText#extractFirstJsonObject} and deserializes it into an | |
| 38 | * {@link AiClassSuggestion}. Any surrounding prose or formatting is silently | |
| 39 | * discarded. | |
| 40 | * </p> | |
| 41 | * | |
| 42 | * @see ManualPrepareEngine | |
| 43 | * @see AiSuggestionEngine | |
| 44 | * @see JsonText | |
| 45 | */ | |
| 46 | public final class ManualConsumeEngine implements AiSuggestionEngine { | |
| 47 | ||
| 48 | private final Path responseDir; | |
| 49 | private final ObjectMapper mapper; | |
| 50 | ||
| 51 | /** | |
| 52 | * Creates a new consume engine that reads response files from the given | |
| 53 | * directory. | |
| 54 | * | |
| 55 | * @param responseDir path to the directory containing operator-saved response | |
| 56 | * files | |
| 57 | */ | |
| 58 | public ManualConsumeEngine(Path responseDir) { | |
| 59 | this.responseDir = responseDir; | |
| 60 | this.mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | |
| 61 | } | |
| 62 | ||
| 63 | /** | |
| 64 | * Returns AI classification results for the specified class by reading the | |
| 65 | * corresponding response file. | |
| 66 | * | |
| 67 | * <p> | |
| 68 | * The response file is looked up as {@code <fileStem>.response.txt} in the | |
| 69 | * configured response directory, where {@code fileStem} is the dot-separated | |
| 70 | * path identifier computed from the source file's location relative to the scan | |
| 71 | * root (e.g. {@code module-a.src.test.java.com.acme.FooTest}). If the file does | |
| 72 | * not exist an empty {@link AiClassSuggestion} is returned so the caller emits | |
| 73 | * blank AI columns rather than failing. | |
| 74 | * </p> | |
| 75 | * | |
| 76 | * @param fileStem dot-separated path stem used to locate the response file | |
| 77 | * ({@code <fileStem>.response.txt}) | |
| 78 | * @param fqcn fully qualified class name; included in the returned | |
| 79 | * suggestion for identification | |
| 80 | * @param classSource ignored — the AI already saw the source during the | |
| 81 | * prepare phase | |
| 82 | * @param targetMethods ignored — method classification is read from the | |
| 83 | * response file | |
| 84 | * @return parsed and normalized suggestion, or an empty suggestion when no | |
| 85 | * response file exists | |
| 86 | * @throws AiSuggestionException if the response file exists but cannot be read | |
| 87 | * or does not contain a valid JSON object | |
| 88 | */ | |
| 89 | @Override | |
| 90 | public AiClassSuggestion suggestForClass(String fileStem, String fqcn, String classSource, | |
| 91 | List<PromptBuilder.TargetMethod> targetMethods) throws AiSuggestionException { | |
| 92 | Path responseFile = responseDir.resolve(fileStem + ".response.txt"); | |
| 93 | ||
| 94 |
2
1. suggestForClass : removed conditional - replaced equality check with false → KILLED 2. suggestForClass : removed conditional - replaced equality check with true → KILLED |
if (!Files.exists(responseFile)) { |
| 95 |
1
1. suggestForClass : replaced return value with null for org/egothor/methodatlas/ai/ManualConsumeEngine::suggestForClass → KILLED |
return new AiClassSuggestion(fqcn, null, List.of(), null, List.of()); // fqcn used for className |
| 96 | } | |
| 97 | ||
| 98 | try { | |
| 99 | String responseText = Files.readString(responseFile, StandardCharsets.UTF_8); | |
| 100 | String json = JsonText.extractFirstJsonObject(responseText); | |
| 101 | AiClassSuggestion raw = mapper.readValue(json, AiClassSuggestion.class); | |
| 102 |
1
1. suggestForClass : replaced return value with null for org/egothor/methodatlas/ai/ManualConsumeEngine::suggestForClass → KILLED |
return normalize(raw); |
| 103 | } catch (IOException e) { | |
| 104 | throw new AiSuggestionException("Failed to read response file: " + responseFile, e); | |
| 105 | } | |
| 106 | } | |
| 107 | ||
| 108 | /** | |
| 109 | * Normalizes a raw provider response into the application's internal result | |
| 110 | * invariants. | |
| 111 | * | |
| 112 | * <p> | |
| 113 | * Ensures that collection-valued fields are never {@code null} and removes | |
| 114 | * malformed method entries that do not contain a valid method name. | |
| 115 | * </p> | |
| 116 | * | |
| 117 | * @param input raw suggestion deserialized from the operator-saved response file | |
| 118 | * @return normalized suggestion instance | |
| 119 | */ | |
| 120 | private static AiClassSuggestion normalize(AiClassSuggestion input) { | |
| 121 |
2
1. normalize : removed conditional - replaced equality check with false → SURVIVED 2. normalize : removed conditional - replaced equality check with true → KILLED |
List<AiMethodSuggestion> methods = input.methods() == null ? List.of() : input.methods(); |
| 122 |
2
1. normalize : removed conditional - replaced equality check with false → KILLED 2. normalize : removed conditional - replaced equality check with true → KILLED |
List<String> classTags = input.classTags() == null ? List.of() : input.classTags(); |
| 123 | ||
| 124 | List<AiMethodSuggestion> normalizedMethods = methods.stream() | |
| 125 |
7
1. lambda$normalize$0 : removed conditional - replaced equality check with true → SURVIVED 2. lambda$normalize$0 : removed conditional - replaced equality check with false → KILLED 3. lambda$normalize$0 : removed conditional - replaced equality check with true → KILLED 4. lambda$normalize$0 : replaced boolean return with true for org/egothor/methodatlas/ai/ManualConsumeEngine::lambda$normalize$0 → KILLED 5. lambda$normalize$0 : removed conditional - replaced equality check with false → KILLED 6. lambda$normalize$0 : removed conditional - replaced equality check with true → KILLED 7. lambda$normalize$0 : removed conditional - replaced equality check with false → KILLED |
.filter(m -> m != null && m.methodName() != null && !m.methodName().isBlank()) |
| 126 |
1
1. lambda$normalize$1 : replaced return value with null for org/egothor/methodatlas/ai/ManualConsumeEngine::lambda$normalize$1 → KILLED |
.map(m -> new AiMethodSuggestion(m.methodName(), m.securityRelevant(), |
| 127 |
2
1. lambda$normalize$1 : removed conditional - replaced equality check with false → KILLED 2. lambda$normalize$1 : removed conditional - replaced equality check with true → KILLED |
m.displayName(), m.tags() == null ? List.of() : m.tags(), m.reason(), m.confidence(), |
| 128 | m.interactionScore())) | |
| 129 | .toList(); | |
| 130 | ||
| 131 |
1
1. normalize : replaced return value with null for org/egothor/methodatlas/ai/ManualConsumeEngine::normalize → KILLED |
return new AiClassSuggestion(input.className(), input.classSecurityRelevant(), classTags, |
| 132 | input.classReason(), normalizedMethods); | |
| 133 | } | |
| 134 | } | |
Mutations | ||
| 94 |
1.1 2.2 |
|
| 95 |
1.1 |
|
| 102 |
1.1 |
|
| 121 |
1.1 2.2 |
|
| 122 |
1.1 2.2 |
|
| 125 |
1.1 2.2 3.3 4.4 5.5 6.6 7.7 |
|
| 126 |
1.1 |
|
| 127 |
1.1 2.2 |
|
| 131 |
1.1 |