LockfileEntry.java

package network.ike.plugin.scaffold;

import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * One file's entry in a scaffold lockfile.
 *
 * <p>The shape depends on the {@link #tier tier}:
 *
 * <ul>
 *   <li>{@link ScaffoldTier#TOOL_OWNED TOOL_OWNED}: only
 *       {@link #templateSha templateSha} is populated
 *       ({@code appliedSha} is the same as templateSha by policy — we
 *       always overwrite — and {@code managedElements} is empty).</li>
 *   <li>{@link ScaffoldTier#TRACKED TRACKED} /
 *       {@link ScaffoldTier#TRACKED_BLOCK TRACKED_BLOCK}: both
 *       {@code templateSha} (last template applied) and
 *       {@code appliedSha} (whole-file hash the last publish produced
 *       on disk) are populated; {@code managedElements} is empty.</li>
 *   <li>{@link ScaffoldTier#MODEL_MANAGED MODEL_MANAGED}:
 *       {@code templateSha} and {@code appliedSha} are null;
 *       {@code managedElements} lists per-element provenance.</li>
 * </ul>
 *
 * <p>Hash values are stored in the form
 * {@code "sha256:" + hex-digest} so future hash algorithms can be
 * added without ambiguity.
 *
 * @param tier             the ownership tier for this file; never
 *                         {@code null}
 * @param templateSha      hash of the template last applied; may be
 *                         {@code null} for {@link ScaffoldTier#MODEL_MANAGED}
 * @param appliedSha       hash of the file on disk at last publish;
 *                         may be {@code null} for
 *                         {@link ScaffoldTier#TOOL_OWNED} and
 *                         {@link ScaffoldTier#MODEL_MANAGED}
 * @param managedElements  per-element provenance for
 *                         {@link ScaffoldTier#MODEL_MANAGED};
 *                         never {@code null} (use empty list for
 *                         whole-file tiers). The stored list is
 *                         unmodifiable.
 */
public record LockfileEntry(
        ScaffoldTier tier,
        String templateSha,
        String appliedSha,
        List<ManagedElement> managedElements) {

    /**
     * Canonical constructor with validation and defensive copying.
     */
    public LockfileEntry {
        Objects.requireNonNull(tier, "tier");
        managedElements = managedElements == null
                ? List.of()
                : List.copyOf(managedElements);

        switch (tier) {
            case MODEL_MANAGED -> {
                if (templateSha != null || appliedSha != null) {
                    throw new IllegalArgumentException(
                            "MODEL_MANAGED entries must not carry "
                                    + "templateSha/appliedSha "
                                    + "(per-element provenance goes "
                                    + "in managedElements)");
                }
            }
            case TOOL_OWNED, TRACKED, TRACKED_BLOCK -> {
                if (!managedElements.isEmpty()) {
                    throw new IllegalArgumentException(
                            tier.manifestValue()
                                    + " entries must not carry "
                                    + "managedElements");
                }
                if (templateSha == null) {
                    throw new IllegalArgumentException(
                            tier.manifestValue()
                                    + " entries require templateSha");
                }
                if (tier != ScaffoldTier.TOOL_OWNED
                        && appliedSha == null) {
                    throw new IllegalArgumentException(
                            tier.manifestValue()
                                    + " entries require appliedSha");
                }
            }
        }
    }

    /**
     * Convenience factory for a tool-owned entry (only templateSha
     * matters; divergence is reported but never blocks publish).
     *
     * @param templateSha hash of the template last applied
     * @return a TOOL_OWNED entry
     */
    public static LockfileEntry toolOwned(String templateSha) {
        return new LockfileEntry(
                ScaffoldTier.TOOL_OWNED,
                templateSha,
                null,
                Collections.emptyList());
    }

    /**
     * Convenience factory for a tracked or tracked-block entry.
     *
     * @param tier        one of {@link ScaffoldTier#TRACKED} or
     *                    {@link ScaffoldTier#TRACKED_BLOCK}
     * @param templateSha hash of the template last applied
     * @param appliedSha  hash of the file on disk after last publish
     * @return the entry
     */
    public static LockfileEntry tracked(
            ScaffoldTier tier,
            String templateSha,
            String appliedSha) {
        if (tier != ScaffoldTier.TRACKED
                && tier != ScaffoldTier.TRACKED_BLOCK) {
            throw new IllegalArgumentException(
                    "tracked() requires TRACKED or TRACKED_BLOCK, "
                            + "got " + tier);
        }
        return new LockfileEntry(tier, templateSha, appliedSha,
                Collections.emptyList());
    }

    /**
     * Convenience factory for a model-managed entry.
     *
     * @param elements per-element provenance; may be empty (the file
     *                 is model-managed but currently no elements are
     *                 installed)
     * @return the entry
     */
    public static LockfileEntry modelManaged(
            List<ManagedElement> elements) {
        return new LockfileEntry(
                ScaffoldTier.MODEL_MANAGED, null, null, elements);
    }
}