PromptBuilder.java

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
Location : build
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_targetMethodWithNullLines_omitsLineRange()]
removed conditional - replaced equality check with true → KILLED

2.2
Location : build
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_rejectsEmptyTargetMethods()]
removed conditional - replaced equality check with false → KILLED

133

1.1
Location : lambda$build$0
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_includesExpectedMethodNamesConstraint()]
replaced return value with "" for org/egothor/methodatlas/ai/PromptBuilder::lambda$build$0 → KILLED

135

1.1
Location : build
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_withConfidence_includesConfidenceRulesAndJsonField()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : build
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_withoutConfidence_excludesConfidenceRulesAndJsonField()]
removed conditional - replaced equality check with true → KILLED

148

1.1
Location : build
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_withConfidence_includesConfidenceRulesAndJsonField()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : build
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_withoutConfidence_excludesConfidenceRulesAndJsonField()]
removed conditional - replaced equality check with true → KILLED

150

1.1
Location : build
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_targetMethodWithNullLines_omitsLineRange()]
replaced return value with "" for org/egothor/methodatlas/ai/PromptBuilder::build → KILLED

233

1.1
Location : formatTargetMethod
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_targetMethodWithNullLines_omitsLineRange()]
removed conditional - replaced equality check with true → KILLED

2.2
Location : formatTargetMethod
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_targetMethodWithOnlyBeginLine_showsQuestionMarkForEnd()]
removed conditional - replaced equality check with true → KILLED

3.3
Location : formatTargetMethod
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_targetMethodWithNullLines_omitsLineRange()]
removed conditional - replaced equality check with false → KILLED

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

234

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

2.2
Location : formatTargetMethod
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_targetMethodWithOnlyBeginLine_showsQuestionMarkForEnd()]
removed conditional - replaced equality check with true → KILLED

235

1.1
Location : formatTargetMethod
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_includesCompleteClassSourceVerbatim()]
removed conditional - replaced equality check with true → KILLED

2.2
Location : formatTargetMethod
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_targetMethodWithOnlyBeginLine_showsQuestionMarkForEnd()]
removed conditional - replaced equality check with false → KILLED

238

1.1
Location : formatTargetMethod
Killed by : org.egothor.methodatlas.ai.PromptBuilderTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.ai.PromptBuilderTest]/[method:build_targetMethodWithOnlyBeginLine_showsQuestionMarkForEnd()]
replaced return value with "" for org/egothor/methodatlas/ai/PromptBuilder::formatTargetMethod → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1