CommitAnalysisJob.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.internal.ui.views.mining;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.sandbox.jdt.triggerpattern.llm.AiRuleInferenceEngine;
import org.sandbox.jdt.triggerpattern.llm.CommitEvaluation;
import org.sandbox.jdt.triggerpattern.mining.analysis.CommitAnalysisResult.AnalysisStatus;
import org.sandbox.jdt.triggerpattern.mining.analysis.DiffHunk;
import org.sandbox.jdt.triggerpattern.mining.analysis.FileDiff;
import org.sandbox.jdt.triggerpattern.mining.git.GitHistoryProvider;
import org.sandbox.jdt.triggerpattern.mining.llm.EclipseLlmService;
/**
* Eclipse {@link Job} that analyzes a single commit in the background,
* using AI-powered inference to generate DSL rules from Java file changes.
*
* <p>When the LLM service is available, the job sends file diffs to the
* AI engine. Otherwise, the job completes without inferring any rules and
* records that no rules are available for the commit.</p>
*
* @since 1.2.6
*/
public class CommitAnalysisJob extends Job {
private final CommitTableEntry entry;
private final GitHistoryProvider gitProvider;
private final Path repositoryPath;
private final Runnable onComplete;
/**
* Creates a new analysis job.
*
* @param entry the commit table entry to analyze
* @param gitProvider the git history provider
* @param repositoryPath path to the Git repository
* @param onComplete callback to run (on any thread) when analysis finishes
*/
public CommitAnalysisJob(CommitTableEntry entry, GitHistoryProvider gitProvider,
Path repositoryPath, Runnable onComplete) {
super("Analyzing commit " + entry.getCommitInfo().shortId()); //$NON-NLS-1$
this.entry = entry;
this.gitProvider = gitProvider;
this.repositoryPath = repositoryPath;
this.onComplete = onComplete;
setSystem(true); // Don't show in Progress view
}
@Override
protected IStatus run(IProgressMonitor monitor) {
entry.setStatus(AnalysisStatus.ANALYZING);
if (monitor.isCanceled()) {
entry.setStatus(AnalysisStatus.PENDING);
return Status.CANCEL_STATUS;
}
try {
List<FileDiff> diffs = gitProvider.getDiffs(repositoryPath, entry.getCommitInfo().id());
if (monitor.isCanceled()) {
entry.setStatus(AnalysisStatus.PENDING);
return Status.CANCEL_STATUS;
}
EclipseLlmService llmService = EclipseLlmService.getInstance();
if (llmService.isAvailable()) {
analyzeWithAi(llmService.getEngine(), diffs, monitor);
} else {
// No LLM available — mark as no rules
entry.setStatus(AnalysisStatus.NO_RULES);
}
} catch (Exception e) {
entry.setStatus(AnalysisStatus.FAILED);
}
if (onComplete != null) {
onComplete.run();
}
return Status.OK_STATUS;
}
private void analyzeWithAi(AiRuleInferenceEngine engine, List<FileDiff> diffs,
IProgressMonitor monitor) {
List<CommitEvaluation> evaluations = new ArrayList<>();
for (FileDiff diff : diffs) {
if (monitor.isCanceled()) {
entry.setStatus(AnalysisStatus.PENDING);
return;
}
String unifiedDiff = buildUnifiedDiff(diff);
engine.inferRuleFromDiff(unifiedDiff)
.filter(e -> e.dslRule() != null && !e.dslRule().isBlank())
.ifPresent(evaluations::add);
}
if (evaluations.isEmpty()) {
entry.setStatus(AnalysisStatus.NO_RULES);
} else {
entry.setEvaluations(evaluations);
entry.setStatus(AnalysisStatus.DONE);
}
}
/**
* Builds a unified diff string from a {@link FileDiff}.
*
* <p>Uses LCS-based hunk line reconstruction to correctly distinguish
* context, added, and removed lines.</p>
*
* @param diff the file diff
* @return a unified diff string suitable for LLM inference
*/
public static String buildUnifiedDiff(FileDiff diff) {
StringBuilder sb = new StringBuilder();
sb.append("--- a/").append(diff.filePath()).append('\n'); //$NON-NLS-1$
sb.append("+++ b/").append(diff.filePath()).append('\n'); //$NON-NLS-1$
for (DiffHunk hunk : diff.hunks()) {
String[] beforeLines = hunk.beforeText().split("\n", -1); //$NON-NLS-1$
String[] afterLines = hunk.afterText().split("\n", -1); //$NON-NLS-1$
sb.append("@@ -").append(hunk.beforeStartLine()) //$NON-NLS-1$
.append(',').append(beforeLines.length)
.append(" +").append(hunk.afterStartLine()) //$NON-NLS-1$
.append(',').append(afterLines.length)
.append(" @@\n"); //$NON-NLS-1$
for (String markedLine : buildHunkLines(beforeLines, afterLines)) {
sb.append(markedLine).append('\n');
}
}
return sb.toString();
}
/**
* Builds unified-diff style hunk lines using LCS to correctly distinguish
* context, added, and removed lines.
*
* @param beforeLines lines from the "before" side of the hunk
* @param afterLines lines from the "after" side of the hunk
* @return list of lines with diff markers ({@code ' '}, {@code '-'}, {@code '+'})
*/
public static List<String> buildHunkLines(String[] beforeLines, String[] afterLines) {
int n = beforeLines.length;
int m = afterLines.length;
int[][] dp = new int[n + 1][m + 1];
for (int i = n - 1; i >= 0; i--) {
for (int j = m - 1; j >= 0; j--) {
if (beforeLines[i].equals(afterLines[j])) {
dp[i][j] = dp[i + 1][j + 1] + 1;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
}
List<String> result = new ArrayList<>();
int i = 0;
int j = 0;
while (i < n && j < m) {
if (beforeLines[i].equals(afterLines[j])) {
result.add(" " + beforeLines[i]); //$NON-NLS-1$
i++;
j++;
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
result.add("-" + beforeLines[i]); //$NON-NLS-1$
i++;
} else {
result.add("+" + afterLines[j]); //$NON-NLS-1$
j++;
}
}
while (i < n) {
result.add("-" + beforeLines[i]); //$NON-NLS-1$
i++;
}
while (j < m) {
result.add("+" + afterLines[j]); //$NON-NLS-1$
j++;
}
return result;
}
}