| 1 | package org.egothor.methodatlas.ai; | |
| 2 | ||
| 3 | import java.util.List; | |
| 4 | import java.util.Objects; | |
| 5 | import java.util.stream.Collectors; | |
| 6 | ||
| 7 | /** | |
| 8 | * Utility responsible for constructing the prompt supplied to AI providers for | |
| 9 | * security classification of JUnit test classes. | |
| 10 | * | |
| 11 | * <p> | |
| 12 | * The prompt produced by this class combines several components into a single | |
| 13 | * instruction payload: | |
| 14 | * </p> | |
| 15 | * | |
| 16 | * <ul> | |
| 17 | * <li>classification instructions for the AI model</li> | |
| 18 | * <li>a controlled security taxonomy definition</li> | |
| 19 | * <li>strict output formatting rules</li> | |
| 20 | * <li>the fully qualified class name</li> | |
| 21 | * <li>the complete source code of the analyzed test class</li> | |
| 22 | * </ul> | |
| 23 | * | |
| 24 | * <p> | |
| 25 | * This revision keeps the full class source as semantic context but removes | |
| 26 | * method discovery from the AI model. The caller supplies the exact list of | |
| 27 | * JUnit test methods that must be classified, optionally with source line | |
| 28 | * anchors. | |
| 29 | * </p> | |
| 30 | * | |
| 31 | * <p> | |
| 32 | * The resulting prompt is passed to the configured AI provider and instructs | |
| 33 | * the model to produce a deterministic JSON classification result describing | |
| 34 | * security relevance and taxonomy tags for individual test methods. | |
| 35 | * </p> | |
| 36 | * | |
| 37 | * <p> | |
| 38 | * The prompt enforces a closed taxonomy and strict JSON output rules to ensure | |
| 39 | * that the returned content can be parsed reliably by the application. | |
| 40 | * </p> | |
| 41 | * | |
| 42 | * <p> | |
| 43 | * This class is a non-instantiable utility holder. | |
| 44 | * </p> | |
| 45 | * | |
| 46 | * @see AiSuggestionEngine | |
| 47 | * @see AiProviderClient | |
| 48 | * @see DefaultSecurityTaxonomy | |
| 49 | * @see OptimizedSecurityTaxonomy | |
| 50 | */ | |
| 51 | public final class PromptBuilder { | |
| 52 | ||
| 53 | /** | |
| 54 | * Deterministically extracted test method descriptor supplied to the prompt. | |
| 55 | * | |
| 56 | * @param methodName name of the JUnit test method | |
| 57 | * @param beginLine first source line of the method, or {@code null} if unknown | |
| 58 | * @param endLine last source line of the method, or {@code null} if unknown | |
| 59 | */ | |
| 60 | public record TargetMethod(String methodName, Integer beginLine, Integer endLine) { | |
| 61 | public TargetMethod { | |
| 62 | Objects.requireNonNull(methodName, "methodName"); | |
| 63 | if (methodName.isBlank()) { | |
| 64 | throw new IllegalArgumentException("methodName must not be blank"); | |
| 65 | } | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | /** | |
| 70 | * Prevents instantiation of this utility class. | |
| 71 | */ | |
| 72 | private PromptBuilder() { | |
| 73 | } | |
| 74 | ||
| 75 | /** | |
| 76 | * Builds the complete prompt supplied to an AI provider for security | |
| 77 | * classification of a JUnit test class. | |
| 78 | * | |
| 79 | * <p> | |
| 80 | * The generated prompt contains: | |
| 81 | * </p> | |
| 82 | * | |
| 83 | * <ul> | |
| 84 | * <li>task instructions describing the classification objective</li> | |
| 85 | * <li>the security taxonomy definition controlling allowed tags</li> | |
| 86 | * <li>the exact list of target test methods to classify</li> | |
| 87 | * <li>strict output rules enforcing JSON-only responses</li> | |
| 88 | * <li>a formal JSON schema describing the expected result structure</li> | |
| 89 | * <li>the fully qualified class name of the analyzed test class</li> | |
| 90 | * <li>the complete class source used as analysis input</li> | |
| 91 | * </ul> | |
| 92 | * | |
| 93 | * <p> | |
| 94 | * The taxonomy text supplied to this method is typically obtained from either | |
| 95 | * {@link DefaultSecurityTaxonomy#text()} or | |
| 96 | * {@link OptimizedSecurityTaxonomy#text()}, depending on the selected | |
| 97 | * {@link AiOptions.TaxonomyMode}. | |
| 98 | * </p> | |
| 99 | * | |
| 100 | * <p> | |
| 101 | * The returned prompt is intended to be used as the content of a user message | |
| 102 | * in chat-based inference APIs. | |
| 103 | * </p> | |
| 104 | * | |
| 105 | * @param fqcn fully qualified class name of the test class being | |
| 106 | * analyzed | |
| 107 | * @param classSource complete source code of the test class | |
| 108 | * @param taxonomyText taxonomy definition guiding classification | |
| 109 | * @param targetMethods exact list of deterministically discovered JUnit test | |
| 110 | * methods to classify | |
| 111 | * @param confidence when {@code true}, the prompt instructs the AI to | |
| 112 | * include a {@code confidence} score for each method | |
| 113 | * classification | |
| 114 | * @return formatted prompt supplied to the AI provider | |
| 115 | * | |
| 116 | * @see AiSuggestionEngine#suggestForClass(String, String, String, List) | |
| 117 | */ | |
| 118 | public static String build(String fqcn, String classSource, String taxonomyText, List<TargetMethod> targetMethods, | |
| 119 | boolean confidence) { | |
| 120 | Objects.requireNonNull(fqcn, "fqcn"); | |
| 121 | Objects.requireNonNull(classSource, "classSource"); | |
| 122 | Objects.requireNonNull(taxonomyText, "taxonomyText"); | |
| 123 | Objects.requireNonNull(targetMethods, "targetMethods"); | |
| 124 | ||
| 125 |
2
1. build : removed conditional - replaced equality check with true → KILLED 2. build : removed conditional - replaced equality check with false → KILLED |
if (targetMethods.isEmpty()) { |
| 126 | throw new IllegalArgumentException("targetMethods must not be empty"); | |
| 127 | } | |
| 128 | ||
| 129 | String targetMethodBlock = targetMethods.stream().map(PromptBuilder::formatTargetMethod) | |
| 130 | .collect(Collectors.joining("\n")); | |
| 131 | ||
| 132 | String expectedMethodNames = targetMethods.stream().map(TargetMethod::methodName) | |
| 133 |
1
1. lambda$build$0 : replaced return value with "" for org/egothor/methodatlas/ai/PromptBuilder::lambda$build$0 → KILLED |
.map(name -> "\"" + name + "\"").collect(Collectors.joining(", ")); |
| 134 | ||
| 135 |
2
1. build : removed conditional - replaced equality check with false → KILLED 2. build : removed conditional - replaced equality check with true → KILLED |
String confidenceOutputRules = confidence |
| 136 | ? "\n- confidence must be a decimal between 0.0 and 1.0 (one decimal place is sufficient).\n" | |
| 137 | + " Use these anchor points when setting it:\n" | |
| 138 | + " 1.0 \u2014 the method name and body explicitly and unambiguously test a named\n" | |
| 139 | + " security property (authentication, authorisation, encryption,\n" | |
| 140 | + " injection prevention, access control, etc.)\n" | |
| 141 | + " 0.7 \u2014 the method clearly tests a security-adjacent concern but the mapping\n" | |
| 142 | + " requires inference from context, class name, or surrounding code\n" | |
| 143 | + " 0.5 \u2014 the classification is plausible; the method name or body is equally\n" | |
| 144 | + " consistent with a non-security interpretation\n" | |
| 145 | + " Prefer securityRelevant=false rather than returning a confidence value below\n" | |
| 146 | + " 0.5. When securityRelevant=false, set confidence to 0.0." | |
| 147 | : ""; | |
| 148 |
2
1. build : removed conditional - replaced equality check with false → KILLED 2. build : removed conditional - replaced equality check with true → KILLED |
String confidenceJsonField = confidence ? "\n \"confidence\": 0.9," : ""; |
| 149 | ||
| 150 |
1
1. build : replaced return value with "" for org/egothor/methodatlas/ai/PromptBuilder::build → KILLED |
return """ |
| 151 | You are analyzing a single JUnit 5 test class and suggesting security tags. | |
| 152 | ||
| 153 | TASK | |
| 154 | - Analyze the WHOLE class for context. | |
| 155 | - Classify ONLY the methods explicitly listed in TARGET TEST METHODS. | |
| 156 | - Do not invent methods that do not exist. | |
| 157 | - Do not classify helper methods, lifecycle methods, nested classes, or any method not listed. | |
| 158 | - Be conservative. | |
| 159 | - If uncertain, classify the method as securityRelevant=false. | |
| 160 | - Ignore pure functional / performance / UX tests unless they explicitly validate a security property. | |
| 161 | ||
| 162 | CONTROLLED TAXONOMY | |
| 163 | %s | |
| 164 | ||
| 165 | TARGET TEST METHODS | |
| 166 | The following methods were extracted deterministically by the parser and are the ONLY methods | |
| 167 | you are allowed to classify. Use the full class source only as context for understanding them. | |
| 168 | ||
| 169 | %s | |
| 170 | ||
| 171 | OUTPUT RULES | |
| 172 | - Return JSON only. | |
| 173 | - No markdown. | |
| 174 | - No prose outside JSON. | |
| 175 | - Return exactly one result for each target method. | |
| 176 | - methodName values in the output must exactly match one of: | |
| 177 | [%s] | |
| 178 | - Do not omit any listed method. | |
| 179 | - Do not include any additional methods. | |
| 180 | - Tags must come only from this closed set: | |
| 181 | security, auth, access-control, crypto, input-validation, injection, data-protection, logging, error-handling, owasp | |
| 182 | - If securityRelevant=true, tags MUST include "security". | |
| 183 | - Add 1-3 tags total per method. | |
| 184 | - If securityRelevant=false, displayName must be null. | |
| 185 | - If securityRelevant=false, tags must be []. | |
| 186 | - If securityRelevant=true, displayName must match: | |
| 187 | SECURITY: <control/property> - <scenario> | |
| 188 | - reason should be short and specific. | |
| 189 | - interactionScore must be a decimal between 0.0 and 1.0 (one decimal place is sufficient). | |
| 190 | It measures what fraction of this test's assertions only verify *interactions* (that | |
| 191 | methods were called, in what order, with what arguments) rather than *outcomes* (return | |
| 192 | values, computed state, thrown exceptions, or observable side effects). | |
| 193 | Use these anchor points: | |
| 194 | 1.0 — EVERY assertion is an interaction check (e.g. verify() only); NO assertion | |
| 195 | verifies any return value, output field, database row, or observable outcome. | |
| 196 | 0.0 — ALL assertions verify actual outputs or state; no interaction-only checks. | |
| 197 | 0.5 — mixed: some real-output assertions alongside interaction checks. | |
| 198 | Score 1.0 only when there is NO assertion on any return value, state change, or | |
| 199 | observable outcome. A test that has even one meaningful output assertion scores ≤ 0.5. | |
| 200 | This applies regardless of testing framework (Mockito, EasyMock, WireMock, etc.).%s | |
| 201 | ||
| 202 | JSON SHAPE | |
| 203 | { | |
| 204 | "className": "string", | |
| 205 | "classSecurityRelevant": true, | |
| 206 | "classTags": ["security", "crypto"], | |
| 207 | "classReason": "string", | |
| 208 | "methods": [ | |
| 209 | { | |
| 210 | "methodName": "string", | |
| 211 | "securityRelevant": true, | |
| 212 | "displayName": "SECURITY: ...", | |
| 213 | "tags": ["security", "crypto"],%s | |
| 214 | "reason": "string", | |
| 215 | "interactionScore": 0.0 | |
| 216 | } | |
| 217 | ] | |
| 218 | } | |
| 219 | ||
| 220 | CLASS | |
| 221 | FQCN: %s | |
| 222 | ||
| 223 | SOURCE | |
| 224 | %s | |
| 225 | """ | |
| 226 | .formatted(taxonomyText, targetMethodBlock, expectedMethodNames, | |
| 227 | confidenceOutputRules, confidenceJsonField, fqcn, classSource); | |
| 228 | } | |
| 229 | ||
| 230 | private static String formatTargetMethod(TargetMethod targetMethod) { | |
| 231 | StringBuilder builder = new StringBuilder("- ").append(targetMethod.methodName()); | |
| 232 | ||
| 233 |
4
1. formatTargetMethod : removed conditional - replaced equality check with false → SURVIVED 2. formatTargetMethod : removed conditional - replaced equality check with true → KILLED 3. formatTargetMethod : removed conditional - replaced equality check with true → KILLED 4. formatTargetMethod : removed conditional - replaced equality check with false → KILLED |
if (targetMethod.beginLine() != null || targetMethod.endLine() != null) { |
| 234 |
2
1. formatTargetMethod : removed conditional - replaced equality check with false → SURVIVED 2. formatTargetMethod : removed conditional - replaced equality check with true → KILLED |
builder.append(" [lines ").append(targetMethod.beginLine() == null ? "?" : targetMethod.beginLine()) |
| 235 |
2
1. formatTargetMethod : removed conditional - replaced equality check with true → KILLED 2. formatTargetMethod : removed conditional - replaced equality check with false → KILLED |
.append('-').append(targetMethod.endLine() == null ? "?" : targetMethod.endLine()).append(']'); |
| 236 | } | |
| 237 | ||
| 238 |
1
1. formatTargetMethod : replaced return value with "" for org/egothor/methodatlas/ai/PromptBuilder::formatTargetMethod → KILLED |
return builder.toString(); |
| 239 | } | |
| 240 | } | |
Mutations | ||
| 125 |
1.1 2.2 |
|
| 133 |
1.1 |
|
| 135 |
1.1 2.2 |
|
| 148 |
1.1 2.2 |
|
| 150 |
1.1 |
|
| 233 |
1.1 2.2 3.3 4.4 |
|
| 234 |
1.1 2.2 |
|
| 235 |
1.1 2.2 |
|
| 238 |
1.1 |