MiningCandidate.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.mining.core.candidate;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* A mined cleanup candidate in the staged DSL mining pipeline.
*
* <p>Candidates are discovered by the LLM, validated, tested, and eventually
* promoted into productive bundled {@code .sandbox-hint} files. They are
* stored in the {@code mining-candidates/} directory as JSON files and
* progress through the {@link CandidateStatus} lifecycle.</p>
*
* <p>Example JSON representation:</p>
* <pre>
* {
* "dslRule": "...",
* "beforeExample": "...",
* "afterExample": "...",
* "negativeExample": "...",
* "targetHintFile": "performance.sandbox-hint",
* "sourceCommit": "abc1234",
* "sourceRepo": "https://github.com/eclipse-jdt/eclipse.jdt.ui",
* "category": "string-modernization",
* "summary": "Replace StringBuffer with StringBuilder",
* "status": "DISCOVERED",
* "discoveredAt": "2026-01-01T00:00:00Z",
* "rejectionReason": null
* }
* </pre>
*/
public class MiningCandidate {
/** The DSL rule text (raw {@code .sandbox-hint} content). */
private String dslRule;
/** Java code example that should be matched by the rule (before transformation). */
private String beforeExample;
/** Expected Java code after the rule is applied. */
private String afterExample;
/** Java code example that should NOT match the rule. */
private String negativeExample;
/** Target {@code .sandbox-hint} filename (e.g. {@code performance.sandbox-hint}). */
private String targetHintFile;
/** The Git commit hash that this candidate was discovered from. */
private String sourceCommit;
/** The repository URL this commit belongs to. */
private String sourceRepo;
/** The category of the transformation (e.g. {@code string-modernization}). */
private String category;
/** Human-readable summary of the candidate. */
private String summary;
/** Current lifecycle status of this candidate. */
private CandidateStatus status;
/** ISO-8601 timestamp when the candidate was first discovered. */
private String discoveredAt;
/** Reason this candidate was rejected (only set when status is {@link CandidateStatus#REJECTED}). */
private String rejectionReason;
/** No-arg constructor for Gson. */
public MiningCandidate() {
this.status = CandidateStatus.DISCOVERED;
}
/**
* Creates a new mining candidate with all required fields.
*
* @param dslRule the DSL rule text
* @param beforeExample the before-transformation Java code example
* @param afterExample the after-transformation Java code example
* @param negativeExample the Java code that should NOT match
* @param targetHintFile the target hint file name
* @param sourceCommit the source commit hash
* @param sourceRepo the source repository URL
* @param category the transformation category
* @param summary a human-readable summary
* @param discoveredAt ISO-8601 discovery timestamp
*/
public MiningCandidate(String dslRule, String beforeExample, String afterExample,
String negativeExample, String targetHintFile, String sourceCommit,
String sourceRepo, String category, String summary, String discoveredAt) {
this.dslRule = dslRule;
this.beforeExample = beforeExample;
this.afterExample = afterExample;
this.negativeExample = negativeExample;
this.targetHintFile = targetHintFile;
this.sourceCommit = sourceCommit;
this.sourceRepo = sourceRepo;
this.category = category;
this.summary = summary;
this.discoveredAt = discoveredAt;
this.status = CandidateStatus.DISCOVERED;
}
public String getDslRule() { return dslRule; }
public void setDslRule(String dslRule) { this.dslRule = dslRule; }
public String getBeforeExample() { return beforeExample; }
public void setBeforeExample(String beforeExample) { this.beforeExample = beforeExample; }
public String getAfterExample() { return afterExample; }
public void setAfterExample(String afterExample) { this.afterExample = afterExample; }
public String getNegativeExample() { return negativeExample; }
public void setNegativeExample(String negativeExample) { this.negativeExample = negativeExample; }
public String getTargetHintFile() { return targetHintFile; }
public void setTargetHintFile(String targetHintFile) { this.targetHintFile = targetHintFile; }
public String getSourceCommit() { return sourceCommit; }
public void setSourceCommit(String sourceCommit) { this.sourceCommit = sourceCommit; }
public String getSourceRepo() { return sourceRepo; }
public void setSourceRepo(String sourceRepo) { this.sourceRepo = sourceRepo; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getSummary() { return summary; }
public void setSummary(String summary) { this.summary = summary; }
public CandidateStatus getStatus() { return status; }
public void setStatus(CandidateStatus status) { this.status = status; }
public String getDiscoveredAt() { return discoveredAt; }
public void setDiscoveredAt(String discoveredAt) { this.discoveredAt = discoveredAt; }
public String getRejectionReason() { return rejectionReason; }
public void setRejectionReason(String rejectionReason) { this.rejectionReason = rejectionReason; }
/**
* Returns a deterministic candidate ID based on candidate-defining content.
*
* @return a stable SHA-256 based candidate ID
*/
public String getCandidateId() {
StringBuilder seed = new StringBuilder();
seed.append(nullToEmpty(sourceRepo)).append('\n')
.append(nullToEmpty(sourceCommit)).append('\n')
.append(nullToEmpty(category)).append('\n')
.append(nullToEmpty(targetHintFile)).append('\n')
.append(nullToEmpty(dslRule));
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256"); //$NON-NLS-1$
byte[] hash = digest.digest(seed.toString().getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(hash.length * 2);
for (byte b : hash) {
int value = b & 0xff;
hex.append(Character.forDigit((value >>> 4) & 0x0f, 16));
hex.append(Character.forDigit(value & 0x0f, 16));
}
return hex.toString();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 algorithm not available", e); //$NON-NLS-1$
}
}
/**
* Returns a stable filename for this candidate based on {@link #getCandidateId()}.
*
* @return a filename like {@code <candidateId>-candidate.json}
*/
public String toFileName() {
return getCandidateId() + "-candidate.json"; //$NON-NLS-1$
}
private static String nullToEmpty(String value) {
return value == null ? "" : value; //$NON-NLS-1$
}
@Override
public String toString() {
return "MiningCandidate{" //$NON-NLS-1$
+ "sourceCommit='" + sourceCommit + '\'' //$NON-NLS-1$
+ ", status=" + status //$NON-NLS-1$
+ ", category='" + category + '\'' //$NON-NLS-1$
+ ", summary='" + summary + '\'' //$NON-NLS-1$
+ '}';
}
}