ManualPrepareEngine.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
/**
10
 * Handles the prepare phase of the manual AI workflow.
11
 *
12
 * <p>
13
 * This engine supports operators who cannot use an automated AI API but can
14
 * interact with an AI through a standard chat window. For each test class it
15
 * writes a <em>work file</em> to the configured work directory. Each work file
16
 * contains:
17
 * </p>
18
 *
19
 * <ul>
20
 * <li>human-readable operator instructions</li>
21
 * <li>the complete AI prompt (taxonomy, method list, class source) that the
22
 * operator should paste into their AI chat window</li>
23
 * </ul>
24
 *
25
 * <p>
26
 * After the AI responds the operator pastes the response text into the
27
 * pre-created {@code <fqcn>.response.txt} stub and then runs the consume phase
28
 * (via {@link ManualConsumeEngine}) to produce the final enriched CSV.
29
 * </p>
30
 *
31
 * <h2>File naming</h2>
32
 *
33
 * <p>
34
 * Work files are named {@code <fqcn>.txt} and written to the work directory.
35
 * Empty response stubs are named {@code <fqcn>.response.txt} and written to
36
 * the response directory. Both directories are flat (no sub-directory
37
 * structure). The two directories may be the same path.
38
 * </p>
39
 *
40
 * @see ManualConsumeEngine
41
 * @see PromptBuilder
42
 */
