ScreeningPromptBuilder.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.triggerpattern.llm;

import java.util.List;

import org.sandbox.jdt.triggerpattern.llm.PromptBuilder.CommitData;

/**
 * Builds compact first-stage prompts for cheap commit screening.
 * <p>
 * The screening prompt intentionally avoids full DSL context and does not ask
 * for DSL rules or code examples. It is intended for Gemini free-tier friendly
 * mining runs where only promising commits should continue to the expensive
 * candidate-generation stage.
 * </p>
 */
public class ScreeningPromptBuilder {

	private static final int DEFAULT_MAX_DIFF_CHARS = 4_000;

	private final int maxDiffChars;

	/** Create a builder with the default diff truncation limit. */
	public ScreeningPromptBuilder() {
		this(DEFAULT_MAX_DIFF_CHARS);
	}

	/**
	 * Create a builder with an explicit diff truncation limit.
	 *
	 * @param maxDiffChars maximum characters of each diff included in the prompt
	 */
	public ScreeningPromptBuilder(int maxDiffChars) {
		if (maxDiffChars < 500) {
			throw new IllegalArgumentException("maxDiffChars must be at least 500"); //$NON-NLS-1$
		}
		this.maxDiffChars = maxDiffChars;
	}

	/**
	 * Builds a compact batch prompt for first-stage screening.
	 *
	 * @param commits commits to screen
	 * @return prompt text
	 */
	public String buildScreeningPrompt(List<CommitData> commits) {
		StringBuilder sb = new StringBuilder();
		sb.append("You are screening commits for reusable Java cleanup/refactoring patterns.\n"); //$NON-NLS-1$
		sb.append("Do not produce DSL rules. Do not produce before/after examples.\n"); //$NON-NLS-1$
		sb.append("Only decide whether each commit is worth a later, more expensive candidate-generation request.\n\n"); //$NON-NLS-1$
		sb.append("Return a JSON array with exactly ").append(commits.size()); //$NON-NLS-1$
		sb.append(" objects, one per commit, in the same order as presented.\n"); //$NON-NLS-1$
		sb.append("Schema per object:\n"); //$NON-NLS-1$
		sb.append("{\n"); //$NON-NLS-1$
		sb.append("  \"commitHash\": \"hash\",\n"); //$NON-NLS-1$
		sb.append("  \"relevant\": true,\n"); //$NON-NLS-1$
		sb.append("  \"trafficLight\": \"GREEN|YELLOW|RED|NOT_APPLICABLE\",\n"); //$NON-NLS-1$
		sb.append("  \"category\": \"short category or null\",\n"); //$NON-NLS-1$
		sb.append("  \"confidence\": 0.0,\n"); //$NON-NLS-1$
		sb.append("  \"reason\": \"one short sentence\"\n"); //$NON-NLS-1$
		sb.append("}\n"); //$NON-NLS-1$
		sb.append("confidence MUST be a decimal between 0.0 and 1.0 (not 0–100).\n\n"); //$NON-NLS-1$
		for (int i = 0; i < commits.size(); i++) {
			CommitData commit = commits.get(i);
			sb.append("## Commit ").append(i).append(" (").append(commit.commitHash()).append(")\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			sb.append("Message:\n").append(commit.commitMessage()).append("\n\n"); //$NON-NLS-1$ //$NON-NLS-2$
			sb.append("Diff:\n```\n"); //$NON-NLS-1$
			sb.append(truncate(commit.diff())).append("\n```\n\n"); //$NON-NLS-1$
		}
		return sb.toString();
	}

	private String truncate(String diff) {
		if (diff == null) {
			return ""; //$NON-NLS-1$
		}
		if (diff.length() <= maxDiffChars) {
			return diff;
		}
		return diff.substring(0, maxDiffChars) + "\n... [diff truncated for screening]"; //$NON-NLS-1$
	}
}