@fjall/deploy-core 2.25.0 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/index.d.ts +1 -1
  3. package/dist/src/aws/index.js +1 -1
  4. package/dist/src/aws/organisations/backup.d.ts +7 -2
  5. package/dist/src/aws/organisations/backup.js +2 -2
  6. package/dist/src/aws/organisations/index.d.ts +2 -2
  7. package/dist/src/aws/organisations/index.js +1 -1
  8. package/dist/src/aws/organisations/oidcProvider.d.ts +37 -0
  9. package/dist/src/aws/organisations/oidcProvider.js +1 -1
  10. package/dist/src/aws/organisations/organisation.d.ts +21 -0
  11. package/dist/src/aws/organisations/organisation.js +1 -1
  12. package/dist/src/aws/organisations/organisationalUnits.d.ts +15 -0
  13. package/dist/src/aws/organisations/organisationalUnits.js +1 -1
  14. package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +45 -0
  15. package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
  16. package/dist/src/aws/utils/cloudformationEventTypes.d.ts +1 -1
  17. package/dist/src/aws/utils/cloudformationEvents.d.ts +2 -1
  18. package/dist/src/aws/utils/cloudformationEvents.js +1 -1
  19. package/dist/src/aws/utils/stackStatus.js +1 -1
  20. package/dist/src/index.d.ts +13 -2
  21. package/dist/src/index.js +1 -1
  22. package/dist/src/orchestration/activeDeploymentGuard.d.ts +41 -0
  23. package/dist/src/orchestration/activeDeploymentGuard.js +8 -4
  24. package/dist/src/orchestration/application/applicationDeploy.d.ts +9 -0
  25. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  26. package/dist/src/orchestration/application/applicationDeployHelpers.d.ts +20 -3
  27. package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
  28. package/dist/src/orchestration/application/applicationDestroy.js +1 -1
  29. package/dist/src/orchestration/application/approvalGate.d.ts +15 -1
  30. package/dist/src/orchestration/application/approvalGate.js +1 -1
  31. package/dist/src/orchestration/application/databaseEndpointReconcile.d.ts +77 -0
  32. package/dist/src/orchestration/application/databaseEndpointReconcile.js +1 -0
  33. package/dist/src/orchestration/application/deploySessionPolicy.d.ts +9 -1
  34. package/dist/src/orchestration/application/deploySessionPolicy.js +1 -1
  35. package/dist/src/orchestration/application/destructionGate.d.ts +164 -0
  36. package/dist/src/orchestration/application/destructionGate.js +5 -0
  37. package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
  38. package/dist/src/orchestration/application/ecrCacheRepository.d.ts +14 -0
  39. package/dist/src/orchestration/application/ecrCacheRepository.js +1 -0
  40. package/dist/src/orchestration/application/leaseGate.d.ts +30 -0
  41. package/dist/src/orchestration/application/leaseGate.js +1 -0
  42. package/dist/src/orchestration/application/plan/approvalToken.d.ts +15 -0
  43. package/dist/src/orchestration/application/plan/approvalToken.js +1 -1
  44. package/dist/src/orchestration/application/plan/changeSetEscalation.d.ts +46 -0
  45. package/dist/src/orchestration/application/plan/changeSetEscalation.js +1 -0
  46. package/dist/src/orchestration/application/plan/computeDeployPlan.d.ts +13 -0
  47. package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -1
  48. package/dist/src/orchestration/application/plan/index.d.ts +4 -1
  49. package/dist/src/orchestration/application/plan/index.js +1 -1
  50. package/dist/src/orchestration/application/plan/loadDeployPlan.d.ts +32 -0
  51. package/dist/src/orchestration/application/plan/loadDeployPlan.js +1 -0
  52. package/dist/src/orchestration/application/plan/pinnedNames.d.ts +31 -0
  53. package/dist/src/orchestration/application/plan/pinnedNames.js +1 -0
  54. package/dist/src/orchestration/application/plan/registryOverlay.d.ts +83 -0
  55. package/dist/src/orchestration/application/plan/registryOverlay.js +1 -0
  56. package/dist/src/orchestration/application/plan/renderDeployPlan.d.ts +4 -1
  57. package/dist/src/orchestration/application/plan/renderDeployPlan.js +1 -1
  58. package/dist/src/orchestration/application/plan/types.d.ts +31 -0
  59. package/dist/src/orchestration/application/plan/types.js +1 -0
  60. package/dist/src/orchestration/contextHelpers.d.ts +37 -1
  61. package/dist/src/orchestration/contextHelpers.js +1 -1
  62. package/dist/src/orchestration/drift/classifyDriftFailure.d.ts +31 -0
  63. package/dist/src/orchestration/drift/classifyDriftFailure.js +1 -0
  64. package/dist/src/orchestration/drift/detectStackDrift.d.ts +37 -0
  65. package/dist/src/orchestration/drift/detectStackDrift.js +1 -0
  66. package/dist/src/orchestration/drift/driftPreFlight.d.ts +73 -0
  67. package/dist/src/orchestration/drift/driftPreFlight.js +7 -0
  68. package/dist/src/orchestration/drift/driftProbe.d.ts +113 -0
  69. package/dist/src/orchestration/drift/driftProbe.js +1 -0
  70. package/dist/src/orchestration/drift/driftSuspectJournal.d.ts +40 -0
  71. package/dist/src/orchestration/drift/driftSuspectJournal.js +1 -0
  72. package/dist/src/orchestration/drift/preFlightGate.d.ts +44 -0
  73. package/dist/src/orchestration/drift/preFlightGate.js +2 -0
  74. package/dist/src/orchestration/index.d.ts +19 -0
  75. package/dist/src/orchestration/index.js +1 -1
  76. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.d.ts +7 -0
  77. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.js +1 -1
  78. package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
  79. package/dist/src/orchestration/organisation/organisationDestroy.js +3 -3
  80. package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +8 -1
  81. package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -1
  82. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +5 -5
  83. package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +28 -2
  84. package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -1
  85. package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
  86. package/dist/src/orchestration/remediation/driftRepairGate.d.ts +66 -0
  87. package/dist/src/orchestration/remediation/driftRepairGate.js +1 -0
  88. package/dist/src/orchestration/remediation/forensicsCapture.d.ts +49 -0
  89. package/dist/src/orchestration/remediation/forensicsCapture.js +1 -0
  90. package/dist/src/orchestration/remediation/forgetResource.d.ts +423 -0
  91. package/dist/src/orchestration/remediation/forgetResource.js +1 -0
  92. package/dist/src/orchestration/remediation/pinRemediation.d.ts +121 -0
  93. package/dist/src/orchestration/remediation/pinRemediation.js +1 -0
  94. package/dist/src/orchestration/remediation/postFailureRepairOffer.d.ts +77 -0
  95. package/dist/src/orchestration/remediation/postFailureRepairOffer.js +14 -0
  96. package/dist/src/orchestration/remediation/recreatePreFlight.d.ts +47 -0
  97. package/dist/src/orchestration/remediation/recreatePreFlight.js +1 -0
  98. package/dist/src/orchestration/remediation/recreateResource.d.ts +110 -0
  99. package/dist/src/orchestration/remediation/recreateResource.js +1 -0
  100. package/dist/src/orchestration/remediation/remediationJournal.d.ts +88 -0
  101. package/dist/src/orchestration/remediation/remediationJournal.js +2 -0
  102. package/dist/src/orchestration/remediation/removalUpdate.d.ts +135 -0
  103. package/dist/src/orchestration/remediation/removalUpdate.js +1 -0
  104. package/dist/src/orchestration/remediation/retainFlip.d.ts +294 -0
  105. package/dist/src/orchestration/remediation/retainFlip.js +1 -0
  106. package/dist/src/services/application/ApplicationStackService.d.ts +5 -3
  107. package/dist/src/services/application/ApplicationStackService.js +1 -1
  108. package/dist/src/services/application/applicationStackHelpers.d.ts +8 -6
  109. package/dist/src/services/application/applicationStackHelpers.js +3 -3
  110. package/dist/src/services/index.d.ts +2 -2
  111. package/dist/src/services/index.js +1 -1
  112. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
  113. package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +15 -0
  114. package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -1
  115. package/dist/src/services/infrastructure/CdkOutputAnalyser.d.ts +8 -0
  116. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +2 -1
  117. package/dist/src/services/infrastructure/CdkService.js +3 -3
  118. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +7 -0
  119. package/dist/src/services/infrastructure/CfnRegistryService.d.ts +68 -0
  120. package/dist/src/services/infrastructure/CfnRegistryService.js +1 -0
  121. package/dist/src/services/infrastructure/CloudFormationService.d.ts +5 -2
  122. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  123. package/dist/src/services/infrastructure/EcsServiceResolver.d.ts +23 -3
  124. package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -1
  125. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
  126. package/dist/src/services/infrastructure/cfnRegistrySeed.d.ts +10 -0
  127. package/dist/src/services/infrastructure/cfnRegistrySeed.js +1 -0
  128. package/dist/src/services/infrastructure/changeSetProbe.d.ts +88 -0
  129. package/dist/src/services/infrastructure/changeSetProbe.js +1 -0
  130. package/dist/src/services/infrastructure/index.d.ts +2 -0
  131. package/dist/src/services/infrastructure/index.js +1 -1
  132. package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -0
  133. package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
  134. package/dist/src/types/callbackKeys.d.ts +1 -1
  135. package/dist/src/types/callbackKeys.js +1 -1
  136. package/dist/src/types/callbacks.d.ts +22 -0
  137. package/dist/src/types/config/entitlements.d.ts +9 -0
  138. package/dist/src/types/config/orgConfig.d.ts +6 -0
  139. package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
  140. package/dist/src/types/deploymentEventSchema.d.ts +1 -0
  141. package/dist/src/types/deploymentEventSchema.js +1 -1
  142. package/dist/src/types/destruction.d.ts +212 -0
  143. package/dist/src/types/destruction.js +1 -0
  144. package/dist/src/types/drift.d.ts +72 -0
  145. package/dist/src/types/drift.js +1 -0
  146. package/dist/src/types/index.d.ts +8 -2
  147. package/dist/src/types/index.js +1 -1
  148. package/dist/src/types/params.d.ts +161 -0
  149. package/dist/src/types/remediation.d.ts +126 -0
  150. package/dist/src/types/remediation.js +1 -0
  151. package/package.json +5 -4
