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;