TierAction.java

package network.ike.plugin.scaffold;

import java.nio.file.Path;
import java.util.Objects;

/**
 * A plan-time decision about what to do with a single scaffold
 * entry. Produced by a {@link TierHandler} from a (manifest entry,
 * current disk state, template bytes, lockfile entry) tuple and
 * consumed by the scaffold applier.
 *
 * <p>The sealed hierarchy gives the applier exhaustive pattern
 * matching and keeps the planner pure — no tier handler touches
 * disk during planning.
 */
public sealed interface TierAction
        permits TierAction.Write,
                TierAction.Skip,
                TierAction.UpToDate,
                TierAction.UserManaged {

    /**
     * The manifest entry this action relates to.
     *
     * @return the originating manifest entry
     */
    ManifestEntry entry();

    /**
     * The absolute destination path on disk, with any {@code ~/}
     * or {@code {project.root}/} placeholders already expanded.
     *
     * @return the fully-resolved destination path
     */
    Path resolvedDest();

    /**
     * Human-readable summary rendered in {@code scaffold-draft}
     * output. One line per entry; may include counts or diff hints.
     *
     * @return the draft-output summary line for this action
     */
    String reason();

    // ── Subtypes ────────────────────────────────────────────────────

    /**
     * Publish should write {@code newContent} to {@link #resolvedDest()}.
     *
     * @param entry        the manifest entry
     * @param resolvedDest absolute destination path
     * @param newContent   bytes to write (full file content — for
     *                     tracked-block this is the fully-rendered
     *                     combined file, not just the managed block)
     * @param appliedSha   hash of {@code newContent} (for the lockfile
     *                     update; for tracked-block this is the hash
     *                     of just the managed block)
     * @param templateSha  hash of the unbounded template source (used
     *                     for drift telemetry in the new lockfile
     *                     entry)
     * @param kind         whether the file existed before this write
     * @param reason       draft-output summary
     */
    record Write(
            ManifestEntry entry,
            Path resolvedDest,
            byte[] newContent,
            String appliedSha,
            String templateSha,
            Kind kind,
            String reason) implements TierAction {

        /** Compact constructor validating required fields. */
        public Write {
            Objects.requireNonNull(entry, "entry");
            Objects.requireNonNull(resolvedDest, "resolvedDest");
            Objects.requireNonNull(newContent, "newContent");
            Objects.requireNonNull(appliedSha, "appliedSha");
            Objects.requireNonNull(templateSha, "templateSha");
            Objects.requireNonNull(kind, "kind");
            Objects.requireNonNull(reason, "reason");
            newContent = newContent.clone();
        }

        /**
         * Accessor that returns a defensive copy of the content
         * bytes so callers cannot mutate the record's state.
         *
         * @return a fresh clone of the bytes to write
         */
        @Override
        public byte[] newContent() {
            return newContent.clone();
        }

        /** Category of write, for draft output and ordering. */
        public enum Kind {
            /** Target did not exist before this write. */
            INSTALL,
            /** Target existed and was safe to update. */
            UPDATE,
            /** Target is being restored by {@code scaffold-revert}. */
            REVERT
        }
    }

    /**
     * Publish must not touch {@link #resolvedDest()} because the user
     * has diverged from the last-applied version. Draft output carries
     * a textual diff so the user can decide.
     *
     * @param entry        the manifest entry
     * @param resolvedDest absolute destination path
     * @param reason       short summary (e.g. "user-edited; +3/-1")
     * @param diff         multi-line textual diff for draft output;
     *                     may be empty for tiers that don't render
     *                     diffs
     */
    record Skip(
            ManifestEntry entry,
            Path resolvedDest,
            String reason,
            String diff) implements TierAction {

        /** Compact constructor validating required fields. */
        public Skip {
            Objects.requireNonNull(entry, "entry");
            Objects.requireNonNull(resolvedDest, "resolvedDest");
            Objects.requireNonNull(reason, "reason");
            diff = diff == null ? "" : diff;
        }
    }

    /**
     * Nothing to write — target already matches the current template.
     * The scaffold applier may still refresh the lockfile entry so
     * telemetry stays current with the new {@code standards-version}.
     *
     * @param entry        the manifest entry
     * @param resolvedDest absolute destination path
     * @param templateSha  hash of the current template (for lockfile
     *                     metadata refresh)
     * @param appliedSha   hash currently on disk — typically equal to
     *                     {@code templateSha} for tool-owned and
     *                     tracked, or the hash of the managed block
     *                     for tracked-block
     * @param reason       draft-output summary
     */
    record UpToDate(
            ManifestEntry entry,
            Path resolvedDest,
            String templateSha,
            String appliedSha,
            String reason) implements TierAction {

        /** Compact constructor validating required fields. */
        public UpToDate {
            Objects.requireNonNull(entry, "entry");
            Objects.requireNonNull(resolvedDest, "resolvedDest");
            Objects.requireNonNull(templateSha, "templateSha");
            Objects.requireNonNull(appliedSha, "appliedSha");
            Objects.requireNonNull(reason, "reason");
        }
    }

    /**
     * Publish must not touch {@link #resolvedDest()} because at least
     * one managed element already exists with a value the user has
     * chosen, and policy is to defer to the user's value rather than
     * overwrite it.
     *
     * <p>Distinct from {@link UpToDate}: the file does <em>not</em>
     * match the manifest's desired value — we simply refuse to
     * overwrite what the user already set. A model adapter emits this
     * state when every ensured element is already present on disk and
     * at least one present value diverges from the manifest's value;
     * if the whole set matches, {@link UpToDate} is used instead.
     *
     * <p>Lockfile semantics mirror {@link UpToDate}: model-managed
     * provenance is refreshed but no bytes are written.
     *
     * @param entry        the manifest entry
     * @param resolvedDest absolute destination path
     * @param templateSha  hash of the current on-disk content (for
     *                     lockfile metadata refresh; the file is not
     *                     rewritten so this equals {@code appliedSha})
     * @param appliedSha   hash of the current on-disk content
     * @param reason       one-line summary — should name which
     *                     element(s) were deferred
     */
    record UserManaged(
            ManifestEntry entry,
            Path resolvedDest,
            String templateSha,
            String appliedSha,
            String reason) implements TierAction {

        /** Compact constructor validating required fields. */
        public UserManaged {
            Objects.requireNonNull(entry, "entry");
            Objects.requireNonNull(resolvedDest, "resolvedDest");
            Objects.requireNonNull(templateSha, "templateSha");
            Objects.requireNonNull(appliedSha, "appliedSha");
            Objects.requireNonNull(reason, "reason");
        }
    }
}