ManualConsumeEngine.java

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
Location : suggestForClass
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_returnsEmptySuggestionWhenResponseFileAbsent(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : suggestForClass
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_throwsWhenResponseFileContainsNoJson(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

95

1.1
Location : suggestForClass
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_returnsEmptySuggestionWhenResponseFileAbsent(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/ai/ManualConsumeEngine::suggestForClass → KILLED

102

1.1
Location : suggestForClass
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_extractsJsonFromWrappedResponse(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/ai/ManualConsumeEngine::suggestForClass → KILLED

121

1.1
Location : normalize
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

2.2
Location : normalize
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_normalizesNullMethodsAndTags(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

122

1.1
Location : normalize
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_normalizesNullMethodsAndTags(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : normalize
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_parsesValidJsonResponseFile(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

125

1.1
Location : lambda$normalize$0
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_normalizesNullMethodsAndTags(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : lambda$normalize$0
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_normalizesNullMethodsAndTags(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

3.3
Location : lambda$normalize$0
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_normalizesNullMethodsAndTags(java.nio.file.Path)]
replaced boolean return with true for org/egothor/methodatlas/ai/ManualConsumeEngine::lambda$normalize$0 → KILLED

4.4
Location : lambda$normalize$0
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

5.5
Location : lambda$normalize$0
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_normalizesNullMethodsAndTags(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

6.6
Location : lambda$normalize$0
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_normalizesNullMethodsAndTags(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

7.7
Location : lambda$normalize$0
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_normalizesNullMethodsAndTags(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

126

1.1
Location : lambda$normalize$1
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_normalizesNullMethodsAndTags(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/ai/ManualConsumeEngine::lambda$normalize$1 → KILLED

127

1.1
Location : lambda$normalize$1
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_normalizesNullMethodsAndTags(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : lambda$normalize$1
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_parsesValidJsonResponseFile(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

131

1.1
Location : normalize
Killed by : org.egothor.methodatlas.ai.ManualConsumeEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualConsumeEngineTest]/[method:suggestForClass_extractsJsonFromWrappedResponse(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/ai/ManualConsumeEngine::normalize → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1