Skip to content

Commit d5c9d64

Browse files
authored
Merge pull request #175 from rostilos/1.5.6-rc
feat: Enhance QA documentation process by supporting multi-PR accumulation and updating existing content
2 parents c4ea5a3 + e3a8aa0 commit d5c9d64

9 files changed

Lines changed: 243 additions & 67 deletions

File tree

java-ecosystem/libs/email/src/it/java/org/rostilos/codecrow/email/EmailDeliveryIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ private String extractFromMultipart(Multipart multipart) throws Exception {
8080
void shouldSendSimpleEmail() throws Exception {
8181
SimpleMailMessage message = new SimpleMailMessage();
8282
message.setTo("user@test.dev");
83-
message.setFrom("noreply@codecrow.dev");
83+
message.setFrom("noreply@codecrow.app");
8484
message.setSubject("Welcome to CodeCrow");
8585
message.setText("Your account has been created.");
8686

java-ecosystem/services/pipeline-agent/src/main/java/org/rostilos/codecrow/pipelineagent/generic/processor/command/QaDocCommandProcessor.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,28 @@ public WebhookResult process(
213213
e.getMessage());
214214
}
215215

216-
// 6. Generate QA documentation via inference orchestrator
216+
// 6. Check for existing QA doc comment on this task (for multi-PR accumulation)
217+
Optional<TaskComment> existingComment = client.findCommentByMarker(taskId, QaAutoDocListener.COMMENT_MARKER_PREFIX);
218+
String previousDocumentation = null;
219+
220+
if (existingComment.isPresent()) {
221+
String existingBody = existingComment.get().body();
222+
boolean isSamePrRerun = QaAutoDocListener.isCurrentPrAlreadyDocumented(existingBody, prNumber);
223+
224+
if (!isSamePrRerun) {
225+
// Different PR on the same task → pass previous doc to LLM for merging
226+
previousDocumentation = existingBody;
227+
log.info("qa-doc command: found existing comment for task {} from earlier PR(s) — will merge",
228+
taskId);
229+
} else {
230+
log.info("qa-doc command: re-analysis of same PR #{} — will overwrite", prNumber);
231+
}
232+
}
233+
234+
// 7. Generate QA documentation via inference orchestrator
217235
String qaDocument = qaDocGenerationService.generateQaDocumentation(
218-
project, prNumber, issuesFound, filesAnalyzed, prMetadata, qaConfig, taskDetails, analysis, diff);
236+
project, prNumber, issuesFound, filesAnalyzed, prMetadata, qaConfig, taskDetails, analysis, diff,
237+
previousDocumentation);
219238

