OverrideLoader.java

// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Egothor
// Copyright 2026 Accenture
package org.egothor.methodatlas.command;

import java.io.IOException;
import java.nio.file.Path;

import org.egothor.methodatlas.emit.ClassificationOverride;

/**
 * Loads classification override files into {@link ClassificationOverride}
 * instances.
 *
 * <p>
 * Override files carry human-reviewed corrections to AI classification
 * results. They are persisted in YAML using the
 * {@code ClassificationOverride} schema and re-applied on every subsequent
 * MethodAtlas run so that reviewer decisions reproduce deterministically
 * across CI builds.
 * </p>
 *
 * <h2>Null handling</h2>
 *
 * <p>
 * Passing {@code null} as the override path is a legitimate signal that the
 * caller wants the empty no-op singleton — typical when no
 * {@code -override-file} flag was supplied. The loader returns
 * {@link ClassificationOverride#empty()} in that case rather than throwing,
 * because the absence of an override file is normal, not exceptional.
 * </p>
 *
 * <h2>Thread safety</h2>
 *
 * <p>
 * This class is thread-safe. It carries no instance state and the underlying
 * {@link ClassificationOverride#load(Path)} call is itself stateless.
 * </p>
 *
 * @see ClassificationOverride
 * @since 1.0.0
 */
public final class OverrideLoader {

    /**
     * Creates a new override loader. The loader carries no instance state.
     */
    public OverrideLoader() {
        // Intentionally empty; OverrideLoader is stateless.
    }

    /**
     * Loads the classification override file at the given path, or returns the
     * empty no-op singleton when no override file was configured.
     *
     * <p>
     * The empty singleton is a sentinel: callers can apply it unconditionally
     * to every classification without checking for {@code null}, which
     * simplifies the orchestration loops in the {@link Command}
     * implementations.
     * </p>
     *
     * @param overrideFile path to the YAML override file, or {@code null} when
     *                     the caller wants the empty no-op singleton
     * @return loaded override set, or the empty singleton; never {@code null}
     * @throws IllegalArgumentException if the file exists but cannot be read
     *                                  or contains invalid YAML
     */
    public ClassificationOverride load(Path overrideFile) {
        if (overrideFile == null) {
            return ClassificationOverride.empty();
        }
        try {
            return ClassificationOverride.load(overrideFile);
        } catch (IOException e) {
            throw new IllegalArgumentException("Cannot load override file: " + overrideFile, e);
        }
    }
}