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");
}
}
}