Skip to content

Commit 2d389aa

Browse files
committed
feat: Implement deterministic sweep for unresolved issues and enhance AI reconciliation
- Added a deterministic sweep method in BranchIssueReconciliationService to resolve unresolved issues based on reliable content anchors. - Updated BranchAnalysisProcessor to invoke the new sweep method after reanalyzing candidate issues. - Enhanced AI reconciliation requests in VcsAiClientService and its implementations to include relevant diff context for improved analysis. - Modified prompt builder to incorporate recent changes from diffs into prompts for better context during issue resolution. - Updated tests to verify new functionality and ensure proper integration of changes across services.
1 parent cb68e14 commit 2d389aa

11 files changed

Lines changed: 557 additions & 29 deletions

File tree

java-ecosystem/libs/analysis-engine/src/main/java/org/rostilos/codecrow/analysisengine/processor/analysis/BranchAnalysisProcessor.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,17 @@ public Map<String, Object> process(
240240

241241
branchIssueReconciliationService.reanalyzeCandidateIssues(
242242
changedFiles, existingFiles, refreshedBranch, project,
243-
request, consumer, archiveContents);
243+
request, consumer, archiveContents, rawDiff);
244+
245+
// ── Deterministic sweep: catch stale issues in non-diff files ────
246+
// The normal reconciliation above only checks files in the diff.
247+
// The sweep checks ALL remaining unresolved issues that have reliable
248+
// content anchors (codeSnippet/lineHash). Zero AI cost.
249+
int sweptCount = branchIssueReconciliationService.sweepDeterministicResolutions(
250+
changedFiles, refreshedBranch, project, request, archiveContents);
251+
if (sweptCount > 0) {
252+
refreshedBranch = refreshAndSaveIssueCounts(refreshedBranch);
253+
}
244254

245255
branchFileOperationsService.updateFileSnapshotsForBranch(existingFiles, project, request,
246256
archiveContents);

java-ecosystem/libs/analysis-engine/src/main/java/org/rostilos/codecrow/analysisengine/service/branch/BranchIssueReconciliationService.java

Lines changed: 329 additions & 11 deletions
Large diffs are not rendered by default.

java-ecosystem/libs/analysis-engine/src/main/java/org/rostilos/codecrow/analysisengine/service/vcs/VcsAiClientService.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,37 @@ default List<AiAnalysisRequest> buildAiAnalysisRequestsForBranchReconciliation(
106106
AnalysisProcessRequest request,
107107
List<AiRequestPreviousIssueDTO> previousIssues,
108108
java.util.Map<String, String> fileContents) throws GeneralSecurityException {
109+
return buildAiAnalysisRequestsForBranchReconciliation(project, request, previousIssues, fileContents, null);
110+
}
111+
112+
/**
113+
* Builds AI analysis requests for branch reconciliation with pre-fetched file
114+
* contents and relevant diff context.
115+
* <p>
116+
* The {@code relevantDiff} parameter, when provided, contains the per-file
117+
* diffs for files that have issues going to AI reconciliation. This gives the
118+
* LLM "before → after" context so it can recognise when a fix has been applied
119+
* even if the resulting code still looks similar to the original.
120+
*
121+
* @param project The project being analyzed
122+
* @param request The analysis process request (must be a
123+
* BranchProcessRequest)
124+
* @param previousIssues Pre-built DTOs describing the issues to reconcile
125+
* @param fileContents Map of filePath → full file content (pre-fetched by
126+
* Java)
127+
* @param relevantDiff Filtered diff containing only per-file diffs for files
128+
* with issues (may be null)
129+
* @return List of AI analysis requests ready to be sent to the AI client
130+
*/
131+
default List<AiAnalysisRequest> buildAiAnalysisRequestsForBranchReconciliation(
132+
Project project,
133+
AnalysisProcessRequest request,
134+
List<AiRequestPreviousIssueDTO> previousIssues,
135+
java.util.Map<String, String> fileContents,
136+
String relevantDiff) throws GeneralSecurityException {
109137
// Default: delegate to standard method without previous issues.
110-
// Providers should override to inject previousIssues and fileContents into the
111-
// builder.
138+
// Providers should override to inject previousIssues, fileContents and
139+
// relevantDiff into the builder.
112140
return buildAiAnalysisRequests(project, request, Optional.empty());
113141
}
114142

