| 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 2.2 |
|
| 141 |
1.1 2.2 |
|
| 149 |
1.1 |
|
| 154 |
1.1 |
|
| 179 |
1.1 2.2 |
|
| 181 |
1.1 |
|
| 186 |
1.1 2.2 |