import type { AuthoringApiClient, UpdateItemInput } from "../api/client.js";
import type { FieldValue } from "../ir/operations.js";
import type { PlannedAction } from "../runtime/plan.js";
import type { RollbackLogger } from "./rollback-log.js";
/**
* Best-effort rollback for partial recipe pushes.
*
* When the executor's apply phase errors mid-IR, ops that already landed
* are still on the tenant. The rollback module unwinds them in LIFO order
* (reverse of the apply order, which mirrors topological order) using the
* pre-mutation snapshot each op captured at plan time:
*
* - applied `createItem` (snapshot was null) → `deleteItem(itemId)`
* - applied `updateItem` → `updateItem` with the prior value of each
* touched field, or empty string when the field was unset
*
* "Best-effort" means a rollback step that itself errors is logged and
* counted, never cascaded — remaining rollbacks still run. The result
* carries `{rolledBack, errors}` for the terminal `failed` event payload.
*/
/**
* Restoration node for a pruned subtree. Trees are walked depth-first by
* the rollback executor: `parent` becomes the freshly-assigned itemId
* after each `createItem`, and that itemId is then used as the parent of
* every child's recursive restore call.
*
* Field restoration:
* - `sharedFields` and `initialFields` are passed to `createItem`, so
* the new item materialises in `defaultLanguage` v1 with the shared
* fields + that (language, version)'s versioned values populated.
* - `extraVersions` carries every OTHER (language, version) the
* snapshot captured. The executor walks them in ascending order per
* language, calling `addItemVersion(itemId, language)` enough times
* to materialise each version slot, then `updateItem(itemId,
* language, version, fields)` to populate it.
*
* After restoration, the recreated item has the same shared fields, the
* same per-(language, version) versioned field values, and the same
* version count per language as the snapshot. Only the itemId differs
* — Sitecore assigns a fresh GUID on `createItem` and the Authoring API
* has no input for preserving the original.
*/
export interface PrunedSubtreeRestoreNode {
/** Sitecore itemId of the parent under which to recreate this item. */
parent: string;
templateId: string;
name: string;
/**
* Language passed to the inverse `createItem` call — typically the
* first entry in the operator's `snapshotLanguages` config that the
* snapshot found versions for. Determines which (language, version)
* tuple `initialFields` populates.
*/
defaultLanguage: string;
/**
* Shared fields (no language/version) + versioned fields for
* (defaultLanguage, version 1). Written by `createItem`.
*/
initialFields: FieldValue[];
/**
* Every other (language, version) the snapshot captured. The
* executor materialises each via `addItemVersion` (called as many
* times as needed to reach the requested version in that language)
* followed by `updateItem(itemId, language, version, fields)`.
*
* Order: language order from whichever resolved into `candidateLanguages`
* in `planPruneChildren` — operator's `snapshotLanguages` when set,
* otherwise the tenant set returned by `client.getTenantLanguages`.
* Within a language, versions ascending. (defaultLanguage, 1) is NOT
* in this list — `createItem` already wrote it.
*/
extraVersions: Array<{
language: string;
version: number;
fields: FieldValue[];
}>;
/** Descendants to restore depth-first under this item's new itemId. */
children: PrunedSubtreeRestoreNode[];
}
export type InverseMutation = {
kind: "deleteItem";
itemId: string;
} | {
kind: "updateItem";
input: UpdateItemInput;
} | {
/**
* Inverse of an applied `pruneChildren` mutation: depth-first
* recreation of each pruned subtree under its prior parent,
* including the full (language, version) field grid for every
* language the snapshot captured.
*
* The single remaining lossy bound: **itemIds change.** Sitecore
* assigns itemIds server-side on `createItem` and the Authoring
* API has no input for preserving the original GUID. Inbound
* references (multi-list values, layout `` GUIDs, link
* fields) that pointed at the old GUID stay broken after
* restoration. GUID-preserving rollback would need the Content
* Serialization API, which scai does not adopt.
*
* What IS preserved:
* - The full subtree shape (recursive snapshot at plan time).
* - Item names, templates, parent-child relationships.
* - Shared field values.
* - Per-(language, version) versioned field values for every
* language the operator listed in `--snapshot-languages`.
* - Per-language version counts.
*
* Per-item failures (parent missing, name collision with a sibling
* already restored, addItemVersion/updateItem failure) are
* aggregated into the rollback errors list; remaining nodes still
* attempt restore. A subtree whose root fails to create skips its
* children (no parent itemId to attach under) and logs the
* cascade as a single failure on the root.
*/
kind: "restoreItems";
trees: ReadonlyArray;
};
export interface RollbackError {
index: number;
label: string;
error: string;
}
export interface RollbackResult {
rolledBack: number;
errors: RollbackError[];
}
export type RollbackEvent = {
kind: "rollback-start";
action: PlannedAction;
} | {
kind: "rollback-skip";
action: PlannedAction;
reason: string;
} | {
kind: "rollback-success";
action: PlannedAction;
} | {
kind: "rollback-failed";
action: PlannedAction;
error: string;
};
export interface RollbackOptions {
emit?: (event: RollbackEvent) => void;
/**
* On-disk audit log. When provided, each compensating-op outcome
* (success/skip/failure) is appended to the run's JSONL file so an
* operator can audit what happened — including which items rollback
* itself failed on. The caller is responsible for writing the run's
* terminal summary line after `rollback()` returns.
*/
log?: {
logger: RollbackLogger;
recipe: string;
};
}
/**
* Produce the inverse mutation for an applied action. Returns `null` when
* there's nothing to undo (a skip with no mutation) OR when the mutation is
* one of the deliberately warn-only kinds — see `residueNoteFor`, which
* supplies the operator-facing explanation for that second group.
*
* For an applied `createItem`, the inverse is `deleteItem(itemId)` where
* `itemId` is the Sitecore-assigned ID — captured by the executor on
* dispatch and looked up here via `capturedItemIds[action.operation.id]`.
*
* For an applied `updateItem`, each touched field reverts to its prior
* snapshot value. If a field was unset prior, the inverse sets it to ""
* (Sitecore's pragmatic clear). True "field-not-set" semantics would
* require a deleteField mutation; not yet implemented.
*/
export declare const inverseOf: (action: PlannedAction, capturedItemIds: ReadonlyMap) => InverseMutation | null;
/**
* Unwind `applied` actions in LIFO order. Each step catches its own
* errors so a rollback failure on op N doesn't abort rollback of op N-1.
*/
export declare const rollback: (applied: PlannedAction[], client: AuthoringApiClient, capturedItemIds: ReadonlyMap, options?: RollbackOptions) => Promise;