/** * Rollback awareness helpers (Sprint 21). * * Reads changelog.jsonl for an incident, identifies ChangeEntries that are * effective-status 'executed' (not yet rolled back), and provides: * * planRollback — returns a RollbackPlan with steps in reverse execution order. * executeRollback — runs each step via the Sprint 20 risky-action gate (per-step). * presentPlan — renders the plan to a human-readable string for CLI output. * * Escalation design note (Sprint 21): * True checkpoint-based escalation is deferred to Sprint 24 (full /bober-incident * flow). Escalation here consists of: * 1. A 'rollback_halted' timeline event with remaining step ids. * 2. A stderr warning via writeWarn. * 3. result.escalated=true + result.remaining=[...] returned to the caller. * Sprint 24 can wrap this return value in a real checkpoint if needed. * * JSONL semantics: changelog.jsonl is append-only; "latest line per id wins". * When a change is executed, Sprint 20 writes two lines (pending, executed). * After rollback, Sprint 21 appends a third line (rolled-back). Effective status * is determined by the LAST line in file-order with a given id. * * Reverse execution order: sort by the FIRST entry's executedAt (when the action * originally ran), then reverse. File order guarantees write monotonicity. * * Sprint 21 — src/incident/rollback.ts */ import type { ExecutorSeam } from "../orchestrator/deploy/types.js"; import type { RiskyActionConfig } from "../orchestrator/deploy/resolve.js"; import type { IncidentId } from "./types.js"; export interface RollbackStep { originalChangeId: string; originalDescription: string; inverseDescription: string; inverseCommand?: string; /** ISO-8601 from the original ChangeEntry's first (pending/executed) record. */ originalExecutedAt: string; } export interface RollbackPlan { incidentId: string; totalChanges: number; rollbackableChanges: number; /** Count of changes excluded because no inverse is available. */ unrollbackableChanges: number; steps: RollbackStep[]; warnings: string[]; } export interface PlanRollbackOpts { /** Include only changes executed AFTER this changeId (strict-after semantics). */ since?: string; } export interface ExecuteRollbackOpts { /** Pipeline config — forwarded to executeAction for gate resolution. */ config?: RiskyActionConfig; /** Injected executor (for tests). Default = real execa-based executor. */ executor?: ExecutorSeam; /** Stderr writer override (for capturing warnings in tests). Default = process.stderr.write. */ writeWarn?: (msg: string) => void; /** Injectable clock (for tests). Default = () => new Date(). */ now?: () => Date; } export interface RollbackExecutionEntry { /** ISO-8601 timestamp of when the rollback step was attempted. */ timestamp: string; originalChangeId: string; inverseDescription: string; status: "rolled-back" | "rolled-back-failed"; durationMs: number; errorMessage?: string; } export interface RollbackResult { attempted: number; succeeded: number; /** 0 or 1 — the sequence halts on the first failure. */ failed: number; /** Non-empty when failed === 1; contains steps that were not attempted. */ remaining: RollbackStep[]; /** true when the rollback halted due to a failure — caller should escalate. */ escalated: boolean; } /** * Build a RollbackPlan from a changelog. * * Effective-status semantics (latest-line-wins): * For each unique `id`, group all entries; the LAST entry in file order * determines the effective status. Only `executed` entries are rollbackable. * * Reverse execution order: * Steps are sorted ascending by the FIRST entry's executedAt (the original * execution time), then reversed. This means the most recent action is * rolled back first. * * --since semantics (changeId-based, strict-after): * Only changes whose FIRST entry's executedAt is strictly greater than the * --since changeId's first entry's executedAt are included. * * @param projectRoot Absolute path to the project root. * @param incidentId Incident to plan a rollback for. * @param opts Optional PlanRollbackOpts. * @throws Error if opts.since references a non-existent changeId. */ export declare function planRollback(projectRoot: string, incidentId: IncidentId, opts?: PlanRollbackOpts): Promise; /** * Render a RollbackPlan to a human-readable string for CLI output. * * Always includes: * - Header with incident ID * - Change counts (total, rollbackable, unrollbackable) * - Numbered steps in reverse execution order * - Warnings section (if any) */ export declare function presentPlan(plan: RollbackPlan): string; /** * Execute a RollbackPlan step by step. * * Each step is a risky action that MUST pass through the Sprint 20 gate * individually. This means N rollback steps → N gate invocations. * * On success of each step: * - Appends a ChangeEntry with id=originalChangeId, status='rolled-back'. * - Appends a RollbackExecutionEntry with status='rolled-back'. * * On failure of any step: * - Appends a ChangeEntry with id=originalChangeId, status='rolled-back-failed'. * - Appends a RollbackExecutionEntry with status='rolled-back-failed'. * - Halts the sequence (remaining steps are NOT attempted). * - Emits a 'rollback_halted' timeline event. * - Returns result.escalated=true with result.remaining=[unrolled steps]. * * @param projectRoot Absolute path to the project root. * @param incidentId Incident to execute the rollback for. * @param plan The plan produced by planRollback. * @param opts Optional injections (config, executor, writeWarn, now). */ export declare function executeRollback(projectRoot: string, incidentId: IncidentId, plan: RollbackPlan, opts?: ExecuteRollbackOpts): Promise; //# sourceMappingURL=rollback.d.ts.map