java-ecosystem/libs/analysis-engine/src/test/java/org/rostilos/codecrow/analysisengine/processor/analysis/BranchAnalysisProcessorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ void shouldCompleteFullHappyPath() throws Exception {
413413
verify(branchIssueMappingService).mapCodeAnalysisIssuesToBranch(anySet(), anySet(), eq(savedBranch), eq(project));
414414
verify(branchIssueReconciliationService).reconcileIssueLineNumbers(eq(rawDiff), anySet(), eq(savedBranch));
415415
verify(branchIssueReconciliationService).reanalyzeCandidateIssues(
416-
anySet(), anySet(), eq(savedBranch), eq(project), eq(request), eq(consumer), eq(archiveContents));
416+
anySet(), anySet(), eq(savedBranch), eq(project), eq(request), eq(consumer), eq(archiveContents), eq(rawDiff));
417417
verify(branchFileOperationsService).updateFileSnapshotsForBranch(anySet(), eq(project), eq(request), eq(archiveContents));
418418
verify(branchIssueReconciliationService).verifyIssueLineNumbersWithSnippets(anySet(), eq(project), any());
419419
verify(analysisLockService).releaseLock("lock-key");

java-ecosystem/libs/core/src/main/java/org/rostilos/codecrow/core/persistence/repository/branch/BranchIssueRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ default List<BranchIssue> findByCodeAnalysisIssueId(Long codeAnalysisIssueId) {
5858
@Query("DELETE FROM BranchIssue bi WHERE bi.branch.id IN (SELECT b.id FROM Branch b WHERE b.project.id = :projectId)")
5959
void deleteByProjectId(@Param("projectId") Long projectId);
6060

61+
// ── Branch-scoped unresolved query ───────────────────────────────────
62+
63+
@Query("SELECT bi FROM BranchIssue bi " +
64+
"WHERE bi.branch.id = :branchId " +
65+
"AND bi.resolved = false")
66+
List<BranchIssue> findAllUnresolvedByBranchId(@Param("branchId") Long branchId);
67+
6168
// ── File-scoped queries (use BranchIssue's own filePath) ────────────
6269

6370
@Query("SELECT bi FROM BranchIssue bi " +

java-ecosystem/services/pipeline-agent/src/main/java/org/rostilos/codecrow/pipelineagent/bitbucket/service/BitbucketAiClientService.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,18 @@ public List<AiAnalysisRequest> buildAiAnalysisRequestsForBranchReconciliation(
342342
AnalysisProcessRequest request,
343343
List<AiRequestPreviousIssueDTO> previousIssues,
344344
java.util.Map<String, String> fileContents) throws GeneralSecurityException {
345+
return buildAiAnalysisRequestsForBranchReconciliation(project, request, previousIssues, fileContents, null);
346+
}
347+
348+
@Override
349+
public List<AiAnalysisRequest> buildAiAnalysisRequestsForBranchReconciliation(
350+
Project project,
351+
AnalysisProcessRequest request,
352+
List<AiRequestPreviousIssueDTO> previousIssues,
353+
java.util.Map<String, String> fileContents,
354+
String relevantDiff) throws GeneralSecurityException {
345355
BranchProcessRequest branchReq = (BranchProcessRequest) request;
346-
return List.of(buildBranchAnalysisRequestInternal(project, branchReq, null, previousIssues, fileContents));
356+
return List.of(buildBranchAnalysisRequestInternal(project, branchReq, null, previousIssues, fileContents, relevantDiff));
347357
}
348358

349359
@Override
@@ -357,6 +367,15 @@ public List<AiAnalysisRequest> buildDirectPushAnalysisRequests(
357367
return List.of(buildDirectPushAnalysisRequestInternal(project, branchReq, rawDiff, fileContents, changedFiles));
358368
}
359369

370+
private AiAnalysisRequest buildBranchAnalysisRequestInternal(
371+
Project project,
372+
BranchProcessRequest request,
373+
Optional<CodeAnalysis> previousAnalysis,
374+
List<AiRequestPreviousIssueDTO> previousIssueDTOs,
375+
java.util.Map<String, String> fileContents) throws GeneralSecurityException {
376+
return buildBranchAnalysisRequestInternal(project, request, previousAnalysis, previousIssueDTOs, fileContents, null);
377+
}
378+
360379
/**
361380
* Internal builder for branch analysis requests.
362381
* Accepts EITHER a CodeAnalysis entity OR pre-built DTOs for previous issues.
@@ -368,7 +387,8 @@ private AiAnalysisRequest buildBranchAnalysisRequestInternal(
368387
BranchProcessRequest request,
369388
Optional<CodeAnalysis> previousAnalysis,
370389
List<AiRequestPreviousIssueDTO> previousIssueDTOs,
371-
java.util.Map<String, String> fileContents) throws GeneralSecurityException {
390+
java.util.Map<String, String> fileContents,
391+
String relevantDiff) throws GeneralSecurityException {
372392
VcsInfo vcsInfo = getVcsInfo(project);
373393
VcsConnection vcsConnection = vcsInfo.vcsConnection();
374394
AIConnection aiConnection = project.getAiBinding().getAiConnection();
@@ -405,6 +425,10 @@ private AiAnalysisRequest buildBranchAnalysisRequestInternal(
405425
builder.withReconciliationFileContents(fileContents);
406426
}
407427

428+
if (relevantDiff != null && !relevantDiff.isBlank()) {
429+
builder.withRawDiff(relevantDiff);
430+
}
431+
408432
return builder.build();
409433
}
410434

java-ecosystem/services/pipeline-agent/src/main/java/org/rostilos/codecrow/pipelineagent/github/service/GitHubAiClientService.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,18 @@ public List<AiAnalysisRequest> buildAiAnalysisRequestsForBranchReconciliation(
324324
AnalysisProcessRequest request,
325325
List<AiRequestPreviousIssueDTO> previousIssues,
326326
java.util.Map<String, String> fileContents) throws GeneralSecurityException {
327+
return buildAiAnalysisRequestsForBranchReconciliation(project, request, previousIssues, fileContents, null);
328+
}
329+
330+
@Override
331+
public List<AiAnalysisRequest> buildAiAnalysisRequestsForBranchReconciliation(
332+
Project project,
333+
AnalysisProcessRequest request,
334+
List<AiRequestPreviousIssueDTO> previousIssues,
335+
java.util.Map<String, String> fileContents,
336+
String relevantDiff) throws GeneralSecurityException {
327337
BranchProcessRequest branchReq = (BranchProcessRequest) request;
328-
return List.of(buildBranchAnalysisRequestInternal(project, branchReq, null, previousIssues, fileContents));
338+
return List.of(buildBranchAnalysisRequestInternal(project, branchReq, null, previousIssues, fileContents, relevantDiff));
329339
}
330340

331341
@Override
@@ -339,6 +349,15 @@ public List<AiAnalysisRequest> buildDirectPushAnalysisRequests(
339349
return List.of(buildDirectPushAnalysisRequestInternal(project, branchReq, rawDiff, fileContents, changedFiles));
340350
}
341351

352+
private AiAnalysisRequest buildBranchAnalysisRequestInternal(
353+
Project project,
354+
BranchProcessRequest request,
355+
Optional<CodeAnalysis> previousAnalysis,
356+
List<AiRequestPreviousIssueDTO> previousIssueDTOs,
357+
java.util.Map<String, String> fileContents) throws GeneralSecurityException {
358+
return buildBranchAnalysisRequestInternal(project, request, previousAnalysis, previousIssueDTOs, fileContents, null);
359+
}
360+
342361
/**
343362
* Internal builder for branch analysis requests.
344363
* Accepts EITHER a CodeAnalysis entity OR pre-built DTOs for previous issues.
@@ -350,7 +369,8 @@ private AiAnalysisRequest buildBranchAnalysisRequestInternal(
350369
BranchProcessRequest request,
351370
Optional<CodeAnalysis> previousAnalysis,
352371
List<AiRequestPreviousIssueDTO> previousIssueDTOs,
353-
java.util.Map<String, String> fileContents) throws GeneralSecurityException {
372+
java.util.Map<String, String> fileContents,
373+
String relevantDiff) throws GeneralSecurityException {
354374
VcsInfo vcsInfo = getVcsInfo(project);
355375
VcsConnection vcsConnection = vcsInfo.vcsConnection();
356376
AIConnection aiConnection = project.getAiBinding().getAiConnection();
@@ -387,6 +407,10 @@ private AiAnalysisRequest buildBranchAnalysisRequestInternal(
387407
builder.withReconciliationFileContents(fileContents);
388408
}
389409

410+
if (relevantDiff != null && !relevantDiff.isBlank()) {
411+
builder.withRawDiff(relevantDiff);
412+
}
413+
390414
return builder.build();
391415
}
392416

java-ecosystem/services/pipeline-agent/src/main/java/org/rostilos/codecrow/pipelineagent/gitlab/service/GitLabAiClientService.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,18 @@ public List<AiAnalysisRequest> buildAiAnalysisRequestsForBranchReconciliation(
327327
AnalysisProcessRequest request,
328328
List<AiRequestPreviousIssueDTO> previousIssues,
329329
java.util.Map<String, String> fileContents) throws GeneralSecurityException {
330+
return buildAiAnalysisRequestsForBranchReconciliation(project, request, previousIssues, fileContents, null);
331+
}
332+
333+
@Override
334+
public List<AiAnalysisRequest> buildAiAnalysisRequestsForBranchReconciliation(
335+
Project project,
336+
AnalysisProcessRequest request,
337+
List<AiRequestPreviousIssueDTO> previousIssues,
338+
java.util.Map<String, String> fileContents,
339+
String relevantDiff) throws GeneralSecurityException {
330340
BranchProcessRequest branchReq = (BranchProcessRequest) request;
331-
return List.of(buildBranchAnalysisRequestInternal(project, branchReq, null, previousIssues, fileContents));
341+
return List.of(buildBranchAnalysisRequestInternal(project, branchReq, null, previousIssues, fileContents, relevantDiff));
332342
}
333343

334344
@Override
@@ -342,6 +352,15 @@ public List<AiAnalysisRequest> buildDirectPushAnalysisRequests(
342352
return List.of(buildDirectPushAnalysisRequestInternal(project, branchReq, rawDiff, fileContents, changedFiles));
343353
}
344354

355+
private AiAnalysisRequest buildBranchAnalysisRequestInternal(
356+
Project project,
357+
BranchProcessRequest request,
358+
Optional<CodeAnalysis> previousAnalysis,
359+
List<AiRequestPreviousIssueDTO> previousIssueDTOs,
360+
java.util.Map<String, String> fileContents) throws GeneralSecurityException {
361+
return buildBranchAnalysisRequestInternal(project, request, previousAnalysis, previousIssueDTOs, fileContents, null);
362+
}
363+
345364
/**
346365
* Internal builder for branch analysis requests.
347366
* Accepts EITHER a CodeAnalysis entity OR pre-built DTOs for previous issues.
@@ -353,7 +372,8 @@ private AiAnalysisRequest buildBranchAnalysisRequestInternal(
353372
BranchProcessRequest request,
354373
Optional<CodeAnalysis> previousAnalysis,
355374
List<AiRequestPreviousIssueDTO> previousIssueDTOs,
356-
java.util.Map<String, String> fileContents) throws GeneralSecurityException {
375+
java.util.Map<String, String> fileContents,
376+
String relevantDiff) throws GeneralSecurityException {
357377
VcsInfo vcsInfo = getVcsInfo(project);
358378
VcsConnection vcsConnection = vcsInfo.vcsConnection();
359379
AIConnection aiConnection = project.getAiBinding().getAiConnection();
@@ -390,6 +410,10 @@ private AiAnalysisRequest buildBranchAnalysisRequestInternal(
390410
builder.withReconciliationFileContents(fileContents);
391411
}
392412

413+
if (relevantDiff != null && !relevantDiff.isBlank()) {
414+
builder.withRawDiff(relevantDiff);
415+
}
416+
393417
return builder.build();
394418
}
395419

python-ecosystem/inference-orchestrator/service/review/orchestrator/orchestrator.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ async def execute_batched_branch_analysis(
183183
batches = self._split_issues_into_batches(all_issues)
184184
total_batches = len(batches)
185185

186+
# Extract raw diff from request (per-file diffs for AI-bound files,
187+
# pre-filtered by Java)
188+
raw_diff: Optional[str] = getattr(request, 'rawDiff', None)
189+
186190
if total_batches == 1:
187191
# Fast path — single batch, no overhead
188192
logger.info(
@@ -191,7 +195,7 @@ async def execute_batched_branch_analysis(
191195
if file_contents:
192196
# MCP-free direct path
193197
prompt = PromptBuilder.build_branch_reconciliation_direct_prompt(
194-
pr_metadata, file_contents
198+
pr_metadata, file_contents, raw_diff=raw_diff,
195199
)
196200
return await execute_branch_reconciliation_direct(
197201
self.llm, prompt, self.event_callback
@@ -248,9 +252,12 @@ async def execute_batched_branch_analysis(
248252
for fp, content in file_contents.items()
249253
if fp in batch_files
250254
}
255+
# Filter raw diff to only per-file diffs for this batch's files
256+
batch_diff = self._filter_diff_for_files(raw_diff, batch_files) if raw_diff else None
251257
prompt = PromptBuilder.build_branch_reconciliation_direct_prompt(
252258
batch_metadata, batch_file_contents,
253259
batch_number=idx, total_batches=total_batches,
260+
raw_diff=batch_diff,
254261
)
255262
result = await execute_branch_reconciliation_direct(
256263
self.llm, prompt, self.event_callback
@@ -288,6 +295,36 @@ async def execute_batched_branch_analysis(
288295
)
289296
return {"issues": merged_issues, "comment": summary}
290297

298+
@staticmethod
299+
def _filter_diff_for_files(
300+
raw_diff: str, file_paths: set
301+
) -> Optional[str]:
302+
"""
303+
Filter a unified diff to include only hunks for the given file paths.
304+
Returns None if no relevant hunks are found.
305+
"""
306+
import re
307+
if not raw_diff or not file_paths:
308+
return None
309+
310+
# Split diff into per-file sections using diff header pattern
311+
# Each section starts with "diff --git a/... b/..."
312+
sections = re.split(r'(?=^diff --git )', raw_diff, flags=re.MULTILINE)
313+
relevant = []
314+
315+
for section in sections:
316+
if not section.strip():
317+
continue
318+
# Extract file path from diff header: "diff --git a/path b/path"
319+
header_match = re.match(r'diff --git a/(.+?) b/(.+?)(?:\n|$)', section)
320+
if header_match:
321+
a_path = header_match.group(1)
322+
b_path = header_match.group(2)
323+
if a_path in file_paths or b_path in file_paths:
324+
relevant.append(section)
325+
326+
return "\n".join(relevant) if relevant else None
327+
291328
def _split_issues_into_batches(
292329
self, issues: List[Dict[str, Any]]
293330
) -> List[List[Dict[str, Any]]]:

0 commit comments

Comments
 (0)