43
public final class ManualPrepareEngine {
44
45
    private static final String SEPARATOR = "=".repeat(80);
46
47
    private final Path workDir;
48
    private final Path responseDir;
49
    private final String taxonomyText;
50
    private final boolean confidence;
51
52
    /**
53
     * Creates a new prepare engine that writes work files and response stubs to
54
     * separate directories.
55
     *
56
     * <p>
57
     * Both directories are created if they do not already exist. The two paths may
58
     * point to the same directory.
59
     * </p>
60
     *
61
     * @param workDir     path to the directory where work files ({@code <fqcn>.txt})
62
     *                    will be written
63
     * @param responseDir path to the directory where empty response stubs
64
     *                    ({@code <fqcn>.response.txt}) will be pre-created
65
     * @param options     AI options used to load the taxonomy text; only taxonomy
66
     *                    settings are relevant here — provider settings are ignored
67
     * @throws AiSuggestionException if either directory cannot be created or the
68
     *                               configured taxonomy file cannot be read
69
     */
70
    public ManualPrepareEngine(Path workDir, Path responseDir, AiOptions options) throws AiSuggestionException {
71
        this.workDir = workDir;
72
        this.responseDir = responseDir;
73
        this.taxonomyText = loadTaxonomy(options);
74
        this.confidence = options.confidence();
75
76
        try {
77
            Files.createDirectories(workDir);
78
        } catch (IOException e) {
79
            throw new AiSuggestionException("Cannot create work directory: " + workDir, e);
80
        }
81 2 1. <init> : removed conditional - replaced equality check with true → SURVIVED
2. <init> : removed conditional - replaced equality check with false → KILLED
        if (!responseDir.equals(workDir)) {
82
            try {
83
                Files.createDirectories(responseDir);
84
            } catch (IOException e) {
85
                throw new AiSuggestionException("Cannot create response directory: " + responseDir, e);
86
            }
87
        }
88
    }
89
90
    /**
91
     * Builds and writes the work file for the specified test class, and
92
     * pre-creates an empty response file alongside it.
93
     *
94
     * <p>
95
     * The work file contains operator instructions at the top followed by the full
96
     * AI prompt. The prompt is built using {@link PromptBuilder#build} and embeds
97
     * the complete class source so the operator can paste the entire block into
98
     * their AI chat window without attaching any files separately.
99
     * </p>
100
     *
101
     * <p>
102
     * File names are derived from {@code fileStem} (a dot-separated path identifier
103
     * based on the source file's location relative to the scan root) rather than the
104
     * FQCN. This ensures uniqueness in multi-module projects where the same FQCN
105
     * may appear in multiple modules. For a standard Maven source root
106
     * (e.g. {@code src/test/java}), the stem is identical to the FQCN.
107
     * </p>
108
     *
109
     * <p>
110
     * An empty {@code <fileStem>.response.txt} file is also written to the response
111
     * directory so the operator only needs to paste the AI response into the
112
     * pre-existing file rather than creating it manually. If the response file
113
     * already contains content (e.g. from a previous run) it is left untouched.
114
     * </p>
115
     *
116
     * @param fileStem      dot-separated path stem used as the base name for the
117
     *                      work file and response stub; derived from the source
118
     *                      file path relative to the scan root
119
     * @param fqcn          fully qualified class name of the test class; used in
120
     *                      the work file content and AI prompt
121
     * @param classSource   complete source code of the test class
122
     * @param targetMethods deterministically discovered JUnit test methods to
123
     *                      classify
124
     * @return path of the written work file
125
     * @throws AiSuggestionException if the work file or the empty response file
126
     *                               cannot be written
127
     */
128
    public Path prepare(String fileStem, String fqcn, String classSource,
129
            List<PromptBuilder.TargetMethod> targetMethods) throws AiSuggestionException {
130
        String prompt = PromptBuilder.build(fqcn, classSource, taxonomyText, targetMethods, confidence);
131
        Path outputFile = workDir.resolve(fileStem + ".txt");
132
        String content = buildFileContent(fileStem, fqcn, prompt);
133
134
        try {
135
            Files.writeString(outputFile, content, StandardCharsets.UTF_8);
136
        } catch (IOException e) {
137
            throw new AiSuggestionException("Failed to write work file: " + outputFile, e);
138
        }
139
140
        Path responseFile = responseDir.resolve(fileStem + ".response.txt");
141 2 1. prepare : removed conditional - replaced equality check with false → KILLED
2. prepare : removed conditional - replaced equality check with true → KILLED
        if (!Files.exists(responseFile)) {
142
            try {
143
                Files.writeString(responseFile, "", StandardCharsets.UTF_8);
144
            } catch (IOException e) {
145
                throw new AiSuggestionException("Failed to create response file: " + responseFile, e);
146
            }
147
        }
148
149 1 1. prepare : replaced return value with null for org/egothor/methodatlas/ai/ManualPrepareEngine::prepare → KILLED
        return outputFile;
150
    }
151
152
    private static String buildFileContent(String fileStem, String fqcn, String prompt) {
153
        String responseFileName = fileStem + ".response.txt";
154 1 1. buildFileContent : replaced return value with "" for org/egothor/methodatlas/ai/ManualPrepareEngine::buildFileContent → KILLED
        return SEPARATOR + "\n"
155
                + "OPERATOR INSTRUCTIONS\n"
156
                + SEPARATOR + "\n"
157
                + "Class      : " + fqcn + "\n"
158
                + "Work file  : " + fileStem + ".txt\n"
159
                + "Response   : " + responseFileName + "\n"
160
                + "\n"
161
                + "Steps:\n"
162
                + "  1. Copy the AI PROMPT block below (between the BEGIN/END markers)\n"
163
                + "     into your AI chat window.\n"
164
                + "  2. Wait for the AI to respond.\n"
165
                + "  3. Paste the complete AI response into the pre-created stub file:\n"
166
                + "       " + responseFileName + "\n"
167
                + "     (created empty in the response directory — do not rename it).\n"
168
                + "  4. Repeat for all other work files.\n"
169
                + "  5. After all responses are saved, run the consume phase:\n"
170
                + "       java -jar methodatlas.jar -manual-consume <workdir> <responsedir> <source-roots...>\n"
171
                + SEPARATOR + "\n"
172
                + "\n"
173
                + "--- BEGIN AI PROMPT ---\n"
174
                + prompt
175
                + "--- END AI PROMPT ---\n";
176
    }
177
178
    private static String loadTaxonomy(AiOptions options) throws AiSuggestionException {
179 2 1. loadTaxonomy : removed conditional - replaced equality check with false → KILLED
2. loadTaxonomy : removed conditional - replaced equality check with true → KILLED
        if (options.taxonomyFile() != null) {
180
            try {
181 1 1. loadTaxonomy : replaced return value with "" for org/egothor/methodatlas/ai/ManualPrepareEngine::loadTaxonomy → KILLED
                return Files.readString(options.taxonomyFile());
182
            } catch (IOException e) {
183
                throw new AiSuggestionException("Failed to read taxonomy file: " + options.taxonomyFile(), e);
184
            }
185
        }
186 2 1. loadTaxonomy : replaced return value with "" for org/egothor/methodatlas/ai/ManualPrepareEngine::loadTaxonomy → SURVIVED
2. loadTaxonomy : Changed switch default to be first case → KILLED
        return switch (options.taxonomyMode()) {
187
            case DEFAULT -> DefaultSecurityTaxonomy.text();
188
            case OPTIMIZED -> OptimizedSecurityTaxonomy.text();
189
        };
190
    }
191
}

Mutations

81

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

2.2
Location : <init>
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

141

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

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

149

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

154

1.1
Location : buildFileContent
Killed by : org.egothor.methodatlas.ai.ManualPrepareEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualPrepareEngineTest]/[method:prepare_usesExternalTaxonomyFileWhenConfigured(java.nio.file.Path)]
replaced return value with "" for org/egothor/methodatlas/ai/ManualPrepareEngine::buildFileContent → KILLED

179

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

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

181

1.1
Location : loadTaxonomy
Killed by : org.egothor.methodatlas.ai.ManualPrepareEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualPrepareEngineTest]/[method:prepare_usesExternalTaxonomyFileWhenConfigured(java.nio.file.Path)]
replaced return value with "" for org/egothor/methodatlas/ai/ManualPrepareEngine::loadTaxonomy → KILLED

186

1.1
Location : loadTaxonomy
Killed by : none
replaced return value with "" for org/egothor/methodatlas/ai/ManualPrepareEngine::loadTaxonomy → SURVIVED
Covering tests

2.2
Location : loadTaxonomy
Killed by : org.egothor.methodatlas.ai.ManualPrepareEngineTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.ManualPrepareEngineTest]/[method:prepare_supportsSameDirForWorkAndResponse(java.nio.file.Path)]
Changed switch default to be first case → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1