220239
if (qaDocument == null || qaDocument.isBlank()) {
221240
return WebhookResult.success(
@@ -229,9 +248,8 @@ public WebhookResult process(
229248
"message", "Posting documentation to " + taskId + "..."
230249
));
231250

232-
// 7. Post or update comment on Jira task
251+
// 8. Post or update comment on Jira task
233252
String commentBody = QaAutoDocListener.COMMENT_MARKER + "\n\n" + qaDocument;
234-
Optional<TaskComment> existingComment = client.findCommentByMarker(taskId, QaAutoDocListener.COMMENT_MARKER);
235253

236254
if (existingComment.isPresent()) {
237255
client.updateComment(taskId, existingComment.get().commentId(), commentBody);

java-ecosystem/services/pipeline-agent/src/main/java/org/rostilos/codecrow/pipelineagent/qadoc/QaAutoDocListener.java

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public class QaAutoDocListener {
5858
/** Hidden marker embedded in auto-doc comments for detection/replacement. */
5959
public static final String COMMENT_MARKER = "<!-- codecrow-qa-autodoc -->";
6060

61+
/** Prefix of the PR-tracking marker variant, e.g. {@code <!-- codecrow-qa-autodoc:prs=42,57 -->}. */
62+
public static final String COMMENT_MARKER_PREFIX = "<!-- codecrow-qa-autodoc";
63+
6164
private final ProjectRepository projectRepository;
6265
private final TaskManagementConnectionRepository connectionRepository;
6366
private final TaskManagementClientFactory clientFactory;
@@ -196,20 +199,40 @@ private void processQaAutoDoc(AnalysisCompletedEvent event) throws Exception {
196199
taskDetails = null;
197200
}
198201

199-
// 5. Generate QA documentation via inference orchestrator
202+
// 5. Check for existing QA doc comment on this task (for multi-PR accumulation)
203+
Optional<TaskComment> existingComment = client.findCommentByMarker(taskId, COMMENT_MARKER_PREFIX);
204+
String previousDocumentation = null;
205+
boolean isSamePrRerun = false;
206+
207+
if (existingComment.isPresent()) {
208+
String existingBody = existingComment.get().body();
209+
// Detect if the current PR is already documented (same-PR re-analysis → overwrite)
210+
isSamePrRerun = isCurrentPrAlreadyDocumented(existingBody, event.getPrNumber());
211+
212+
if (!isSamePrRerun) {
213+
// Different PR on the same task → pass previous doc to LLM for merging
214+
previousDocumentation = existingBody;
215+
log.info("QA auto-doc: found existing comment for task {} from earlier PR(s) — will merge (commentId={})",
216+
taskId, existingComment.get().commentId());
217+
} else {
218+
log.info("QA auto-doc: re-analysis of same PR #{} — will overwrite existing comment",
219+
event.getPrNumber());
220+
}
221+
}
222+
223+
// 6. Generate QA documentation via inference orchestrator
200224
String qaDocument = qaDocGenerationService.generateQaDocumentation(
201-
project, event, qaConfig, taskDetails, analysis, diff);
225+
project, event, qaConfig, taskDetails, analysis, diff, previousDocumentation);
202226

203227
if (qaDocument == null || qaDocument.isBlank()) {
204228
log.info("QA auto-doc: LLM determined no documentation needed for task {}", taskId);
205229
return;
206230
}
207231

208-
// 6. Prepend marker for detection
232+
// 7. Prepend marker for detection
209233
String commentBody = COMMENT_MARKER + "\n\n" + qaDocument;
210234

211-
// 7. Post or update comment
212-
Optional<TaskComment> existingComment = client.findCommentByMarker(taskId, COMMENT_MARKER);
235+
// 8. Post or update comment
213236
if (existingComment.isPresent()) {
214237
client.updateComment(taskId, existingComment.get().commentId(), commentBody);
215238
log.info("QA auto-doc: updated existing comment on task {} (commentId={})",
@@ -260,4 +283,35 @@ private TaskManagementClient createClient(TaskManagementConnection conn) {
260283
creds.getOrDefault("apiToken", "")
261284
);
262285
}
286+
287+
/**
288+
* Check if the current PR is already documented in an existing QA doc comment.
289+
* <p>
290+
* Looks for the PR-tracking marker {@code <!-- codecrow-qa-autodoc:prs=42,57 -->}.
291+
* If the current PR number appears in the list, this is a re-analysis of the same PR
292+
* (e.g., new commits pushed) and we should overwrite rather than merge.
293+
*
294+
* @param commentBody the existing comment body (plain text extracted from ADF)
295+
* @param currentPrNumber the PR number being analyzed now
296+
* @return true if the current PR is already listed in the marker
297+
*/
298+
public static boolean isCurrentPrAlreadyDocumented(String commentBody, Long currentPrNumber) {
299+
if (commentBody == null || currentPrNumber == null) return false;
300+
// Match the PR-tracking marker: <!-- codecrow-qa-autodoc:prs=42,57 -->
301+
Pattern prMarkerPattern = Pattern.compile("<!-- codecrow-qa-autodoc:prs=([\\d,]+) -->");
302+
Matcher matcher = prMarkerPattern.matcher(commentBody);
303+
if (matcher.find()) {
304+
String prList = matcher.group(1);
305+
for (String pr : prList.split(",")) {
306+
try {
307+
if (Long.parseLong(pr.trim()) == currentPrNumber) {
308+
return true;
309+
}
310+
} catch (NumberFormatException ignored) {
311+
// skip malformed entries
312+
}
313+
}
314+
}
315+
return false;
316+
}
263317
}

java-ecosystem/services/pipeline-agent/src/main/java/org/rostilos/codecrow/pipelineagent/qadoc/QaDocGenerationService.java

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,10 @@ public QaDocGenerationService(
6969
* @param project the project
7070
* @param event the analysis completed event (contains metrics with issue count, files, etc.)
7171
* @param qaConfig the QA auto-doc configuration
72-
* @param taskDetails task details from the task management platform (may be null)
73-
* @param analysis the code analysis with issues eagerly loaded (may be null)
74-
* @param diff raw unified diff from the VCS platform (may be null)
72+
* @param taskDetails task details from the task management platform (may be null)
73+
* @param analysis the code analysis with issues eagerly loaded (may be null)
74+
* @param diff raw unified diff from the VCS platform (may be null)
75+
* @param previousDocumentation existing QA doc comment body from an earlier PR on the same task (may be null)
7576
* @return generated QA document text, or null if the LLM decided documentation isn't needed
7677
* @throws IOException if the inference orchestrator call fails after retries
7778
*/
@@ -80,8 +81,9 @@ public String generateQaDocumentation(Project project,
8081
QaAutoDocConfig qaConfig,
8182
TaskDetails taskDetails,
8283
CodeAnalysis analysis,
83-
String diff) throws IOException {
84-
Map<String, Object> payload = buildPayload(project, event, qaConfig, taskDetails, analysis, diff);
84+
String diff,
85+
String previousDocumentation) throws IOException {
86+
Map<String, Object> payload = buildPayload(project, event, qaConfig, taskDetails, analysis, diff, previousDocumentation);
8587
return callInferenceOrchestrator(payload);
8688
}
8789

@@ -94,10 +96,11 @@ public String generateQaDocumentation(Project project,
9496
* @param issuesFound number of issues found in the latest analysis (0 if none)
9597
* @param filesAnalyzed number of files analyzed (0 if none)
9698
* @param prMetadata a map with optional keys: sourceBranch, targetBranch, prTitle, prDescription
97-
* @param qaConfig the QA auto-doc configuration
98-
* @param taskDetails task details from the task management platform (may be null)
99-
* @param analysis the code analysis with issues eagerly loaded (may be null)
100-
* @param diff raw unified diff from the VCS platform (may be null)
99+
* @param qaConfig the QA auto-doc configuration
100+
* @param taskDetails task details from the task management platform (may be null)
101+
* @param analysis the code analysis with issues eagerly loaded (may be null)
102+
* @param diff raw unified diff from the VCS platform (may be null)
103+
* @param previousDocumentation existing QA doc comment body from an earlier PR on the same task (may be null)
101104
* @return generated QA document text, or null if the LLM decided documentation isn't needed
102105
* @throws IOException if the inference orchestrator call fails after retries
103106
*/
@@ -109,9 +112,10 @@ public String generateQaDocumentation(Project project,
109112
QaAutoDocConfig qaConfig,
110113
TaskDetails taskDetails,
111114
CodeAnalysis analysis,
112-
String diff) throws IOException {
115+
String diff,
116+
String previousDocumentation) throws IOException {
113117
Map<String, Object> payload = buildPayloadFromRaw(
114-
project, prNumber, issuesFound, filesAnalyzed, prMetadata, qaConfig, taskDetails, analysis, diff);
118+
project, prNumber, issuesFound, filesAnalyzed, prMetadata, qaConfig, taskDetails, analysis, diff, previousDocumentation);
115119
return callInferenceOrchestrator(payload);
116120
}
117121

@@ -178,7 +182,8 @@ private Map<String, Object> buildPayload(Project project,
178182
QaAutoDocConfig qaConfig,
179183
TaskDetails taskDetails,
180184
CodeAnalysis analysis,
181-
String diff) {
185+
String diff,
186+
String previousDocumentation) {
182187
Map<String, Object> payload = new LinkedHashMap<>();
183188
payload.put("project_id", project.getId());
184189
payload.put("project_name", project.getName());
@@ -195,6 +200,11 @@ private Map<String, Object> buildPayload(Project project,
195200
payload.put("diff", truncateDiff(diff));
196201
}
197202

203+
// Include previous documentation for multi-PR accumulation
204+
if (previousDocumentation != null && !previousDocumentation.isBlank()) {
205+
payload.put("previous_documentation", previousDocumentation);
206+
}
207+
198208
// Analysis metrics (includes sourceBranch, targetBranch, etc.)
199209
// Enrich with the full analysis summary from CodeAnalysis issues
200210
Map<String, Object> prMeta = event.getMetrics() != null
@@ -214,11 +224,11 @@ private Map<String, Object> buildPayload(Project project,
214224
payload.put("custom_template", qaConfig.customTemplate());
215225
}
216226

217-
// Task context (if available)
227+
// Task context (if available) — keys match Python placeholder names
218228
if (taskDetails != null) {
219229
Map<String, String> taskContext = new LinkedHashMap<>();
220-
taskContext.put("task_id", taskDetails.taskId());
221-
taskContext.put("summary", taskDetails.summary());
230+
taskContext.put("task_key", taskDetails.taskId());
231+
taskContext.put("task_summary", taskDetails.summary());
222232
taskContext.put("description", taskDetails.description());
223233
taskContext.put("status", taskDetails.status());
224234
taskContext.put("task_type", taskDetails.taskType());
@@ -259,7 +269,8 @@ private Map<String, Object> buildPayloadFromRaw(Project project,
259269
QaAutoDocConfig qaConfig,
260270
TaskDetails taskDetails,
261271
CodeAnalysis analysis,
262-
String diff) {
272+
String diff,
273+
String previousDocumentation) {
263274
Map<String, Object> payload = new LinkedHashMap<>();
264275
payload.put("project_id", project.getId());
265276
payload.put("project_name", project.getName());
@@ -275,6 +286,11 @@ private Map<String, Object> buildPayloadFromRaw(Project project,
275286
payload.put("diff", truncateDiff(diff));
276287
}
277288

289+
// Include previous documentation for multi-PR accumulation
290+
if (previousDocumentation != null && !previousDocumentation.isBlank()) {
291+
payload.put("previous_documentation", previousDocumentation);
292+
}
293+
278294
// Enrich pr_metadata with the full analysis summary from CodeAnalysis issues
279295
Map<String, Object> prMeta = prMetadata != null
280296
? new LinkedHashMap<>(prMetadata)
@@ -293,11 +309,11 @@ private Map<String, Object> buildPayloadFromRaw(Project project,
293309
payload.put("custom_template", qaConfig.customTemplate());
294310
}
295311

296-
// Task context (if available)
312+
// Task context (if available) — keys match Python placeholder names
297313
if (taskDetails != null) {
298314
Map<String, String> taskContext = new LinkedHashMap<>();
299-
taskContext.put("task_id", taskDetails.taskId());
300-
taskContext.put("summary", taskDetails.summary());
315+
taskContext.put("task_key", taskDetails.taskId());
316+
taskContext.put("task_summary", taskDetails.summary());
301317
taskContext.put("description", taskDetails.description());
302318
taskContext.put("status", taskDetails.status());
303319
taskContext.put("task_type", taskDetails.taskType());

0 commit comments

Comments
 (0)