/** * The `forget` orchestrator — the composing layer over the remediation * primitives (unlock-engine layering: privileged primitives in * `retainFlip.ts`/`forensicsCapture.ts`, this module composes): * * captured → flip-verified → flip-applied → (converge deploy) → * removal-applied → done * * `runForgetSurgery` drives phases 1–3. The removal phase is deliberately NOT * here: it is the normal converge deploy (plan § 3.2 phase 4 — the synth no * longer contains the resource, CFN sees delete-with-Retain and skips the * physical call, and every other pending change lands in the same update). * `completeForgetAfterConverge` closes the journal once a deploy (any surface * — CLI, worker, another operator) has removed the targets from the live * template. * * Consent is the CALLER's job (the destruction ceremony runs before surgery); * `assessForgetResumability` tells the caller whether the ceremony may be * skipped — a journal at any phase, or the in-template `fjall:forget` marker * [G3], proves the destructive act was already consented (capture and flip * only ever run post-ceremony), so resume never re-ceremonies (plan § 3.2). * Targets advanced via the FLIPLESS fast-path (defect #5 — live policies * already Retain, no UpdateStack, no marker) or the FLIPLESS-BY-NECESSITY * path (same incident, outbound direction — the flip is structurally * impossible because the target's own references resolve against dead * siblings, and the target is verified physically deleted so the converge's * removal is a physical no-op) have no AWS-side consent trace: with the * journal lost they re-ceremony, never silently resume. * * Every phase re-derives its precondition from live state: the flip * derivation, diff-guard and change-set proof re-run on every pass (the * journalled proof is an audit record, never a skip-token), which is also * what "live-template digest mismatch ⇒ re-verify from phase 2" degrades to. * * The closure machinery (`resolveConvergedTargets`, * `completeForgetAfterConverge`, `completeForgetAfterDeploy`) serves BOTH * surgery verbs — the recreate orchestrator (`recreateResource.ts`) closes * through it, with the marker key and terminal field derived from the * journal's verb. The surgery entry points here are forget-only and fail * closed on a cross-verb journal (the opId derivation excludes the verb). * * Failure messages are pre-masked at construction. */ import { type Result } from "@fjall/generator"; import type { AwsProvider } from "../../aws/AwsProvider.js"; import type { DriftFinding } from "../../types/drift.js"; import { type RemediationJournalRecord, type RemediationPhase, type RemediationTarget } from "../../types/remediation.js"; import { type DriftProbeOptions } from "../drift/driftProbe.js"; import { type DriftJournalOptions } from "../drift/driftSuspectJournal.js"; import { type ForensicsCaptureOptions } from "./forensicsCapture.js"; import { type RemediationJournalOptions } from "./remediationJournal.js"; import { type RetainFlipOptions } from "./retainFlip.js"; export interface ForgetOrchestrationOptions { journal?: RemediationJournalOptions; forensics?: ForensicsCaptureOptions; flip?: RetainFlipOptions; driftJournal?: DriftJournalOptions; /** Poll caps + clock for the closure probe's drift-API fallback. */ probe?: DriftProbeOptions; /** Injectable clock for journal/marker timestamps. */ now?: () => Date; } export interface ForgetSurgeryParams { stackName: string; targets: readonly RemediationTarget[]; /** The deploy target that owns the stack — names the converge command. */ appName?: string; /** Drift-probe evidence persisted in the forensics bundle. */ findings?: readonly DriftFinding[]; /** Audit trail: the exact physical names consented at the ceremony. */ consentedPhysicalNames?: readonly string[]; /** * How to treat a journal already at "done" for this opId. The default * ("report-completed") suits an explicit `drift resume ` — the * operator asked about THAT operation. A REPAIR pass targeting a fresh * finding must pass "archive-and-restart": the done journal belongs to a * prior incarnation of the same stack + logical-ID set (opId has no * incarnation component) and would otherwise false-report completion while * the new deletion stays wedged. */ doneJournalPolicy?: "report-completed" | "archive-and-restart"; onProgress?: (message: string) => void; abortSignal?: AbortSignal; } export interface ForgetSurgeryReport { opId: string; /** Journal phase after this pass. */ phase: RemediationPhase; /** True until the converge deploy removes the targets from the template. */ convergePending: boolean; forensicsPath?: string; journal: RemediationJournalRecord; } /** Shared with the recreate orchestrator (`recreateResource.ts`). */ export interface LiveStackState { templateBody: string; capabilities: string[]; /** Live parameter keys — echoed UsePreviousValue on both flip CFN calls. */ parameterKeys: string[]; } export declare function fetchLiveStackState(aws: AwsProvider, stackName: string, abortSignal?: AbortSignal): Promise>; /** * [G1] The two TERMINATING cures for an in-flight recreate operation, shared * by every surface that meets one it cannot itself complete (the forget verb * guards here, the post-failure repair offer, the [G2] recreate-converging * warn, and the CLI drift surfaces — deferred-cure, open-operation and * in-flight-remediation lines [H1], the only DISCOVERABLE teach when a * byte-identical revert excludes the stack from the deploy pre-flight). * A bare "re-run the deploy" teach is circular when the offending * configuration change was reverted: the plan then carries no pinned-name * replacement, the gate never re-fires, and the converge no-ops against the * open journal forever. Only a config change terminates the operation — * re-applying the replacement (the recreate completes) or removing the * resource (the operation closes as removed). */ export declare function formatRecreateInFlightCures(appName?: string): string; export type ForgetResumeSource = "journal" | "marker" | "none"; export interface ForeignMarkerOperation { opId: string; /** * The logical IDs still carrying this opId's marker in the LIVE template * [G3]. This may be a PROPER SUBSET of the operation's original target set * (stripped markers are invisible) — adoption MUST prove completeness by * hash identity: `computeRemediationOpId(stackName, markedLogicalIds)` * reproducing `opId`. */ markedLogicalIds: string[]; /** logicalId → CFN resource Type from the live template — lets the * adoption seam build targets for recovered IDs with no drift finding. */ markedResourceTypes: Record; } export interface ForgetResumeAssessment { opId: string; /** * True when the destructive act was already consented — a journal at any * phase (capture only runs post-ceremony) or the in-template fjall:forget * marker [G3]. The caller skips the ceremony; a fresh operation must not. */ alreadyConsented: boolean; source: ForgetResumeSource; journal?: RemediationJournalRecord; /** * Marker-only in-flight operations owned by OTHER opIds found on the * assessed targets ([G3] — their journal is lost or on another machine). * The caller must never mint an operation whose targets overlap these: * adopt each op with exactly its own recovered set — first proving the * recovery is COMPLETE via hash identity (see `ForeignMarkerOperation`) — * or defer. Empty on the journal-resume path (not scanned — the journal * already decides). */ foreignMarkers: ForeignMarkerOperation[]; } /** * Pre-ceremony check: is this forget operation already in flight? Journal * first (local resume record), then the live template's fjall:forget marker * (AWS-side truth that survives machine loss — the journal-lost case). */ export declare function assessForgetResumability(aws: AwsProvider, params: Pick, options?: ForgetOrchestrationOptions): Promise>; /** * Phases 1–3 of the two-deploy forget: forensics capture (abort on failure — * no capture, no mutate), flip derivation + both proofs, flip apply, then the * completion check (a converge that already ran closes the journal in the * same pass). Idempotent and resumable: safe to re-run at any phase. */ export declare function runForgetSurgery(aws: AwsProvider, params: ForgetSurgeryParams, options?: ForgetOrchestrationOptions): Promise>; export interface CompleteForgetParams { journal: RemediationJournalRecord; abortSignal?: AbortSignal; } export type CompleteForgetOutcome = { status: "done"; journal: RemediationJournalRecord; } /** At least one target is still present (flipped) or unverifiable — the * converge deploy has not run, or ran and could not be verified. The * per-target buckets let callers act on the ACTUAL failure set instead of * the whole target list [H4]. */ | { status: "converge-pending"; journal: RemediationJournalRecord; /** Targets no longer in the live template — their removal landed. */ absent: string[]; /** Targets verified retained-by-redeclaration (recreate: re-created). */ retained: string[]; /** Targets neither absent nor verifiably retained: still carrying this * op's marker (converge not run, or the removal was reverted), or * present-unmarked with a closure-probe verdict that fails closed * (confirmed-deleted, unconfirmed, stack-missing, membership-gate * "present", probe failure). Every entry is template-PRESENT at the * closure read by construction. */ unverified: string[]; }; /** * Post-converge closure: when every target is either gone from the live * template or verified retained-by-redeclaration (the on-demand closure probe * in `resolveConvergedTargets`), advance the journal removal-applied → done * and clear the forgotten logical IDs from the drift suspect journal (the * wedge is gone). Called by `drift resume`, by a re-run of `drift repair`, * and by the deploy engine after a successful deploy (cross-surface resume, * plan § 3.2) — so the mainline converge deploy closes retained targets * naturally at completion. * * Serves BOTH surgery verbs (the marker key and terminal field derive from * `journal.verb`): for a recreate journal the `retained` bucket is the * re-created (or operator-reverted) targets, recorded as `recreatedResources`; * absent targets are those the converge that just ran no longer declares. * Consent alone does not prove their deletion executed [G6] — the * `probeAbsentClosureTargets` call below physically verifies each absent * target, recording still-alive ones as `orphanedResources` and unverifiable * ones as `unverifiedRemovals` rather than claiming an executed deletion. */ export declare function completeForgetAfterConverge(aws: AwsProvider, params: CompleteForgetParams, options?: ForgetOrchestrationOptions): Promise>; export interface PreFlightRemediationPartition { /** Findings whose block stands — includes flip-not-live and still-declared * findings below, so callers gate on `blocked.length` alone. */ blocked: DriftFinding[]; /** * Blocked findings whose operation exists but has NOT applied the flip — * the deploy would still wedge; the block message adds the resume hint. */ inProgressOpIds: string[]; /** * Findings whose flip is verifiably live AND whose synthesised template * omits the target: THIS deploy is the converge deploy — the block is * suppressed so the surgery can complete. */ converging: Array<{ finding: DriftFinding; opId: string; }>; /** Journal says flip-applied but the live template VERIFIABLY no longer * carries Retain + our marker — surgery must re-run before deploying. */ flipNotLive: Array<{ finding: DriftFinding; opId: string; }>; /** The live template could NOT be read, so the flip's liveness is * unverifiable — fail-closed block whose cure is retrying the deploy, never * the flip-stripped copy. `message` is pre-masked at construction. */ flipUnverified: Array<{ finding: DriftFinding; opId: string; message: string; }>; /** Flip is live but the synth still declares the target — this deploy is * NOT the converge; the resource must leave the fjall config first. */ stillDeclared: Array<{ finding: DriftFinding; opId: string; }>; /** * SUPPRESSED findings covered by an in-flight RECREATE journal — the deploy * itself is the cure, so blocking would deadlock (recreate resumes ONLY * through the deploy; `drift repair` fails closed on its journal). Safe at * every in-flight phase because the 3f surgery's removal update strips the * dead-tracked target from the template BEFORE the converge — CloudFormation * never updates the deleted resource, then the converge re-CREATEs it. See * the phase-by-phase evidence in the function JSDoc below. Surfaced to the * caller for the honest onLog line, never dropped silently. */ recreateConverging: Array<{ finding: DriftFinding; opId: string; appName?: string; }>; } export interface PreFlightRemediationDeps { aws: AwsProvider; /** * Whether the incoming deploy's synthesised template still declares a * logical ID (read from cdk.out at the deploy call site — see * `synthTemplateDeclaresResource`). `undefined` = the synth template could * not be read; suppression then rests on the live-flip check alone rather * than deadlocking a genuine converge on an unreadable artefact. */ synthDeclaresTarget: (stackName: string, logicalId: string) => Promise; /** Caller shutdown/cancel signal, threaded into the per-stack live-template * probes; once aborted, remaining findings fail closed to `blocked`. */ abortSignal?: AbortSignal; } /** * Answer the partition's synth-omission question from a cdk.out directory. * Read failure returns `undefined` (debug-logged) — the caller decides how * suppression degrades. */ export declare function synthTemplateDeclaresResource(cdkOutPath: string, stackName: string, logicalId: string): Promise; /** * The [G2] pre-flight × remediation-journal interaction (plan § 3.2 decision, * recorded in the Phase 6 report): a confirmed-deleted finding covered by a * remediation journal at phase ≥ flip-applied does NOT block the deploy — * blocking would deadlock the orchestration whose removal phase IS the next * deploy. Operations still pre-flip keep the block (deploying before the * Retain flip would re-wedge) but name the resume command. * * The journal phase alone is never the suppression authority. Both must hold: * (1) the live template still carries the Retain policies for the target and * no FOREIGN marker (`readRetainFlipState` — stripped policies re-block * to `drift repair`; the policies are the safety property CFN honours at * the converge's delete, and flipless targets [defect #5] are markerless * by design, so our own marker is corroborating rather than required). * Flipless-BY-NECESSITY targets (journal `fliplessByNecessity`) pass (1) * while policy-pending: the flip was structurally impossible and their * safety property is verified physical absence — re-proven by the * finding in hand, this pre-flight's own confirmed-deleted verdict; * (2) the deploy's synthesised template OMITS the target — a deploy that * still declares the resource is not the converge: cdk would strip the * flip and re-wedge on the dead resource. * A target already ABSENT from the live template passes (1) — its removal * landed and the deploy cannot touch it. An UNREADABLE live template proves * neither state: those findings fail closed to `flipUnverified` (cure: retry * the deploy), never to `flipNotLive` — its re-run-repair cure would be a lie. * * RECREATE coverage suppresses unconditionally (refines sanction 10). The * forget-shaped conditions cannot apply — a recreate converge re-declares its * targets by design, so demanding synth-omits would deadlock: recreate resumes * ONLY through the deploy (`drift repair` fails closed on its journal, verb * guard), and the deploy would be blocked by this very finding. Suppression is * safe because the [G2] hazard is CloudFormation UPDATING a dead-tracked * resource, and no in-flight phase lets that happen: * - captured / flip-verified: the snapshot flip is metadata-only * (DeletionPolicy + marker) — CFN never touches the physical resource; the * forget arm applies the same `applyRetainFlip` machinery to dead-tracked * targets as its core function. * - flip-applied: the surgical removal DELETEs the target from the template; * a physically-absent resource deletes as not-found success, and a genuine * DELETE_FAILED is caught fail-closed by `findRemovalDeletionFailures` * with a terminating cure. * - removal-applied: the target is already stripped — the deploy is pure * converge and re-creates it. * - `runRecreatePreFlight` probes never block an absent target (NotFound → * not blocked; template-absent targets skip probes entirely). * The surgery re-runs before the converge because the pinned-name finding * re-fires at the destruction gate while the journal is open (resumability * rides the deploy path); if the config no longer declares the target, the * converge removes the dead-tracked resource (not-found delete) and the * post-deploy sweep closes the journal as removed-instead-of-re-created. * DONE journals never suppress — `listActiveRemediationOps` excludes them. * * [G3] Degenerate third variant — config reverted byte-identically: when the * operator reverts the offending configuration change while the journal is * still PRE-flip-applied (captured/flip-verified), the plan carries no * pinned-name replacement, so the gate never re-fires and no surgery resumes; * the converge deploys a template identical to the live one, and the closure * predicate can never account for the targets (present, and pre-flip-applied * phases issue no closure probe — the retained bucket is empty by * construction), so the journal wedges open. Worse, a byte-identical revert * leaves the stack hash-unchanged, so it is EXCLUDED from the deploy set * (`getStackNamesToDeploy`) and this partition never probes it — the * recreate-converging warn is unreachable in exactly the degenerate that * needs it. The drift surfaces (`fjall drift detect` / `fjall drift repair` * open-operation and deferred-cure lines) are therefore the only * DISCOVERABLE teach, which is why they carry the same two cures. (At * flip-applied or later the converge strips the marker and the closure probe * verifies the reverted target live — closing it as `recreatedResources`.) * When the warn IS reachable it carries the two TERMINATING cures * (`formatRecreateInFlightCures` [G1]): re-apply the replacement-requiring * change (the recreate completes) or remove the resource from the config * (the operation closes as removed). Suppression remains correct throughout * — the wedge is a stale journal, never a CloudFormation hazard. */ export declare function partitionPreFlightByRemediation(deps: PreFlightRemediationDeps, confirmed: readonly DriftFinding[], options?: ForgetOrchestrationOptions): Promise; export interface ByNecessityConvergeTarget { stackName: string; logicalId: string; opId: string; } export interface ByNecessityConvergeCheck { /** Targets re-proven absent against the owning service at THIS deploy's * pre-flight (by the fresh probe here, or by the pre-flight's own * confirmed-deleted finding for the same target). */ verifiedAbsent: ByNecessityConvergeTarget[]; /** Targets whose absence could NOT be re-proven — the converge must not * run. `reason` is identifier-only plus the probe detail, which * `probePhysicalPresence` pre-masks at construction. */ blocked: Array; } /** * Converge-time absence re-verification for flipless-BY-NECESSITY targets — * and it must run on EVERY converge, independent of drift findings. A * by-necessity target reaches the converge with its live DeletionPolicy * still Delete (the Retain flip was structurally impossible), so the * converge's removal issues a REAL physical delete. Its safety property is * verified absence — but absence is point-in-time: a target recreated * out-of-band after flip-applied probes PRESENT at the pre-flight, produces * no confirmed finding, and therefore never reaches * `partitionPreFlightByRemediation` — the finding-driven [G2] path has * nothing to check exactly when the danger appears. This pass closes that * hole by re-proving absence from the journal side: every flip-applied * forget journal's `fliplessByNecessity` targets are re-probed against the * owning service unless this pre-flight already confirmed them deleted. * Anything not provably absent blocks the deploy (fail closed — proceeding * would destroy the recreation). A target the synth still DECLARES is * skipped: this deploy issues no removal for it, so no delete can fire. * * The probe routes through `probeSuspects`, never `probePhysicalPresence` * directly: the converge's removal deletes whatever physical id the stack * CURRENTLY tracks for the logicalId, so absence must be proven at that id. * The listing-outranks rule re-resolves it — a target replaced under the * same logicalId out-of-band probes at the live successor; probing the * journal-captured id would re-confirm absence at a dead address while the * converge destroys the live recreation. */ export declare function verifyByNecessityBeforeConverge(deps: PreFlightRemediationDeps, stackNames: readonly string[], freshlyConfirmed: ReadonlyMap>, options?: ForgetOrchestrationOptions): Promise; export interface CompleteForgetAfterDeployParams { stackNames: readonly string[]; /** When set, the sweep ALSO visits stacks outside `stackNames` that hold * an open RECREATE journal recorded for this app — a resumed deploy can * legitimately skip a stack whose re-create already landed, and this sweep * is the recreate arm's sole closure point. */ appName?: string; onLog?: (message: string, level: "info" | "warn") => void; abortSignal?: AbortSignal; } /** * Cross-surface resume (plan § 3.2): after a successful deploy, close any * remediation operation (forget AND recreate — step 9b is the recreate arm's * sole closure point) whose targets the deploy just converged — regardless of * which surface started the surgery. Best-effort: a completion failure warns * and never fails the deploy that just succeeded. */ export declare function completeForgetAfterDeploy(aws: AwsProvider, params: CompleteForgetAfterDeployParams, options?: ForgetOrchestrationOptions): Promise;