@@ -0,0 +1,423 @@
1
+ /**
2
+ * The `forget` orchestrator — the composing layer over the remediation
3
+ * primitives (unlock-engine layering: privileged primitives in
4
+ * `retainFlip.ts`/`forensicsCapture.ts`, this module composes):
5
+ *
6
+ * captured → flip-verified → flip-applied → (converge deploy) →
7
+ * removal-applied → done
8
+ *
9
+ * `runForgetSurgery` drives phases 1–3. The removal phase is deliberately NOT
10
+ * here: it is the normal converge deploy (plan § 3.2 phase 4 — the synth no
11
+ * longer contains the resource, CFN sees delete-with-Retain and skips the
12
+ * physical call, and every other pending change lands in the same update).
13
+ * `completeForgetAfterConverge` closes the journal once a deploy (any surface
14
+ * — CLI, worker, another operator) has removed the targets from the live
15
+ * template.
16
+ *
17
+ * Consent is the CALLER's job (the destruction ceremony runs before surgery);
18
+ * `assessForgetResumability` tells the caller whether the ceremony may be
19
+ * skipped — a journal at any phase, or the in-template `fjall:forget` marker
20
+ * [G3], proves the destructive act was already consented (capture and flip
21
+ * only ever run post-ceremony), so resume never re-ceremonies (plan § 3.2).
22
+ * Targets advanced via the FLIPLESS fast-path (defect #5 — live policies
23
+ * already Retain, no UpdateStack, no marker) or the FLIPLESS-BY-NECESSITY
24
+ * path (same incident, outbound direction — the flip is structurally
25
+ * impossible because the target's own references resolve against dead
26
+ * siblings, and the target is verified physically deleted so the converge's
27
+ * removal is a physical no-op) have no AWS-side consent trace: with the
28
+ * journal lost they re-ceremony, never silently resume.
29
+ *
30
+ * Every phase re-derives its precondition from live state: the flip
31
+ * derivation, diff-guard and change-set proof re-run on every pass (the
32
+ * journalled proof is an audit record, never a skip-token), which is also
33
+ * what "live-template digest mismatch ⇒ re-verify from phase 2" degrades to.
34
+ *
35
+ * The closure machinery (`resolveConvergedTargets`,
36
+ * `completeForgetAfterConverge`, `completeForgetAfterDeploy`) serves BOTH
37
+ * surgery verbs — the recreate orchestrator (`recreateResource.ts`) closes
38
+ * through it, with the marker key and terminal field derived from the
39
+ * journal's verb. The surgery entry points here are forget-only and fail
40
+ * closed on a cross-verb journal (the opId derivation excludes the verb).
41
+ *
42
+ * Failure messages are pre-masked at construction.
43
+ */
44
+ import { type Result } from "@fjall/generator";
45
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
46
+ import type { DriftFinding } from "../../types/drift.js";
47
+ import { type RemediationJournalRecord, type RemediationPhase, type RemediationTarget } from "../../types/remediation.js";
48
+ import { type DriftProbeOptions } from "../drift/driftProbe.js";
49
+ import { type DriftJournalOptions } from "../drift/driftSuspectJournal.js";
50
+ import { type ForensicsCaptureOptions } from "./forensicsCapture.js";
51
+ import { type RemediationJournalOptions } from "./remediationJournal.js";
52
+ import { type RetainFlipOptions } from "./retainFlip.js";
53
+ export interface ForgetOrchestrationOptions {
54
+ journal?: RemediationJournalOptions;
55
+ forensics?: ForensicsCaptureOptions;
56
+ flip?: RetainFlipOptions;
57
+ driftJournal?: DriftJournalOptions;
58
+ /** Poll caps + clock for the closure probe's drift-API fallback. */
59
+ probe?: DriftProbeOptions;
60
+ /** Injectable clock for journal/marker timestamps. */
61
+ now?: () => Date;
62
+ }
63
+ export interface ForgetSurgeryParams {
64
+ stackName: string;
65
+ targets: readonly RemediationTarget[];
66
+ /** The deploy target that owns the stack — names the converge command. */
67
+ appName?: string;
68
+ /** Drift-probe evidence persisted in the forensics bundle. */
69
+ findings?: readonly DriftFinding[];
70
+ /** Audit trail: the exact physical names consented at the ceremony. */
71
+ consentedPhysicalNames?: readonly string[];
72
+ /**
73
+ * How to treat a journal already at "done" for this opId. The default
74
+ * ("report-completed") suits an explicit `drift resume <opId>` — the
75
+ * operator asked about THAT operation. A REPAIR pass targeting a fresh
76
+ * finding must pass "archive-and-restart": the done journal belongs to a
77
+ * prior incarnation of the same stack + logical-ID set (opId has no
78
+ * incarnation component) and would otherwise false-report completion while
79
+ * the new deletion stays wedged.
80
+ */
81
+ doneJournalPolicy?: "report-completed" | "archive-and-restart";
82
+ onProgress?: (message: string) => void;
83
+ abortSignal?: AbortSignal;
84
+ }
85
+ export interface ForgetSurgeryReport {
86
+ opId: string;
87
+ /** Journal phase after this pass. */
88
+ phase: RemediationPhase;
89
+ /** True until the converge deploy removes the targets from the template. */
90
+ convergePending: boolean;
91
+ forensicsPath?: string;
92
+ journal: RemediationJournalRecord;
93
+ }
94
+ /** Shared with the recreate orchestrator (`recreateResource.ts`). */
95
+ export interface LiveStackState {
96
+ templateBody: string;
97
+ capabilities: string[];
98
+ /** Live parameter keys — echoed UsePreviousValue on both flip CFN calls. */
99
+ parameterKeys: string[];
100
+ }
101
+ export declare function fetchLiveStackState(aws: AwsProvider, stackName: string, abortSignal?: AbortSignal): Promise<Result<LiveStackState, Error>>;
102
+ /**
103
+ * [G1] The two TERMINATING cures for an in-flight recreate operation, shared
104
+ * by every surface that meets one it cannot itself complete (the forget verb
105
+ * guards here, the post-failure repair offer, the [G2] recreate-converging
106
+ * warn, and the CLI drift surfaces — deferred-cure, open-operation and
107
+ * in-flight-remediation lines [H1], the only DISCOVERABLE teach when a
108
+ * byte-identical revert excludes the stack from the deploy pre-flight).
109
+ * A bare "re-run the deploy" teach is circular when the offending
110
+ * configuration change was reverted: the plan then carries no pinned-name
111
+ * replacement, the gate never re-fires, and the converge no-ops against the
112
+ * open journal forever. Only a config change terminates the operation —
113
+ * re-applying the replacement (the recreate completes) or removing the
114
+ * resource (the operation closes as removed).
115
+ */
116
+ export declare function formatRecreateInFlightCures(appName?: string): string;
117
+ export type ForgetResumeSource = "journal" | "marker" | "none";
118
+ export interface ForeignMarkerOperation {
119
+ opId: string;
120
+ /**
121
+ * The logical IDs still carrying this opId's marker in the LIVE template
122
+ * [G3]. This may be a PROPER SUBSET of the operation's original target set
123
+ * (stripped markers are invisible) — adoption MUST prove completeness by
124
+ * hash identity: `computeRemediationOpId(stackName, markedLogicalIds)`
125
+ * reproducing `opId`.
126
+ */
127
+ markedLogicalIds: string[];
128
+ /** logicalId → CFN resource Type from the live template — lets the
129
+ * adoption seam build targets for recovered IDs with no drift finding. */
130
+ markedResourceTypes: Record<string, string>;
131
+ }
132
+ export interface ForgetResumeAssessment {
133
+ opId: string;
134
+ /**
135
+ * True when the destructive act was already consented — a journal at any
136
+ * phase (capture only runs post-ceremony) or the in-template fjall:forget
137
+ * marker [G3]. The caller skips the ceremony; a fresh operation must not.
138
+ */
139
+ alreadyConsented: boolean;
140
+ source: ForgetResumeSource;
141
+ journal?: RemediationJournalRecord;
142
+ /**
143
+ * Marker-only in-flight operations owned by OTHER opIds found on the
144
+ * assessed targets ([G3] — their journal is lost or on another machine).
145
+ * The caller must never mint an operation whose targets overlap these:
146
+ * adopt each op with exactly its own recovered set — first proving the
147
+ * recovery is COMPLETE via hash identity (see `ForeignMarkerOperation`) —
148
+ * or defer. Empty on the journal-resume path (not scanned — the journal
149
+ * already decides).
150
+ */
151
+ foreignMarkers: ForeignMarkerOperation[];
152
+ }
153
+ /**
154
+ * Pre-ceremony check: is this forget operation already in flight? Journal
155
+ * first (local resume record), then the live template's fjall:forget marker
156
+ * (AWS-side truth that survives machine loss — the journal-lost case).
157
+ */
158
+ export declare function assessForgetResumability(aws: AwsProvider, params: Pick<ForgetSurgeryParams, "stackName" | "targets" | "abortSignal">, options?: ForgetOrchestrationOptions): Promise<Result<ForgetResumeAssessment, Error>>;
159
+ /**
160
+ * Phases 1–3 of the two-deploy forget: forensics capture (abort on failure —
161
+ * no capture, no mutate), flip derivation + both proofs, flip apply, then the
162
+ * completion check (a converge that already ran closes the journal in the
163
+ * same pass). Idempotent and resumable: safe to re-run at any phase.
164
+ */
165
+ export declare function runForgetSurgery(aws: AwsProvider, params: ForgetSurgeryParams, options?: ForgetOrchestrationOptions): Promise<Result<ForgetSurgeryReport, Error>>;
166
+ export interface CompleteForgetParams {
167
+ journal: RemediationJournalRecord;
168
+ abortSignal?: AbortSignal;
169
+ }
170
+ export type CompleteForgetOutcome = {
171
+ status: "done";
172
+ journal: RemediationJournalRecord;
173
+ }
174
+ /** At least one target is still present (flipped) or unverifiable — the
175
+ * converge deploy has not run, or ran and could not be verified. The
176
+ * per-target buckets let callers act on the ACTUAL failure set instead of
177
+ * the whole target list [H4]. */
178
+ | {
179
+ status: "converge-pending";
180
+ journal: RemediationJournalRecord;
181
+ /** Targets no longer in the live template — their removal landed. */
182
+ absent: string[];
183
+ /** Targets verified retained-by-redeclaration (recreate: re-created). */
184
+ retained: string[];
185
+ /** Targets neither absent nor verifiably retained: still carrying this
186
+ * op's marker (converge not run, or the removal was reverted), or
187
+ * present-unmarked with a closure-probe verdict that fails closed
188
+ * (confirmed-deleted, unconfirmed, stack-missing, membership-gate
189
+ * "present", probe failure). Every entry is template-PRESENT at the
190
+ * closure read by construction. */
191
+ unverified: string[];
192
+ };
193
+ /**
194
+ * Post-converge closure: when every target is either gone from the live
195
+ * template or verified retained-by-redeclaration (the on-demand closure probe
196
+ * in `resolveConvergedTargets`), advance the journal removal-applied → done
197
+ * and clear the forgotten logical IDs from the drift suspect journal (the
198
+ * wedge is gone). Called by `drift resume`, by a re-run of `drift repair`,
199
+ * and by the deploy engine after a successful deploy (cross-surface resume,
200
+ * plan § 3.2) — so the mainline converge deploy closes retained targets
201
+ * naturally at completion.
202
+ *
203
+ * Serves BOTH surgery verbs (the marker key and terminal field derive from
204
+ * `journal.verb`): for a recreate journal the `retained` bucket is the
205
+ * re-created (or operator-reverted) targets, recorded as `recreatedResources`;
206
+ * absent targets are those the converge that just ran no longer declares.
207
+ * Consent alone does not prove their deletion executed [G6] — the
208
+ * `probeAbsentClosureTargets` call below physically verifies each absent
209
+ * target, recording still-alive ones as `orphanedResources` and unverifiable
210
+ * ones as `unverifiedRemovals` rather than claiming an executed deletion.
211
+ */
212
+ export declare function completeForgetAfterConverge(aws: AwsProvider, params: CompleteForgetParams, options?: ForgetOrchestrationOptions): Promise<Result<CompleteForgetOutcome, Error>>;
213
+ export interface PreFlightRemediationPartition {
214
+ /** Findings whose block stands — includes flip-not-live and still-declared
215
+ * findings below, so callers gate on `blocked.length` alone. */
216
+ blocked: DriftFinding[];
217
+ /**
218
+ * Blocked findings whose operation exists but has NOT applied the flip —
219
+ * the deploy would still wedge; the block message adds the resume hint.
220
+ */
221
+ inProgressOpIds: string[];
222
+ /**
223
+ * Findings whose flip is verifiably live AND whose synthesised template
224
+ * omits the target: THIS deploy is the converge deploy — the block is
225
+ * suppressed so the surgery can complete.
226
+ */
227
+ converging: Array<{
228
+ finding: DriftFinding;
229
+ opId: string;
230
+ }>;
231
+ /** Journal says flip-applied but the live template VERIFIABLY no longer
232
+ * carries Retain + our marker — surgery must re-run before deploying. */
233
+ flipNotLive: Array<{
234
+ finding: DriftFinding;
235
+ opId: string;
236
+ }>;
237
+ /** The live template could NOT be read, so the flip's liveness is
238
+ * unverifiable — fail-closed block whose cure is retrying the deploy, never
239
+ * the flip-stripped copy. `message` is pre-masked at construction. */
240
+ flipUnverified: Array<{
241
+ finding: DriftFinding;
242
+ opId: string;
243
+ message: string;
244
+ }>;
245
+ /** Flip is live but the synth still declares the target — this deploy is
246
+ * NOT the converge; the resource must leave the fjall config first. */
247
+ stillDeclared: Array<{
248
+ finding: DriftFinding;
249
+ opId: string;
250
+ }>;
251
+ /**
252
+ * SUPPRESSED findings covered by an in-flight RECREATE journal — the deploy
253
+ * itself is the cure, so blocking would deadlock (recreate resumes ONLY
254
+ * through the deploy; `drift repair` fails closed on its journal). Safe at
255
+ * every in-flight phase because the 3f surgery's removal update strips the
256
+ * dead-tracked target from the template BEFORE the converge — CloudFormation
257
+ * never updates the deleted resource, then the converge re-CREATEs it. See
258
+ * the phase-by-phase evidence in the function JSDoc below. Surfaced to the
259
+ * caller for the honest onLog line, never dropped silently.
260
+ */
261
+ recreateConverging: Array<{
262
+ finding: DriftFinding;
263
+ opId: string;
264
+ appName?: string;
265
+ }>;
266
+ }
267
+ export interface PreFlightRemediationDeps {
268
+ aws: AwsProvider;
269
+ /**
270
+ * Whether the incoming deploy's synthesised template still declares a
271
+ * logical ID (read from cdk.out at the deploy call site — see
272
+ * `synthTemplateDeclaresResource`). `undefined` = the synth template could
273
+ * not be read; suppression then rests on the live-flip check alone rather
274
+ * than deadlocking a genuine converge on an unreadable artefact.
275
+ */
276
+ synthDeclaresTarget: (stackName: string, logicalId: string) => Promise<boolean | undefined>;
277
+ /** Caller shutdown/cancel signal, threaded into the per-stack live-template
278
+ * probes; once aborted, remaining findings fail closed to `blocked`. */
279
+ abortSignal?: AbortSignal;
280
+ }
281
+ /**
282
+ * Answer the partition's synth-omission question from a cdk.out directory.
283
+ * Read failure returns `undefined` (debug-logged) — the caller decides how
284
+ * suppression degrades.
285
+ */
286
+ export declare function synthTemplateDeclaresResource(cdkOutPath: string, stackName: string, logicalId: string): Promise<boolean | undefined>;
287
+ /**
288
+ * The [G2] pre-flight × remediation-journal interaction (plan § 3.2 decision,
289
+ * recorded in the Phase 6 report): a confirmed-deleted finding covered by a
290
+ * remediation journal at phase ≥ flip-applied does NOT block the deploy —
291
+ * blocking would deadlock the orchestration whose removal phase IS the next
292
+ * deploy. Operations still pre-flip keep the block (deploying before the
293
+ * Retain flip would re-wedge) but name the resume command.
294
+ *
295
+ * The journal phase alone is never the suppression authority. Both must hold:
296
+ * (1) the live template still carries the Retain policies for the target and
297
+ * no FOREIGN marker (`readRetainFlipState` — stripped policies re-block
298
+ * to `drift repair`; the policies are the safety property CFN honours at
299
+ * the converge's delete, and flipless targets [defect #5] are markerless
300
+ * by design, so our own marker is corroborating rather than required).
301
+ * Flipless-BY-NECESSITY targets (journal `fliplessByNecessity`) pass (1)
302
+ * while policy-pending: the flip was structurally impossible and their
303
+ * safety property is verified physical absence — re-proven by the
304
+ * finding in hand, this pre-flight's own confirmed-deleted verdict;
305
+ * (2) the deploy's synthesised template OMITS the target — a deploy that
306
+ * still declares the resource is not the converge: cdk would strip the
307
+ * flip and re-wedge on the dead resource.
308
+ * A target already ABSENT from the live template passes (1) — its removal
309
+ * landed and the deploy cannot touch it. An UNREADABLE live template proves
310
+ * neither state: those findings fail closed to `flipUnverified` (cure: retry
311
+ * the deploy), never to `flipNotLive` — its re-run-repair cure would be a lie.
312
+ *
313
+ * RECREATE coverage suppresses unconditionally (refines sanction 10). The
314
+ * forget-shaped conditions cannot apply — a recreate converge re-declares its
315
+ * targets by design, so demanding synth-omits would deadlock: recreate resumes
316
+ * ONLY through the deploy (`drift repair` fails closed on its journal, verb
317
+ * guard), and the deploy would be blocked by this very finding. Suppression is
318
+ * safe because the [G2] hazard is CloudFormation UPDATING a dead-tracked
319
+ * resource, and no in-flight phase lets that happen:
320
+ * - captured / flip-verified: the snapshot flip is metadata-only
321
+ * (DeletionPolicy + marker) — CFN never touches the physical resource; the
322
+ * forget arm applies the same `applyRetainFlip` machinery to dead-tracked
323
+ * targets as its core function.
324
+ * - flip-applied: the surgical removal DELETEs the target from the template;
325
+ * a physically-absent resource deletes as not-found success, and a genuine
326
+ * DELETE_FAILED is caught fail-closed by `findRemovalDeletionFailures`
327
+ * with a terminating cure.
328
+ * - removal-applied: the target is already stripped — the deploy is pure
329
+ * converge and re-creates it.
330
+ * - `runRecreatePreFlight` probes never block an absent target (NotFound →
331
+ * not blocked; template-absent targets skip probes entirely).
332
+ * The surgery re-runs before the converge because the pinned-name finding
333
+ * re-fires at the destruction gate while the journal is open (resumability
334
+ * rides the deploy path); if the config no longer declares the target, the
335
+ * converge removes the dead-tracked resource (not-found delete) and the
336
+ * post-deploy sweep closes the journal as removed-instead-of-re-created.
337
+ * DONE journals never suppress — `listActiveRemediationOps` excludes them.
338
+ *
339
+ * [G3] Degenerate third variant — config reverted byte-identically: when the
340
+ * operator reverts the offending configuration change while the journal is
341
+ * still PRE-flip-applied (captured/flip-verified), the plan carries no
342
+ * pinned-name replacement, so the gate never re-fires and no surgery resumes;
343
+ * the converge deploys a template identical to the live one, and the closure
344
+ * predicate can never account for the targets (present, and pre-flip-applied
345
+ * phases issue no closure probe — the retained bucket is empty by
346
+ * construction), so the journal wedges open. Worse, a byte-identical revert
347
+ * leaves the stack hash-unchanged, so it is EXCLUDED from the deploy set
348
+ * (`getStackNamesToDeploy`) and this partition never probes it — the
349
+ * recreate-converging warn is unreachable in exactly the degenerate that
350
+ * needs it. The drift surfaces (`fjall drift detect` / `fjall drift repair`
351
+ * open-operation and deferred-cure lines) are therefore the only
352
+ * DISCOVERABLE teach, which is why they carry the same two cures. (At
353
+ * flip-applied or later the converge strips the marker and the closure probe
354
+ * verifies the reverted target live — closing it as `recreatedResources`.)
355
+ * When the warn IS reachable it carries the two TERMINATING cures
356
+ * (`formatRecreateInFlightCures` [G1]): re-apply the replacement-requiring
357
+ * change (the recreate completes) or remove the resource from the config
358
+ * (the operation closes as removed). Suppression remains correct throughout
359
+ * — the wedge is a stale journal, never a CloudFormation hazard.
360
+ */
361
+ export declare function partitionPreFlightByRemediation(deps: PreFlightRemediationDeps, confirmed: readonly DriftFinding[], options?: ForgetOrchestrationOptions): Promise<PreFlightRemediationPartition>;
362
+ export interface ByNecessityConvergeTarget {
363
+ stackName: string;
364
+ logicalId: string;
365
+ opId: string;
366
+ }
367
+ export interface ByNecessityConvergeCheck {
368
+ /** Targets re-proven absent against the owning service at THIS deploy's
369
+ * pre-flight (by the fresh probe here, or by the pre-flight's own
370
+ * confirmed-deleted finding for the same target). */
371
+ verifiedAbsent: ByNecessityConvergeTarget[];
372
+ /** Targets whose absence could NOT be re-proven — the converge must not
373
+ * run. `reason` is identifier-only plus the probe detail, which
374
+ * `probePhysicalPresence` pre-masks at construction. */
375
+ blocked: Array<ByNecessityConvergeTarget & {
376
+ reason: string;
377
+ }>;
378
+ }
379
+ /**
380
+ * Converge-time absence re-verification for flipless-BY-NECESSITY targets —
381
+ * and it must run on EVERY converge, independent of drift findings. A
382
+ * by-necessity target reaches the converge with its live DeletionPolicy
383
+ * still Delete (the Retain flip was structurally impossible), so the
384
+ * converge's removal issues a REAL physical delete. Its safety property is
385
+ * verified absence — but absence is point-in-time: a target recreated
386
+ * out-of-band after flip-applied probes PRESENT at the pre-flight, produces
387
+ * no confirmed finding, and therefore never reaches
388
+ * `partitionPreFlightByRemediation` — the finding-driven [G2] path has
389
+ * nothing to check exactly when the danger appears. This pass closes that
390
+ * hole by re-proving absence from the journal side: every flip-applied
391
+ * forget journal's `fliplessByNecessity` targets are re-probed against the
392
+ * owning service unless this pre-flight already confirmed them deleted.
393
+ * Anything not provably absent blocks the deploy (fail closed — proceeding
394
+ * would destroy the recreation). A target the synth still DECLARES is
395
+ * skipped: this deploy issues no removal for it, so no delete can fire.
396
+ *
397
+ * The probe routes through `probeSuspects`, never `probePhysicalPresence`
398
+ * directly: the converge's removal deletes whatever physical id the stack
399
+ * CURRENTLY tracks for the logicalId, so absence must be proven at that id.
400
+ * The listing-outranks rule re-resolves it — a target replaced under the
401
+ * same logicalId out-of-band probes at the live successor; probing the
402
+ * journal-captured id would re-confirm absence at a dead address while the
403
+ * converge destroys the live recreation.
404
+ */
405
+ export declare function verifyByNecessityBeforeConverge(deps: PreFlightRemediationDeps, stackNames: readonly string[], freshlyConfirmed: ReadonlyMap<string, ReadonlySet<string>>, options?: ForgetOrchestrationOptions): Promise<ByNecessityConvergeCheck>;
406
+ export interface CompleteForgetAfterDeployParams {
407
+ stackNames: readonly string[];
408
+ /** When set, the sweep ALSO visits stacks outside `stackNames` that hold
409
+ * an open RECREATE journal recorded for this app — a resumed deploy can
410
+ * legitimately skip a stack whose re-create already landed, and this sweep
411
+ * is the recreate arm's sole closure point. */
412
+ appName?: string;
413
+ onLog?: (message: string, level: "info" | "warn") => void;
414
+ abortSignal?: AbortSignal;
415
+ }
416
+ /**
417
+ * Cross-surface resume (plan § 3.2): after a successful deploy, close any
418
+ * remediation operation (forget AND recreate — step 9b is the recreate arm's
419
+ * sole closure point) whose targets the deploy just converged — regardless of
420
+ * which surface started the surgery. Best-effort: a completion failure warns
421
+ * and never fails the deploy that just succeeded.
422
+ */
423
+ export declare function completeForgetAfterDeploy(aws: AwsProvider, params: CompleteForgetAfterDeployParams, options?: ForgetOrchestrationOptions): Promise<void>;
@@ -0,0 +1 @@
1
+ import{readFile as ge}from"node:fs/promises";import{join as he}from"node:path";import{CloudFormationClient as me,DescribeStacksCommand as ye,GetTemplateCommand as ve}from"@aws-sdk/client-cloudformation";import{success as $,failure as j}from"@fjall/generator";import{getErrorMessage as q,maskSensitiveOutput as _}from"@fjall/util";import{logger as J}from"@fjall/util/logger";import{composeSdkAbortSignal as Z,isAborted as ee}from"../../aws/organisations/types.js";import{isRemediationPlaceholderPhysicalId as H,remediationPhaseRank as A}from"../../types/remediation.js";import{DriftProbe as te,MEMBERSHIP_GATE_PROBE_METHOD as re,probePhysicalPresence as ae}from"../drift/driftProbe.js";import{clearDriftSuspects as be,readDriftSuspects as we,recordDriftSuspects as Ie}from"../drift/driftSuspectJournal.js";import{captureRemediationForensics as $e}from"./forensicsCapture.js";import{findDeadReferencesFromTargets as ke,findTargetReferrers as Se}from"./removalUpdate.js";import{archiveCompletedRemediationJournal as Re,computeRemediationOpId as ne,formatArchivedOperationNotice as Ne,formatCompletedOperationNotice as Te,listActiveRemediationOps as z,listActiveRemediationOpsForContext as Pe,readRemediationJournal as oe,sweepExpiredRemediationJournals as Ce,writeRemediationJournal as ie}from"./remediationJournal.js";import{applyRetainFlip as je,computeTemplateDigest as se,deriveRetainFlipTemplate as Oe,findMarkedResourcesByOpId as V,proveFlipMetadataOnly as Ee,readResourceTypes as Fe,readRetainFlipState as M,verifyRetainFlipDiff as Ae,FORGET_MARKER_KEY as ce,RECREATE_MARKER_KEY as Be}from"./retainFlip.js";const U="ForgetResource";async function x(n,t,s){const r=n.getClient(me);try{const d=await r.send(new ve({StackName:t,TemplateStage:"Original"}),{abortSignal:Z(s)});if(d.TemplateBody===void 0||d.TemplateBody==="")return j(new Error(`GetTemplate returned an empty template body for ${t}`));const o=(await r.send(new ye({StackName:t}),{abortSignal:Z(s)})).Stacks?.[0];return $({templateBody:d.TemplateBody,capabilities:o?.Capabilities??[],parameterKeys:(o?.Parameters??[]).flatMap(c=>c.ParameterKey!==void 0?[c.ParameterKey]:[])})}catch(d){return j(new Error(`Could not read the live state of ${t}: ${_(q(d))}`))}}function de(n){const t=`'fjall deploy${n!==void 0?` ${n}`:""}'`;return`Two cures terminate it: re-apply the replacement-requiring configuration change and re-run ${t}, re-consenting when prompted \u2014 the recreate completes; or remove the resource from the fjall config and run ${t} \u2014 the operation closes as removed.`}async function Ze(n,t,s){const r=t.targets.map(f=>f.logicalId),d=ne(t.stackName,r),a=await oe(n.getAccountId(),n.getRegion(),t.stackName,d,s?.journal);if(a!==null&&a.verb!=="forget"&&a.phase!=="done")return j(new Error(`Forget operation ${d} on ${t.stackName} collides with an in-flight '${a.verb}' operation covering the same resources \u2014 that surgery resumes only through its deploy. ${de(a.appName)} Nothing was mutated.`));if(a!==null&&a.phase!=="done")return $({opId:d,alreadyConsented:!0,source:"journal",journal:a,foreignMarkers:[]});const o=await x(n,t.stackName,t.abortSignal);if(!o.success)return o;const c=M(o.data.templateBody,r,t.stackName);if(!c.success)return c;const e=c.data.markerOpIds.filter(f=>f!==d).map(f=>{const l=V(o.data.templateBody,f,t.stackName),u=l.success?l.data:[];return{opId:f,markedLogicalIds:u,markedResourceTypes:Fe(o.data.templateBody,u,t.stackName)}});return c.data.markerOpIds.includes(d)?$({opId:d,alreadyConsented:!0,source:"marker",foreignMarkers:e}):$({opId:d,alreadyConsented:!1,source:"none",foreignMarkers:e})}function le(n,t,s,r,d){const a=r.filter(e=>e!==s);if(a.length===0)return $(void 0);const o=a.map(e=>{const f=V(n,e,t);return`${e} covers ${f.success&&f.data.length>0?f.data.join(", "):"(could not recover its target set)"}`}),c=d?`resume this operation with 'fjall drift resume ${s}'`:"re-run 'fjall drift repair' to restart this operation";return j(new Error(`Forget operation ${s} on ${t} found target resources already carrying a ${ce} marker from a different operation \u2014 its journal may live on another machine, and completing this operation over it would strand that surgery. Marked resources recovered from the live template: ${o.join("; ")}. Complete the foreign operation first \u2014 on the machine that holds its journal, run 'fjall drift resume ${a.join("' / 'fjall drift resume ")}' (or 'fjall drift repair' there if it is journal-less); once it converges, ${c}. Nothing was mutated.`))}async function Q(n,t,s,r,d){const a=s.targets.map(i=>i.logicalId),o=M(t,a,s.stackName);if(!o.success)return o;if(A(s.phase)<A("flip-applied"))return $({absent:o.data.absentTargets,retained:[]});const c=V(t,s.opId,s.stackName,s.verb==="recreate"?Be:ce);if(!c.success)return c;const e=new Set(c.data),f=new Set(o.data.absentTargets),l=s.targets.filter(i=>!f.has(i.logicalId)&&!e.has(i.logicalId));if(l.length===0)return $({absent:o.data.absentTargets,retained:[]});const u=await pe(n,s,l,r,d);return $({absent:o.data.absentTargets,retained:l.map(i=>i.logicalId).filter(i=>u.has(i))})}async function pe(n,t,s,r,d){const a=s.map(e=>({stackName:t.stackName,logicalId:e.logicalId,resourceType:e.resourceType,...H(e,t.stackName)?{}:{physicalId:e.physicalId},failedStatus:t.verb==="recreate"?"RECREATE_CLOSURE_CHECK":"FORGET_CLOSURE_CHECK",statusReason:`Presence verification at ${t.verb} closure`})),c=await new te(n,d?.probe).probeSuspects({stackName:t.stackName,suspects:a,...r!==void 0?{abortSignal:r}:{}});return c.success?new Set(c.data.filter(e=>e.verdict==="present"&&e.probeMethod!==re).map(e=>e.logicalId)):(J.debug(U,"Closure probe failed \u2014 failing closed (not retained)",{stackName:t.stackName,opId:t.opId,error:c.error.message}),new Set)}async function et(n,t,s){const{stackName:r,targets:d,onProgress:a,abortSignal:o}=t;if(d.length===0)return j(new Error("Forget surgery requires at least one target"));const c=s?.now??(()=>new Date),e=n.getAccountId(),f=n.getRegion(),l=d.map(h=>h.logicalId),u=ne(r,l);let i=await oe(e,f,r,u,s?.journal);if(i!==null&&i.verb!=="forget"&&i.phase!=="done")return j(new Error(`Forget surgery ${u} on ${r} collides with an in-flight '${i.verb}' operation covering the same resources \u2014 that surgery resumes only through its deploy. ${de(i.appName)} Nothing was mutated.`));if(i!==null&&i.phase==="done"){if(t.doneJournalPolicy!=="archive-and-restart")return a?.(Te(i)),$({opId:u,phase:"done",convergePending:!1,...i.forensicsPath!==void 0?{forensicsPath:i.forensicsPath}:{},journal:i});const h=await Re(i,s?.journal);if(!h.success)return h;a?.(Ne(i)),i=null}const k=async h=>{const N=await ie(h,s?.journal);return N.success?$(h):j(N.error)};let b;if(i===null){const h=await x(n,r,o);if(!h.success)return h;const N=M(h.data.templateBody,l,r);if(!N.success)return N;const D=le(h.data.templateBody,r,u,N.data.markerOpIds,!1);if(!D.success)return D;if(N.data.markerOpIds.includes(u)){a?.(`Recognised in-flight forget markers on ${r} \u2014 resuming from the fjall:forget marker`);const m=c().toISOString(),T=await k({version:1,opId:u,verb:"forget",accountId:e,region:f,stackName:r,...t.appName!==void 0?{appName:t.appName}:{},targets:[...d],phase:N.data.allFlipped?"flip-applied":"captured",createdAt:m,updatedAt:m});if(!T.success)return T;i=T.data,b=h.data}else{a?.(`Capturing forensics for ${r} (${u})`);const m=await $e(n,{stackName:r,opId:u,findings:t.findings??[],...o!==void 0?{abortSignal:o}:{}},s?.forensics);if(!m.success)return m;const T=c().toISOString(),I=await k({version:1,opId:u,verb:"forget",accountId:e,region:f,stackName:r,...t.appName!==void 0?{appName:t.appName}:{},targets:[...d],phase:"captured",createdAt:T,updatedAt:T,forensicsPath:m.data.forensicsPath,...t.consentedPhysicalNames!==void 0?{consentedPhysicalNames:[...t.consentedPhysicalNames]}:{}});if(!I.success)return I;i=I.data,b={templateBody:m.data.liveTemplateBody,capabilities:m.data.capabilities,parameterKeys:m.data.parameterKeys}}}if(b===void 0){const h=await x(n,r,o);if(!h.success)return h;b=h.data}let g=b.templateBody,p=M(g,l,r);if(!p.success)return p;const R=le(g,r,u,p.data.markerOpIds,!0);if(!R.success)return R;const O=i.phase,B=await Q(n,g,i,o,s);if(!B.success)return B;const w=new Set(B.data.retained),E=p.data.policyPendingTargets.filter(h=>!w.has(h)),F=[];let L=!1;if(E.length>0){const h=Se(g,E,r);if(!h.success)return h;if(h.data.length>0){const y=E.length>1;return j(new Error(`Forget surgery cannot proceed on ${r}: ${E.join(", ")} must have DeletionPolicy changed to Retain before removal, but other resources in the stack still reference the deleted resource${y?"s":""} (${h.data.join(", ")}) \u2014 CloudFormation re-resolves references to a deleted resource during any update that modifies it, so the policy change cannot be applied. Either restore the deleted resource${y?"s":""} out-of-band and re-run 'fjall drift detect' (the finding clears; this operation record then idles until the 90-day retention sweep), or remove the target and every referencing resource from the stack in one manual template update \u2014 they cannot outlive it. This shape is a recorded fjall limitation (drift-remediation ADR, 2026-07-12). Nothing was mutated.`))}const N=l.filter(y=>!w.has(y)),D=ke(g,E,N,r);if(!D.success)return D;let m=D.data;if(m.length>0){const y=new Set(m.flatMap(v=>v.deadReferences)),S=await pe(n,i,i.targets.filter(v=>y.has(v.logicalId)),o,s);S.size>0&&(m=m.map(v=>({...v,deadReferences:v.deadReferences.filter(P=>!S.has(P))})).filter(v=>v.deadReferences.length>0))}if(m.length>0){const y=[];for(const S of m){const v=i.targets.find(C=>C.logicalId===S.pendingTarget);if(v===void 0||H(v,r)){y.push(`${S.pendingTarget} (no probeable physical id on this operation)`);continue}const P=await ae(n,{resourceType:v.resourceType,physicalId:v.physicalId,...o!==void 0?{abortSignal:o}:{}});P.verdict==="confirmed-deleted"?F.push(S.pendingTarget):y.push(`${S.pendingTarget} (${P.verdict}: ${P.detail})`)}if(y.length>0){const S=m.map(C=>`${C.pendingTarget} itself references the deleted ${C.deadReferences.join(", ")}`).join("; "),v=new Set(m.flatMap(C=>C.deadReferences)).size>1,P=m.length>1;return j(new Error(`Forget surgery cannot proceed on ${r}: ${m.map(C=>C.pendingTarget).join(", ")} must have DeletionPolicy changed to Retain before removal, but ${S} \u2014 CloudFormation resolves a modified resource's own references at execution time, and a reference to a deleted resource cannot resolve, so the policy change cannot be applied. fjall proceeds without the policy change only when the referencing target is verified deleted against its owning service (the converge's removal is then a physical no-op), and verification did not confirm deletion for ${y.join("; ")}. Either restore the referenced resource${v?"s":""} out-of-band and resume this operation (the policy change can then apply), or remove the referencing target${P?"s":""} and every reference to ${P?"them":"it"} (including stack Outputs) from the stack in one manual template update \u2014 CloudFormation deletes template-removed resources from its stored state, so removing an already-deleted resource succeeds where modifying it cannot. The unverified-absence shape is a recorded fjall limitation (drift-remediation ADR, 2026-07-12). Nothing was mutated.`))}a?.(`${F.join(", ")} cannot carry the Retain flip (${m.map(S=>`${S.pendingTarget} references the deleted ${S.deadReferences.join(", ")}`).join("; ")}) but ${F.length===1?"is":"are"} verified deleted against the owning service \u2014 proceeding without the flip: the converge's removal of an already-deleted resource is a physical no-op (flipless by necessity)`)}const T=new Set(F),I=E.filter(y=>!T.has(y));if(I.length>0){L=!0,i.liveTemplateDigest!==void 0&&i.liveTemplateDigest!==se(g)&&A(i.phase)>=A("flip-verified")&&a?.(`Live template of ${r} changed since the flip was verified \u2014 re-verifying from the current template`),a?.(`Deriving the retain flip for ${r}`);const y=Oe({liveTemplateBody:g,stackName:r,targets:I,opId:u,at:c().toISOString()});if(!y.success)return y;const S=Ae(g,y.data.flipTemplateBody,I,r);if(!S.success)return S;a?.("Proving the flip is metadata-only (review change set)");const v=await Ee(n,{stackName:r,flipTemplateBody:y.data.flipTemplateBody,targets:I,parameterKeys:b.parameterKeys,...o!==void 0?{abortSignal:o}:{}},s?.flip);if(!v.success)return v;const P=await k({...i,phase:"flip-verified",updatedAt:c().toISOString(),liveTemplateDigest:y.data.liveTemplateDigest,proof:{diffGuardPassed:!0,changeSetMetadataOnly:!0,noChanges:v.data.noChanges,provenAt:c().toISOString()}});if(!P.success)return P;i=P.data,a?.(`Applying the retain flip to ${r}`);const C=await je(n,{stackName:r,flipTemplateBody:y.data.flipTemplateBody,capabilities:b.capabilities,parameterKeys:b.parameterKeys,derivedFromTemplateDigest:y.data.liveTemplateDigest,opId:u,...o!==void 0?{abortSignal:o}:{}},s?.flip);if(!C.success)return C;const Y=await x(n,r,o);if(!Y.success)return Y;if(g=Y.data.templateBody,p=M(g,l,r),!p.success)return p;const ue=new Set(I),W=p.data.unflippedTargets.filter(X=>ue.has(X)&&!w.has(X));if(W.length>0)return j(new Error(`Retain flip on ${r} reported ${C.data.outcome} but GetTemplate does not show Retain + the fjall:forget marker on ${W.join(", ")} \u2014 aborting to manual repair.`))}}if(A(i.phase)<A("flip-applied")){const h=new Set(F),N=p.data.policyPendingTargets.filter(I=>!w.has(I)&&!h.has(I));if(N.length>0)return j(new Error(`Retain flip on ${r} could not be recorded: ${N.join(", ")} do not carry the Retain deletion policies in the live template \u2014 re-run 'fjall drift repair' to re-verify.`));const D=new Set(p.data.absentTargets),m=p.data.unflippedTargets.filter(I=>!D.has(I)&&!w.has(I)&&!h.has(I));m.length>0&&a?.(`${m.join(", ")} already ${m.length===1?"carries":"carry"} DeletionPolicy Retain in the live template \u2014 no stack update needed (flipless fast-path)`);const T=await k({...i,phase:"flip-applied",updatedAt:c().toISOString(),liveTemplateDigest:se(g),...m.length>0?{fliplessTargets:m}:{},...F.length>0?{fliplessByNecessity:F}:{}});if(!T.success)return T;i=T.data}const K=!L&&i.phase===O?B:await Q(n,g,i,o,s);if(!K.success)return K;const G=K.data.retained;if(K.data.absent.length+G.length===l.length){const h=await fe(i,G,c,s?.journal,s?.driftJournal);return h.success?(i=h.data,a?.(G.length>0?`Forget operation ${u} complete \u2014 the converge kept ${G.join(", ")} (re-declared in the synthesised template)${K.data.absent.length>0?` and ${r} no longer tracks the remaining targets`:""}`:`Targets are no longer in the ${r} template \u2014 forget operation ${u} complete`),$({opId:u,phase:"done",convergePending:!1,...i.forensicsPath!==void 0?{forensicsPath:i.forensicsPath}:{},journal:i})):h}return a?.(`Retain flip is live on ${r} \u2014 run the converge deploy to complete the forget`),$({opId:u,phase:i.phase,convergePending:!0,...i.forensicsPath!==void 0?{forensicsPath:i.forensicsPath}:{},journal:i})}async function De(n,t,s){const{journal:r}=t;if(r.phase==="done")return $({status:"done",journal:r});const d=s?.now??(()=>new Date),a=r.targets.map(l=>l.logicalId),o=await x(n,r.stackName,t.abortSignal);if(!o.success)return o;const c=await Q(n,o.data.templateBody,r,t.abortSignal,s);if(!c.success)return c;if(c.data.absent.length+c.data.retained.length!==a.length){const l=new Set([...c.data.absent,...c.data.retained]);return $({status:"converge-pending",journal:r,absent:[...c.data.absent],retained:[...c.data.retained],unverified:a.filter(u=>!l.has(u))})}const e=r.verb==="recreate"&&c.data.absent.length>0?await Me(n,r,c.data.absent,t.abortSignal):void 0,f=await fe(r,c.data.retained,d,s?.journal,s?.driftJournal,e);return f.success?$({status:"done",journal:f.data}):f}async function Me(n,t,s,r){const d={orphaned:[],unverified:[]};for(const a of s){const o=t.targets.find(e=>e.logicalId===a);if(o===void 0)continue;if(H(o,t.stackName)){d.unverified.push(a);continue}const c=await ae(n,{resourceType:o.resourceType,physicalId:o.physicalId,...r!==void 0?{abortSignal:r}:{}});c.verdict==="present"?d.orphaned.push(a):c.verdict==="unconfirmed"&&d.unverified.push(a)}return d}async function fe(n,t,s,r,d,a){const o=s().toISOString(),c={...n,phase:"done",updatedAt:o,completedAt:o,...t.length>0?n.verb==="recreate"?{recreatedResources:[...t]}:{retainedByRedeclaration:[...t]}:{},...a!==void 0&&a.orphaned.length>0?{orphanedResources:[...a.orphaned]}:{},...a!==void 0&&a.unverified.length>0?{unverifiedRemovals:[...a.unverified]}:{}},e=await ie(c,r);return e.success?(await xe(n,d).catch(f=>{J.warn(U,"Failed to clear drift suspects after forget",{stackName:n.stackName,opId:n.opId,error:_(q(f))})}),$(c)):j(e.error)}async function xe(n,t){const s=await we(n.accountId,n.region,n.stackName,t);if(s===null)return;const r=new Set(n.targets.map(a=>a.logicalId)),d=s.suspects.filter(a=>!r.has(a.logicalId));if(d.length!==s.suspects.length){if(d.length===0){await be(n.accountId,n.region,n.stackName,t);return}await Ie({...s,suspects:d},t)}}async function tt(n,t,s){try{const r=await ge(he(n,`${t}.template.json`),"utf8"),d=JSON.parse(r);return typeof d.Resources=="object"&&d.Resources!==null&&Object.hasOwn(d.Resources,s)}catch(r){J.debug(U,"Could not read the synthesised template",{stackName:t,error:_(q(r))});return}}async function rt(n,t,s){await Ce(s?.journal,s?.now);const r=n.aws.getAccountId(),d=n.aws.getRegion(),a={blocked:[],inProgressOpIds:[],converging:[],flipNotLive:[],flipUnverified:[],stillDeclared:[],recreateConverging:[]},o=new Map,c=new Map;for(const e of t){if(ee(n.abortSignal)){a.blocked.push(e);continue}let f=o.get(e.stackName);f===void 0&&(f=await z(r,d,e.stackName,s?.journal),o.set(e.stackName,f));const l=f.find(p=>p.verb==="forget"&&p.targets.some(R=>R.logicalId===e.logicalId));if(l===void 0){const p=f.find(R=>R.verb==="recreate"&&R.targets.some(O=>O.logicalId===e.logicalId));if(p!==void 0){a.recreateConverging.push({finding:e,opId:p.opId,...p.appName!==void 0?{appName:p.appName}:{}});continue}a.blocked.push(e);continue}if(A(l.phase)<A("flip-applied")){a.blocked.push(e),a.inProgressOpIds.includes(l.opId)||a.inProgressOpIds.push(l.opId);continue}let u=c.get(e.stackName);if(u===void 0){const p=await x(n.aws,e.stackName,n.abortSignal);p.success?u={templateBody:p.data.templateBody}:(J.debug(U,"Could not read the live template for the [G2] partition",{stackName:e.stackName,error:p.error.message}),u={errorMessage:p.error.message}),c.set(e.stackName,u)}if("errorMessage"in u){a.blocked.push(e),a.flipUnverified.push({finding:e,opId:l.opId,message:u.errorMessage});continue}const i=M(u.templateBody,[e.logicalId],e.stackName),k=new Set(l.fliplessByNecessity??[]);if(!(i.success&&(i.data.absentTargets.length===1||(i.data.policyPendingTargets.length===0||k.has(e.logicalId))&&i.data.markerOpIds.every(p=>p===l.opId)))){a.blocked.push(e),a.flipNotLive.push({finding:e,opId:l.opId});continue}if(await n.synthDeclaresTarget(e.stackName,e.logicalId)===!0){a.blocked.push(e),a.stillDeclared.push({finding:e,opId:l.opId});continue}a.converging.push({finding:e,opId:l.opId})}return a}async function at(n,t,s,r){const d=n.aws.getAccountId(),a=n.aws.getRegion(),o={verifiedAbsent:[],blocked:[]};for(const c of t){const e=await z(d,a,c,r?.journal);for(const f of e){if(f.verb!=="forget"||A(f.phase)<A("flip-applied"))continue;const l=[];for(const g of f.fliplessByNecessity??[]){const p={stackName:c,logicalId:g,opId:f.opId};if(s.get(c)?.has(g)===!0){o.verifiedAbsent.push(p);continue}if(ee(n.abortSignal)){o.blocked.push({...p,reason:"absence re-verification aborted before it could run"});continue}if(await n.synthDeclaresTarget(c,g)===!0)continue;const O=f.targets.find(B=>B.logicalId===g);if(O===void 0||H(O,c)){o.blocked.push({...p,reason:"no probeable physical id on the covering operation"});continue}l.push({entry:p,target:O})}if(l.length===0)continue;const u=l.map(({entry:g,target:p})=>({stackName:c,logicalId:g.logicalId,resourceType:p.resourceType,physicalId:p.physicalId,failedStatus:"BY_NECESSITY_CONVERGE_CHECK",statusReason:"Absence re-verification before the converge deploy"})),k=await new te(n.aws,r?.probe).probeSuspects({stackName:c,suspects:u,...n.abortSignal!==void 0?{abortSignal:n.abortSignal}:{}});if(!k.success){for(const{entry:g}of l)o.blocked.push({...g,reason:`absence probe failed: ${k.error.message}`});continue}const b=new Map(k.data.map(g=>[g.logicalId,g]));for(const{entry:g}of l){const p=b.get(g.logicalId);if(p===void 0){o.blocked.push({...g,reason:"absence probe returned no verdict for this target"});continue}if(p.verdict==="confirmed-deleted"||p.verdict==="present"&&p.probeMethod===re){o.verifiedAbsent.push(g);continue}o.blocked.push({...g,reason:`${p.verdict}: ${p.verdictDetail}`})}}}return o}async function nt(n,t,s){const r=n.getAccountId(),d=n.getRegion(),a=[...t.stackNames];if(t.appName!==void 0){const o=new Set(a),c=await Pe(r,d,s?.journal);for(const e of c)e.verb==="recreate"&&e.appName===t.appName&&!o.has(e.stackName)&&(o.add(e.stackName),a.push(e.stackName))}for(const o of a){const c=await z(r,d,o,s?.journal);for(const e of c){const f=await De(n,{journal:e,...t.abortSignal!==void 0?{abortSignal:t.abortSignal}:{}},s);if(!f.success){const l=e.verb==="recreate"?`re-run 'fjall deploy${e.appName!==void 0?` ${e.appName}`:""}' to complete it`:`run 'fjall drift resume ${e.opId}' to complete it`;t.onLog?.(`Could not verify ${e.verb} operation ${e.opId} on ${o} after the deploy: ${_(f.error.message)} \u2014 ${l}`,"warn");continue}if(f.data.status==="done"&&e.verb==="recreate"){const l=f.data.journal,u=l.recreatedResources??[],i=new Set(u),k=new Set(l.orphanedResources??[]),b=new Set(l.unverifiedRemovals??[]),g=l.targets.map(w=>w.logicalId).filter(w=>!i.has(w)),p=g.filter(w=>!k.has(w)&&!b.has(w)),R=[];u.length>0&&R.push(`${u.join(", ")} verified live under the pinned names`),p.length>0&&R.push(`${p.join(", ")} removed instead of re-created (no longer declared)`),t.onLog?.(`Recreate operation ${e.opId} completed${R.length>0?` \u2014 ${R.join("; ")}`:""}`,"info");const O=g.filter(w=>k.has(w));if(O.length>0){const w=O.map(E=>{const F=l.targets.find(L=>L.logicalId===E)?.physicalId;return F!==void 0?`${E} [${F}]`:E}).join(", ");t.onLog?.(`Recreate operation ${e.opId}: ${w} left CloudFormation but the physical resource still exists outside CloudFormation \u2014 the consented deletion did not execute. Delete it via the owning service if it is no longer wanted.`,"warn")}const B=g.filter(w=>b.has(w));B.length>0&&t.onLog?.(`Recreate operation ${e.opId}: ${B.join(", ")} left CloudFormation but the physical deletion could not be verified \u2014 check the owning service before treating it as deleted.`,"warn")}else if(f.data.status==="done"){const l=f.data.journal.retainedByRedeclaration??[],u=new Set(l),i=f.data.journal.targets.map(b=>b.logicalId).filter(b=>!u.has(b)),k=i.length>0?`${o} no longer tracks ${i.join(", ")}${l.length>0?` (${l.join(", ")} retained by re-declaration)`:""}`:`${o} retained ${l.join(", ")} by re-declaration`;t.onLog?.(`Forget operation ${e.opId} completed \u2014 ${k}`,"info")}}}}export{Ze as assessForgetResumability,De as completeForgetAfterConverge,nt as completeForgetAfterDeploy,x as fetchLiveStackState,de as formatRecreateInFlightCures,rt as partitionPreFlightByRemediation,et as runForgetSurgery,tt as synthTemplateDeclaresResource,at as verifyByNecessityBeforeConverge};
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The `pin` arm — the non-destructive escape hatch (plan § 3): probe the LIVE
3
+ * value of each offending property from its owning service and emit an
4
+ * edit-ready codemod into the user's fjall config so the diff disappears.
5
+ * Degrades to report-only carrying the exact config line whenever any step
6
+ * cannot complete confidently — never a partial edit (the
7
+ * `restoreAndReconcileQuarantinedBucket` reconcile doctrine). The codemod is
8
+ * OFFERED to the caller (Q6): this module only computes `modifiedContent`;
9
+ * the surface that owns the config file decides to write it.
10
+ *
11
+ * v1 probe table: RDS DBInstance/DBCluster `Port`. Every other
12
+ * (resourceType, property) pair degrades honestly to `no-probe-support`
13
+ * naming the pair.
14
+ *
15
+ * Failure messages are pre-masked at construction.
16
+ */
17
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
18
+ export interface PinTarget {
19
+ stack: string;
20
+ logicalId: string;
21
+ resourceType: string;
22
+ /** Ticket `physicalName` — a pinned literal, or the shared placeholder
23
+ * shape (`remediationPlaceholderPhysicalId`) for unpinned rows. */
24
+ physicalName: string;
25
+ /** Offending property paths from the ticket row (bare names or
26
+ * `/properties/`-prefixed pointers). */
27
+ offendingProperties: readonly string[];
28
+ }
29
+ /** One probed live value plus the exact config line that pins it. */
30
+ export interface PinnedProperty {
31
+ /** CloudFormation property name (e.g. "Port"). */
32
+ property: string;
33
+ /** fjall config property the codemod writes (e.g. "port"). */
34
+ configProperty: string;
35
+ liveValue: string | number;
36
+ /** The exact config line for manual application (e.g. `port: 5433`). */
37
+ configLine: string;
38
+ }
39
+ export type PinReportOnlyReason = "no-probe-support" | "physical-id-unresolved" | "probe-failed" | "no-infrastructure-content" | "unresolved-construct" | "edit-failed"
40
+ /** The codemod was a byte-identical no-op — the config already carries the
41
+ * probed value(s), so claiming an edit would loop the deploy forever. */
42
+ | "already-pinned";
43
+ interface PinTargetIdentity {
44
+ stack: string;
45
+ logicalId: string;
46
+ resourceType: string;
47
+ physicalName: string;
48
+ }
49
+ export type PinResourceOutcome = (PinTargetIdentity & {
50
+ outcome: "edit-ready";
51
+ /** The construct symbol the codemod edited. */
52
+ constructName: string;
53
+ pinned: PinnedProperty[];
54
+ }) | (PinTargetIdentity & {
55
+ outcome: "report-only";
56
+ reason: PinReportOnlyReason;
57
+ /** Pre-masked human detail (unsupported pairs, probe error, edit error). */
58
+ detail?: string;
59
+ /** Probed values that WERE resolved — the exact config lines the
60
+ * operator can apply by hand even though no edit was emitted. */
61
+ pinned?: PinnedProperty[];
62
+ /** Candidate construct names on ambiguous resolution. */
63
+ candidates?: string[];
64
+ });
65
+ export interface PinRemediationReport {
66
+ outcomes: PinResourceOutcome[];
67
+ /** The config content with EVERY edit-ready outcome applied, present iff
68
+ * at least one target reached edit-ready. Later targets compose on top of
69
+ * earlier edits. */
70
+ modifiedContent?: string;
71
+ /** The exact source snapshot the edits were composed against — present iff
72
+ * `modifiedContent` is. The write-back owner MUST verify the file on disk
73
+ * still matches this snapshot before replacing it wholesale: the consent
74
+ * ceremony can sit for minutes, and a whole-file write composed from a
75
+ * stale read silently reverts a concurrent user edit (lost update). */
76
+ sourceContent?: string;
77
+ }
78
+ export interface PinProbeCoverage {
79
+ /** True iff EVERY offending property has a live-value probe. */
80
+ covered: boolean;
81
+ /** The `<resourceType>#<property>` pairs without a probe (all pairs when
82
+ * no offending properties were recorded — nothing is probeable). */
83
+ unsupportedPairs: string[];
84
+ }
85
+ /**
86
+ * Whether the v1 probe table can execute a pin for this resource. The
87
+ * destruction gate consults this BEFORE accepting a `pin` consent — declaring
88
+ * the verb executable and then degrading every target to report-only
89
+ * (no-probe-support) would stop the deploy for a write-back that can never
90
+ * happen.
91
+ */
92
+ export declare function pinProbeCoverage(resourceType: string, offendingProperties: readonly string[]): PinProbeCoverage;
93
+ /**
94
+ * One-line detail for a pin outcome (everything after the physical name).
95
+ * Shared by `renderPinReportLines` and the CLI's labelled row rendering so
96
+ * both surfaces stay byte-identical. Identifier-only plus pre-masked details
97
+ * — safe for any sink.
98
+ */
99
+ export declare function formatPinOutcomeDetail(outcome: PinResourceOutcome): string;
100
+ /**
101
+ * Render the per-resource pin outcome lines every surface shares (engine
102
+ * callbacks, CLI human output, agent output).
103
+ */
104
+ export declare function renderPinReportLines(report: PinRemediationReport): string[];
105
+ export interface RunPinRemediationParams {
106
+ targets: readonly PinTarget[];
107
+ /** The user's infrastructure config source. Absent (worker surfaces with
108
+ * no config checkout) degrades every target to report-only with the exact
109
+ * config lines. */
110
+ infrastructureContent?: string;
111
+ /** Display path for codemod diagnostics. */
112
+ infrastructureFilePath?: string;
113
+ abortSignal?: AbortSignal;
114
+ }
115
+ /**
116
+ * Run the pin arm over consented targets. Never throws; every failure mode
117
+ * is an in-band report-only outcome (pre-masked at construction). Outcomes
118
+ * may be fewer than targets when the abort signal fires mid-loop.
119
+ */
120
+ export declare function runPinRemediation(aws: AwsProvider, params: RunPinRemediationParams): Promise<PinRemediationReport>;
121
+ export {};
@@ -0,0 +1 @@
1
+ import{CloudFormationClient as w,DescribeStackResourceCommand as B}from"@aws-sdk/client-cloudformation";import{DescribeDBClustersCommand as O,DescribeDBInstancesCommand as T,RDSClient as R}from"@aws-sdk/client-rds";import{success as h,failure as f}from"@fjall/generator";import{listResources as L,modifyResource as A}from"@fjall/generator/codemod";import{getErrorMessage as P,maskSensitiveOutput as D}from"@fjall/util";import{logger as j}from"@fjall/util/logger";import{composeSdkAbortSignal as C,isAborted as k}from"../../aws/organisations/types.js";import{remediationPlaceholderPhysicalId as x}from"../../types/remediation.js";const F="PinRemediation",N="/properties/";async function V(e,n,o){try{const i=(await e.getClient(R).send(new T({DBInstanceIdentifier:n}),{abortSignal:C(o)})).DBInstances?.[0]?.Endpoint?.Port;return i===void 0?f(new Error(`rds:DescribeDBInstances returned no endpoint port for '${n}'`)):h(i)}catch(t){return f(new Error(D(P(t))))}}async function W(e,n,o){try{const i=(await e.getClient(R).send(new O({DBClusterIdentifier:n}),{abortSignal:C(o)})).DBClusters?.[0]?.Port;return i===void 0?f(new Error(`rds:DescribeDBClusters returned no port for '${n}'`)):h(i)}catch(t){return f(new Error(D(P(t))))}}const v={"AWS::RDS::DBInstance#Port":{configProperty:"port",constructType:"database",method:"rds:DescribeDBInstances",probe:V},"AWS::RDS::DBCluster#Port":{configProperty:"port",constructType:"database",method:"rds:DescribeDBClusters",probe:W}};function E(e){return e.startsWith(N)?e.slice(N.length):e}function Z(e,n){if(n.length===0)return{covered:!1,unsupportedPairs:[`${e} (no offending properties recorded)`]};const o=n.map(E).filter(t=>v[`${e}#${t}`]===void 0).map(t=>`${e}#${t}`);return{covered:o.length===0,unsupportedPairs:o}}function _(e){return typeof e=="number"?String(e):JSON.stringify(e)}async function G(e,n,o){const t=x(n.stack,n.logicalId);if(n.physicalName!==t)return h(n.physicalName);try{const s=(await e.getClient(w).send(new B({StackName:n.stack,LogicalResourceId:n.logicalId}),{abortSignal:C(o)})).StackResourceDetail?.PhysicalResourceId;return s===void 0||s===""?f(new Error(`cfn:DescribeStackResource resolved no physical id for ${n.stack}/${n.logicalId}`)):h(s)}catch(r){return f(new Error(D(P(r))))}}function H(e,n,o){const t=L(e,n);if(!t.success)return{detail:`config parse failed: ${t.error.kind}`};const r=t.data.resources.filter(i=>i.type===o);return r.length===1&&r[0]!==void 0?{constructName:r[0].name}:r.length===0?{detail:`no ${o} construct found in the config`}:{candidates:r.map(i=>i.name),detail:`${r.length} ${o} constructs found \u2014 cannot pin unambiguously`}}function J(e){if(e.outcome==="edit-ready")return`pinned ${e.pinned.map(r=>r.configLine).join(", ")} on construct '${e.constructName}' \u2014 edit ready`;const n=e.detail!==void 0?` \u2014 ${e.detail}`:"",o=e.candidates!==void 0&&e.candidates.length>0?`; candidate constructs: ${e.candidates.join(", ")}`:"",t=e.reason!=="already-pinned"&&e.pinned!==void 0&&e.pinned.length>0?`; apply by hand: ${e.pinned.map(r=>r.configLine).join(", ")}`:"";return`report-only (${e.reason})${n}${o}${t}`}function ee(e){return e.outcomes.map(n=>`${n.physicalName}: ${J(n)}`)}async function ne(e,n){const o=n.infrastructureFilePath??"infrastructure.ts",t=[];let r=n.infrastructureContent,i=!1;for(const s of n.targets){if(k(n.abortSignal)){j.debug(F,"Aborted mid-loop \u2014 remaining targets not probed",{remaining:n.targets.length-t.length});break}const d={stack:s.stack,logicalId:s.logicalId,resourceType:s.resourceType,physicalName:s.physicalName},g=s.offendingProperties.map(E).map(c=>({property:c,spec:v[`${s.resourceType}#${c}`]})),b=g.filter(c=>c.spec!==void 0);if(g.length===0||b.length<g.length){const c=g.filter(u=>u.spec===void 0),p=g.length===0?`${s.resourceType} (no offending properties recorded)`:c.map(u=>`${s.resourceType}#${u.property}`).join(", ");t.push({...d,outcome:"report-only",reason:"no-probe-support",detail:`no live-value probe for ${p} in this release`});continue}const $=await G(e,s,n.abortSignal);if(!$.success){t.push({...d,outcome:"report-only",reason:"physical-id-unresolved",detail:$.error.message});continue}const a=[];let m;for(const{property:c,spec:p}of b){if(k(n.abortSignal)){m="aborted before probing completed";break}const u=await p.probe(e,$.data,n.abortSignal);if(!u.success){m=`${p.method}: ${u.error.message}`;break}a.push({property:c,configProperty:p.configProperty,liveValue:u.data,configLine:`${p.configProperty}: ${_(u.data)}`})}if(m!==void 0){t.push({...d,outcome:"report-only",reason:"probe-failed",detail:m,...a.length>0?{pinned:a}:{}});continue}if(r===void 0){t.push({...d,outcome:"report-only",reason:"no-infrastructure-content",detail:"no infrastructure config available on this surface \u2014 apply the config line(s) by hand",pinned:a});continue}const S=b[0]?.spec.constructType??"database",l=H(r,o,S);if(l.constructName===void 0){t.push({...d,outcome:"report-only",reason:"unresolved-construct",...l.detail!==void 0?{detail:l.detail}:{},...l.candidates!==void 0?{candidates:l.candidates}:{},pinned:a});continue}const I={};for(const c of a)I[c.configProperty]=c.liveValue;const y=A(r,{type:S,name:l.constructName,properties:I,filePath:o});if(!y.success){t.push({...d,outcome:"report-only",reason:"edit-failed",detail:`codemod failed: ${y.error.kind}`,pinned:a});continue}if(y.data.content===r){t.push({...d,outcome:"report-only",reason:"already-pinned",detail:"the config already contains the probed value(s) \u2014 no edit needed; if the deploy still plans a replacement, the drift lies outside these properties",pinned:a});continue}r=y.data.content,i=!0,t.push({...d,outcome:"edit-ready",constructName:l.constructName,pinned:a})}return{outcomes:t,...i&&r!==void 0&&n.infrastructureContent!==void 0?{modifiedContent:r,sourceContent:n.infrastructureContent}:{}}}export{J as formatPinOutcomeDetail,Z as pinProbeCoverage,ee as renderPinReportLines,ne as runPinRemediation};