CheckPromptsCommand.java

package org.egothor.methodatlas.command;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

import org.egothor.methodatlas.ai.PromptTemplateKind;
import org.egothor.methodatlas.ai.PromptTemplateSet;
import org.egothor.methodatlas.ai.PromptTemplateValidator;

/**
 * Utility CLI mode ({@code -check-prompts}) that validates prompt templates and
 * prints their SHA-256 checksums without running a scan.
 *
 * <p>
 * For each {@link PromptTemplateKind} it reports whether the effective template
 * (the operator's override, if supplied via the matching flag, otherwise the
 * built-in default) is structurally valid, lists any problems, and prints the
 * checksum that a reproducibility receipt would record. It exits with status
 * {@code 1} if any template is invalid, making it suitable as a CI pre-flight
 * gate; otherwise {@code 0}.
 * </p>
 *
 * <p>
 * Validation covers placeholder correctness and the survival of the JSON
 * structural anchor the response parser needs; it cannot verify that a model will
 * follow the instructions (see {@link PromptTemplateValidator}).
 * </p>
 *
 * @since 4.1.0
 */
public final class CheckPromptsCommand implements Command {

    /** Flag that selects this utility mode. */
    public static final String FLAG_CHECK_PROMPTS = "-check-prompts";

    /** Flag supplying a custom method-classification template file. */
    public static final String FLAG_CLASSIFICATION_PROMPT = "-classification-prompt";

    /** Flag supplying a custom folded credential-triage appendix template file. */
    public static final String FLAG_TRIAGE_PROMPT = "-triage-prompt";

    /** Flag supplying a custom standalone credential-triage template file. */
    public static final String FLAG_DEDICATED_TRIAGE_PROMPT = "-dedicated-triage-prompt";

    private final Map<PromptTemplateKind, Path> overrides;

    /**
     * Creates a command for the supplied template overrides.
     *
     * @param overrides map of kind to override file; kinds absent from the map use
     *                  the built-in default; must not be {@code null}
     */
    /* default */ CheckPromptsCommand(Map<PromptTemplateKind, Path> overrides) {
        // Build via putAll rather than the EnumMap copy constructor, which rejects an
        // empty source map (it cannot infer the enum type from no entries).
        Map<PromptTemplateKind, Path> copy = new EnumMap<>(PromptTemplateKind.class);
        copy.putAll(overrides);
        this.overrides = copy;
    }

    /**
     * Builds a command by scanning raw command-line arguments for the three
     * prompt-override flags.
     *
     * @param args raw command-line arguments; must not be {@code null}
     * @return a configured command; never {@code null}
     */
    public static CheckPromptsCommand fromArgs(String... args) {
        Map<PromptTemplateKind, Path> overrides = new EnumMap<>(PromptTemplateKind.class);
        putIfPresent(overrides, args, FLAG_CLASSIFICATION_PROMPT, PromptTemplateKind.CLASSIFICATION);
        putIfPresent(overrides, args, FLAG_TRIAGE_PROMPT, PromptTemplateKind.TRIAGE_APPENDIX);
        putIfPresent(overrides, args, FLAG_DEDICATED_TRIAGE_PROMPT, PromptTemplateKind.DEDICATED_TRIAGE);
        return new CheckPromptsCommand(overrides);
    }

    private static void putIfPresent(Map<PromptTemplateKind, Path> map, String[] args, String flag,
            PromptTemplateKind kind) {
        for (int i = 0; i < args.length - 1; i++) {
            if (flag.equals(args[i])) {
                map.put(kind, Path.of(args[i + 1]));
                return;
            }
        }
    }

    /**
     * Validates each template kind and prints a report.
     *
     * @param out writer receiving the report; never {@code null}
     * @return {@code 0} when every template is valid; {@code 1} otherwise
     * @throws IOException never thrown directly; per-file read failures are reported
     *                     inline as validation failures
     */
    @Override
    public int execute(PrintWriter out) throws IOException {
        boolean allValid = true;
        for (PromptTemplateKind kind : PromptTemplateKind.values()) {
            allValid &= reportKind(out, kind);
        }
        out.println();
        out.println(allValid
                ? "All prompt templates are valid."
                : "One or more prompt templates are INVALID.");
        return allValid ? 0 : 1;
    }

    private boolean reportKind(PrintWriter out, PromptTemplateKind kind) {
        Path file = overrides.get(kind);
        String source = file == null ? "built-in default" : file.toString();
        String body;
        try {
            body = file == null ? PromptTemplateSet.defaults().get(kind) : Files.readString(file);
        } catch (IOException | UncheckedIOException e) {
            out.println(kind + " [" + source + "]: FAIL");
            out.println("  - cannot read file: " + e.getMessage());
            return false;
        }

        List<String> problems = PromptTemplateValidator.validate(kind, body);
        boolean ok = problems.isEmpty();
        out.println(kind + " [" + source + "]: " + (ok ? "PASS" : "FAIL"));
        out.println("  sha256: " + PromptTemplateSet.defaults().with(kind, body).hash(kind));
        for (String problem : problems) {
            out.println("  - " + problem);
        }
        return ok;
    }
}