@go-to-k/cdkd 0.284.49 → 0.284.51

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.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $n as setAwsClients, Bt as DiffCalculator, Cr as normalizeAwsError, Gt as TemplateParser, Jn as clearBucketRegionCache, Jt as S3StateBackend, Kn as AssemblyReader, Kt as LockManager, Qn as resetAwsClients, Sr as isCdkdError, W as IAMRoleProvider, Wt as DagBuilder, Xn as AwsClients, Yn as resolveBucketRegion, Zn as getAwsClients, Zt as AssetPublisher, br as SynthesisError, er as AssetError, et as CloudControlProvider, ir as DependencyError, mr as ProvisioningError, nr as ConfigError, q as ProviderRegistry, r as DeployEngine, tr as CdkdError, ur as LockError, wn as Synthesizer, xr as formatError, yr as StateError } from "./deploy-engine-D-NZ_16N.js";
1
+ import { $n as setAwsClients, Bt as DiffCalculator, Cr as normalizeAwsError, Gt as TemplateParser, Jn as clearBucketRegionCache, Jt as S3StateBackend, Kn as AssemblyReader, Kt as LockManager, Qn as resetAwsClients, Sr as isCdkdError, W as IAMRoleProvider, Wt as DagBuilder, Xn as AwsClients, Yn as resolveBucketRegion, Zn as getAwsClients, Zt as AssetPublisher, br as SynthesisError, er as AssetError, et as CloudControlProvider, ir as DependencyError, mr as ProvisioningError, nr as ConfigError, q as ProviderRegistry, r as DeployEngine, tr as CdkdError, ur as LockError, wn as Synthesizer, xr as formatError, yr as StateError } from "./deploy-engine-Dnp730rc.js";
2
2
  import { n as getLogger, r as setLogger, t as ConsoleLogger } from "./logger-zRrlbaQt.js";
3
3
 
4
4
  export { AssemblyReader, AssetError, AssetPublisher, AwsClients, CdkdError, CloudControlProvider, ConfigError, ConsoleLogger, DagBuilder, DependencyError, DeployEngine, DiffCalculator, IAMRoleProvider, LockError, LockManager, ProviderRegistry, ProvisioningError, S3StateBackend, StateError, SynthesisError, Synthesizer, TemplateParser, clearBucketRegionCache, formatError, getAwsClients, getLogger, isCdkdError, normalizeAwsError, resetAwsClients, resolveBucketRegion, setAwsClients, setLogger };
@@ -1,7 +1,7 @@
1
- import { $ as startInterruptWatch, $n as setAwsClients, $t as WorkGraph, A as renderStatefulReason, An as resolveCaptureObservedState, At as redactSecretsForState, B as exportAliasCollisionScrubWarning, Bn as findLargeInlineResources, Bt as DiffCalculator, C as isFinalSnapshotError, Cn as getDockerImageBySourceHash, Cr as normalizeAwsError, Ct as TEMPLATE_SOURCED_RULES, D as extractDeploymentEventError, Dn as getLegacyStateBucketName, Dr as isThrottlingError, Dt as isSingleDynamicReferenceToken, E as makeCanonicalizePropertiesFn, En as getDefaultStateBucketName, Er as isRetryableTransientError, Et as errorCauseChain, F as green, Fn as stateBucketExistenceConfirmed, Ft as s3BucketDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gn as derivePartitionAndUrlSuffix, Gt as TemplateParser, H as secretBearingStateKeyWarning, Hn as expectedOwnerParam, Ht as describeTypeWithThrottleRetry, I as red, In as warnDeprecatedNoPrefixCliFlag, It as s3BucketDualStackDomainName, J as findActionableSilentDrops, Jt as S3StateBackend, K as clearOnUpdateRemoval, Kn as AssemblyReader, Kt as LockManager, L as yellow, Ln as CFN_TEMPLATE_BODY_LIMIT, Lt as s3BucketRegionalDomainName, M as bold, Mn as resolveStateBucketWithDefault, Mt as classifyReplaySecretRegion, N as cyan, Nn as resolveStateBucketWithDefaultAndSource, Nt as producerRegionsFromState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as resolveApp, Or as markNonRetryable, Ot as maskSecretsInError, P as gray, Pn as resolveUseCdkBootstrapAssets, Pt as s3BucketArn, Q as isInterruptedWaitError, Qn as resetAwsClients, Qt as stringifyValue, R as collectDeclaredOutputNames, Rn as CFN_TEMPLATE_URL_LIMIT, Rt as s3BucketWebsiteUrl, S as createPreDeleteFinalSnapshot, Sn as AssetManifestLoader, St as STATE_SOURCED_READBACK_RULES, T as unsupportedFinalSnapshotError, Tn as synthesisStatusMessage, Tr as isMarkedNonRetryable, Tt as dynamicReferenceTokens, U as stateKeySecretExposure, Un as PARTITION_TABLE, Ut as withRetry, V as isExportAliasCollision, Vn as uploadCfnTemplate, Vt as INTRINSIC_KEYS, W as IAMRoleProvider, Wn as canonicalizeRegion, Wt as DagBuilder, X as beginCommandInterruptScope, Xn as AwsClients, Xt as shouldRetainResource, Y as findSilentDropProperties, Yn as resolveBucketRegion, Yt as rebuildClientForBucketRegion, Z as endCommandInterruptScope, Zn as getAwsClients, Zt as AssetPublisher, _ as computeImplicitDeleteEdges, _n as formatDockerLoginError, _r as StackHasActiveImportsError, _t as replayWarn, a as DeploymentEventsStore, an as stripControlChars, ar as DeployCancelledError, at as carriesDynamicReference, b as buildFinalSnapshotIdentifier, bn as runDockerForeground, bt as requireConfigString, c as replayFailedOperations, cn as ensureAssetStorage, cr as LocalMigrateError, ct as refStateLookupFromResource, d as updatePartialReason, dn as readBootstrapMarkerBody, dr as MissingCdkCliError, dt as resolveExplicitPhysicalId, en as buildAssetRedirectMap, et as CloudControlProvider, f as UNSPECIFIED_SKIP_REASON, fn as validateAssetBucketName, fr as NestedStackChildDirectDestroyError, ft as assertRegionMatch, g as IMPLICIT_DELETE_DEPENDENCIES, gn as dockerSpawnEnvWithSensitive, gr as ResourceUpdateNotSupportedError, gt as readConfigString, h as maskingRetryLogger, hn as buildDockerImage, hr as ResourceTimeoutError, ht as configStringRefusal, i as DeploymentEventsReader, in as escapeRegExp, it as IntrinsicFunctionResolver, j as formatResourceLine, jn as resolveSkipPrefix, jt as scrubResourceRecord, k as isStatefulRecreateTargetSync, kn as resolveAutoAssetStorage, kt as maskSecretsInText, l as replayRollback, ln as getBootstrapMarkerKey, lr as LocalStartServiceError, lt as WAFv2WebACLProvider, m as withResourceDeadline, mn as buildDenyExternalAccessPolicy, mr as ProvisioningError, mt as configBooleanRefusal, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as loadPublishableAssetManifest, nt as disableInstanceApiTermination, o as planFailedOps, on as AssetModeResolver, or as DynamicReferenceRegionAmbiguousError, ot as cfnRefValueFromPhysicalId, p as deleteSkipReason, pn as validateContainerRepoName, pr as PartialFailureError, pt as coerceCfnBoolean, q as ProviderRegistry, qn as processStackMessages, qt as displaySafe, r as DeployEngine, rn as rewriteTemplateAssetReferences, rr as CrossAccountSecretRefusalError, rt as isTerminationProtectionPropagationError, s as planRollback, sn as BOOTSTRAP_MARKER_PREFIX, sr as LocalInvokeBuildError$1, st as getAccountInfo, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as createAssetRedirectResolver, tr as CdkdError, tt as slowCcOperationTimeoutMs, u as updatePartialMessage, un as parseBootstrapMarker, ut as normalizeAwsTagsToCfn, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as getDockerCmd, vr as StackTerminationProtectionError, vt as requireConfigArray, w as refusesFinalSnapshot, wn as Synthesizer, wr as withErrorHandling, wt as createSecretMasker, x as ccRoutedFinalSnapshotError, xn as runDockerStreaming, xt as STATE_SOURCED_CROSS_GENERATION_RULES, y as PRE_DELETE_SNAPSHOT_TYPES, yn as partitionSensitiveEnv, yt as requireConfigObject, z as collectPublishedOutputNames, zn as MIGRATE_TMP_PREFIX, zt as applyRoleArnIfSet } from "./deploy-engine-D-NZ_16N.js";
1
+ import { $ as startInterruptWatch, $n as setAwsClients, $t as WorkGraph, A as renderStatefulReason, An as resolveCaptureObservedState, At as redactSecretsForState, B as exportAliasCollisionScrubWarning, Bn as findLargeInlineResources, Bt as DiffCalculator, C as isFinalSnapshotError, Cn as getDockerImageBySourceHash, Cr as normalizeAwsError, Ct as TEMPLATE_SOURCED_RULES, D as extractDeploymentEventError, Dn as getLegacyStateBucketName, Dr as isThrottlingError, Dt as isSingleDynamicReferenceToken, E as makeCanonicalizePropertiesFn, En as getDefaultStateBucketName, Er as isRetryableTransientError, Et as errorCauseChain, F as green, Fn as stateBucketExistenceConfirmed, Ft as s3BucketDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gn as derivePartitionAndUrlSuffix, Gt as TemplateParser, H as secretBearingStateKeyWarning, Hn as expectedOwnerParam, Ht as describeTypeWithThrottleRetry, I as red, In as warnDeprecatedNoPrefixCliFlag, It as s3BucketDualStackDomainName, J as findActionableSilentDrops, Jt as S3StateBackend, K as clearOnUpdateRemoval, Kn as AssemblyReader, Kt as LockManager, L as yellow, Ln as CFN_TEMPLATE_BODY_LIMIT, Lt as s3BucketRegionalDomainName, M as bold, Mn as resolveStateBucketWithDefault, Mt as classifyReplaySecretRegion, N as cyan, Nn as resolveStateBucketWithDefaultAndSource, Nt as producerRegionsFromState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as resolveApp, Or as markNonRetryable, Ot as maskSecretsInError, P as gray, Pn as resolveUseCdkBootstrapAssets, Pt as s3BucketArn, Q as isInterruptedWaitError, Qn as resetAwsClients, Qt as stringifyValue, R as collectDeclaredOutputNames, Rn as CFN_TEMPLATE_URL_LIMIT, Rt as s3BucketWebsiteUrl, S as createPreDeleteFinalSnapshot, Sn as AssetManifestLoader, St as STATE_SOURCED_READBACK_RULES, T as unsupportedFinalSnapshotError, Tn as synthesisStatusMessage, Tr as isMarkedNonRetryable, Tt as dynamicReferenceTokens, U as stateKeySecretExposure, Un as PARTITION_TABLE, Ut as withRetry, V as isExportAliasCollision, Vn as uploadCfnTemplate, Vt as INTRINSIC_KEYS, W as IAMRoleProvider, Wn as canonicalizeRegion, Wt as DagBuilder, X as beginCommandInterruptScope, Xn as AwsClients, Xt as shouldRetainResource, Y as findSilentDropProperties, Yn as resolveBucketRegion, Yt as rebuildClientForBucketRegion, Z as endCommandInterruptScope, Zn as getAwsClients, Zt as AssetPublisher, _ as computeImplicitDeleteEdges, _n as formatDockerLoginError, _r as StackHasActiveImportsError, _t as replayWarn, a as DeploymentEventsStore, an as stripControlChars, ar as DeployCancelledError, at as carriesDynamicReference, b as buildFinalSnapshotIdentifier, bn as runDockerForeground, bt as requireConfigString, c as replayFailedOperations, cn as ensureAssetStorage, cr as LocalMigrateError, ct as refStateLookupFromResource, d as updatePartialReason, dn as readBootstrapMarkerBody, dr as MissingCdkCliError, dt as resolveExplicitPhysicalId, en as buildAssetRedirectMap, et as CloudControlProvider, f as UNSPECIFIED_SKIP_REASON, fn as validateAssetBucketName, fr as NestedStackChildDirectDestroyError, ft as assertRegionMatch, g as IMPLICIT_DELETE_DEPENDENCIES, gn as dockerSpawnEnvWithSensitive, gr as ResourceUpdateNotSupportedError, gt as readConfigString, h as maskingRetryLogger, hn as buildDockerImage, hr as ResourceTimeoutError, ht as configStringRefusal, i as DeploymentEventsReader, in as escapeRegExp, it as IntrinsicFunctionResolver, j as formatResourceLine, jn as resolveSkipPrefix, jt as scrubResourceRecord, k as isStatefulRecreateTargetSync, kn as resolveAutoAssetStorage, kt as maskSecretsInText, l as replayRollback, ln as getBootstrapMarkerKey, lr as LocalStartServiceError, lt as WAFv2WebACLProvider, m as withResourceDeadline, mn as buildDenyExternalAccessPolicy, mr as ProvisioningError, mt as configBooleanRefusal, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as loadPublishableAssetManifest, nt as disableInstanceApiTermination, o as planFailedOps, on as AssetModeResolver, or as DynamicReferenceRegionAmbiguousError, ot as cfnRefValueFromPhysicalId, p as deleteSkipReason, pn as validateContainerRepoName, pr as PartialFailureError, pt as coerceCfnBoolean, q as ProviderRegistry, qn as processStackMessages, qt as displaySafe, r as DeployEngine, rn as rewriteTemplateAssetReferences, rr as CrossAccountSecretRefusalError, rt as isTerminationProtectionPropagationError, s as planRollback, sn as BOOTSTRAP_MARKER_PREFIX, sr as LocalInvokeBuildError$1, st as getAccountInfo, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as createAssetRedirectResolver, tr as CdkdError, tt as slowCcOperationTimeoutMs, u as updatePartialMessage, un as parseBootstrapMarker, ut as normalizeAwsTagsToCfn, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as getDockerCmd, vr as StackTerminationProtectionError, vt as requireConfigArray, w as refusesFinalSnapshot, wn as Synthesizer, wr as withErrorHandling, wt as createSecretMasker, x as ccRoutedFinalSnapshotError, xn as runDockerStreaming, xt as STATE_SOURCED_CROSS_GENERATION_RULES, y as PRE_DELETE_SNAPSHOT_TYPES, yn as partitionSensitiveEnv, yt as requireConfigObject, z as collectPublishedOutputNames, zn as MIGRATE_TMP_PREFIX, zt as applyRoleArnIfSet } from "./deploy-engine-Dnp730rc.js";
2
2
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as getCurrentStackName, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
3
- import { t as getCdkdVersion } from "./version-DZ6zMwCR.js";
4
- import { t as ASGProvider } from "./asg-provider-BBqefiXQ.js";
3
+ import { t as getCdkdVersion } from "./version-CJG_PFNb.js";
4
+ import { t as ASGProvider } from "./asg-provider-CynKJdi4.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -15887,6 +15887,155 @@ function autoScalingRetriesWithinDeleteBudget(budget, defaultMaxRetries) {
15887
15887
  const AUTO_SCALING_TEARDOWN_RESERVE_MS = 20 * 6e4;
15888
15888
  /** `withRetry`'s capped backoff step (`maxDelayMs`), the cost of one retry. */
15889
15889
  const AUTO_SCALING_RETRY_STEP_MS = 8e3;
15890
+ /**
15891
+ * {@link ProtectionFlipRecord}s keyed by the resource they describe, so a
15892
+ * `delete()` that is RE-ENTERED remembers what an earlier attempt did.
15893
+ *
15894
+ * Shaped after `ElapsedBudgetRegistry` in `../../utils/elapsed-budget.ts` and
15895
+ * keyed with the same {@link deleteBudgetKey}, because it is the same re-entry:
15896
+ * `destroy-runner.ts` wraps its outer retry loop — up to four `delete()` calls
15897
+ * — in ONE deadline, and re-invokes `delete()` for anything it classes as
15898
+ * retryable. A record created per call cannot see that. Attempt 1 observes the
15899
+ * guard ON, flips it off and fails on a throttle; attempt 2's pre-flip
15900
+ * `DescribeTable` now reports the guard OFF (attempt 1 is why), so a per-call
15901
+ * record starts and stays `false` and a TERMINAL failure on attempt 2
15902
+ * compensates nothing. That is precisely the issue's retry-then-fail case
15903
+ * (#1978).
15904
+ *
15905
+ * A provider INSTANCE FIELD would be wrong for the same reason the budget is
15906
+ * not one: providers are singletons serving concurrent resources, so one field
15907
+ * would carry another table's flip. The key qualifies the physical id by
15908
+ * region, exactly as the budget's does.
15909
+ *
15910
+ * Entries are RETAINED on a throw — that is the point — and released by the
15911
+ * caller on a terminal outcome (a completed delete, or a NotFound), so the map
15912
+ * holds at most one entry per in-flight resource.
15913
+ */
15914
+ var ProtectionFlipRegistry = class {
15915
+ records = /* @__PURE__ */ new Map();
15916
+ /**
15917
+ * The record for `key`, creating it on first use and REUSING it on re-entry.
15918
+ *
15919
+ * `reuseWithinMs` bounds how long an entry may be reused for and is passed
15920
+ * the same `DELETE_BUDGET_REUSE_WINDOW_MS` the allowance uses: past the
15921
+ * deadline the whole path is sized against, that deadline has certainly
15922
+ * fired, so a caller arriving now belongs to a NEW operation and must not
15923
+ * inherit a previous one's flip. Without it a retained record would make a
15924
+ * much later destroy of the same table re-enable a guard it never touched.
15925
+ */
15926
+ acquire(key, reuseWithinMs, clock = monotonicNowMs) {
15927
+ const existing = this.records.get(key);
15928
+ if (existing) {
15929
+ if (existing.age.elapsedMs() <= reuseWithinMs) return existing.record;
15930
+ this.records.delete(key);
15931
+ }
15932
+ const created = {
15933
+ record: {
15934
+ flippedOffByThisRun: false,
15935
+ deleteAccepted: false
15936
+ },
15937
+ age: new ElapsedBudget(reuseWithinMs, clock)
15938
+ };
15939
+ this.records.set(key, created);
15940
+ return created.record;
15941
+ }
15942
+ /** Drop `key`'s record — call on a TERMINAL outcome, never between retries. */
15943
+ release(key) {
15944
+ this.records.delete(key);
15945
+ }
15946
+ /** Live entries; exists so a test can prove release actually released. */
15947
+ get size() {
15948
+ return this.records.size;
15949
+ }
15950
+ clear() {
15951
+ this.records.clear();
15952
+ }
15953
+ };
15954
+ /**
15955
+ * Whether `error` ends the delete for good, i.e. whether `destroy-runner.ts`'s
15956
+ * outer loop will NOT re-enter `delete()` for it.
15957
+ *
15958
+ * This is the gate on the compensation, and it is the issue's own point:
15959
+ * re-enabling the guard after a RETRYABLE failure would flip the flag back and
15960
+ * forth across a retry sequence, since the next `delete()` immediately turns it
15961
+ * off again. Compensation belongs on the terminal failure only.
15962
+ *
15963
+ * The predicate MIRRORS `destroy-runner.ts`'s re-entry condition
15964
+ * (`!isMarkedNonRetryable && (isRetryableTransientError || 'Too Many
15965
+ * Requests')`) rather than inventing a second classification, and it is applied
15966
+ * to the error the provider is about to THROW — the same wrapped
15967
+ * `ProvisioningError` message the outer loop will classify — so the two cannot
15968
+ * disagree about who gets re-entered.
15969
+ *
15970
+ * A user abort is terminal here even though nothing classifies it: the run is
15971
+ * being torn down, so no re-entry is coming and the guard would otherwise stay
15972
+ * down. That is the Ctrl-C route recorded on the issue.
15973
+ *
15974
+ * KNOWN NARROWINGS, all deliberate. Each leaves the guard off in a case this
15975
+ * mechanism does not reach; none of them is silent about it here.
15976
+ *
15977
+ * 1. **Attempt-cap exhaustion.** A genuinely retryable failure that exhausts
15978
+ * the outer loop's attempt cap ends the run with the guard still off,
15979
+ * because the provider cannot see which attempt is the last one.
15980
+ * Compensating on every attempt instead would trade that residue for an
15981
+ * `UpdateTable` pair per retry against a table AWS is already throttling.
15982
+ * 2. **The per-resource DEADLINE route.** `src/deployment/resource-deadline.ts`
15983
+ * rejects the OUTER promise on its timer and does NOT cancel what it
15984
+ * wraps — the provider's own `await` never settles as a rejection, so no
15985
+ * `ResourceTimeoutError` ever enters `delete()`'s `catch` and nothing here
15986
+ * runs for it. The provider keeps polling behind a run that has already
15987
+ * reported failure (the same non-cancelling shape issue #1955 documents),
15988
+ * and if that poll eventually succeeds the table is gone anyway. Closing
15989
+ * this would mean making the deadline cancel — a change to a mechanism
15990
+ * every provider shares — so it is recorded, not fixed here.
15991
+ * 3. **The compensation itself is unbounded.** `reEnable` is one `UpdateTable`
15992
+ * with no timeout of its own, so on Ctrl-C it adds one SDK call per flipped
15993
+ * table to a teardown the user has already asked to end. Left unbounded on
15994
+ * purpose: an `UpdateTable` is a single control-plane round trip (no
15995
+ * polling, no wait for ACTIVE), the SDK's own retry/timeout config already
15996
+ * applies to it, and it is the ONLY thing standing between a Ctrl-C and a
15997
+ * live table with its guard stripped — a timeout short enough to be felt
15998
+ * during teardown would mostly convert successful restores into the "could
15999
+ * NOT re-enable" line. Revisit if the compensation ever grows a WAIT.
16000
+ */
16001
+ function isTerminalDeleteFailure(error) {
16002
+ if (isInterruptedWaitError(error)) return true;
16003
+ if (isMarkedNonRetryable(error)) return true;
16004
+ const message = error instanceof Error ? error.message : String(error);
16005
+ return !(isRetryableTransientError(error, message) || message.includes("Too Many Requests"));
16006
+ }
16007
+ /**
16008
+ * Put `DeletionProtectionEnabled` back after a `--remove-protection` flip whose
16009
+ * delete then failed terminally (issue #1978).
16010
+ *
16011
+ * BEST-EFFORT, and it says so. Three properties, each of which the issue names:
16012
+ *
16013
+ * - **It never masks the original error.** This function does not throw —
16014
+ * including when the re-enable itself fails — because it runs inside the
16015
+ * `catch` that is about to re-throw the delete failure, and a secondary
16016
+ * write that throws would REPLACE the reported outcome with its own. The
16017
+ * delete failure stays the outcome; this is a secondary line.
16018
+ * - **It never edits the primary message either.** Splicing the re-enable's
16019
+ * text into the thrown error would change what `isRetryableTransientError`
16020
+ * substring-matches on, so a compensation failure carrying a throttle phrase
16021
+ * could flip a terminal delete failure into a retryable one and re-run the
16022
+ * whole path. The narration goes to the logger, not into the throw.
16023
+ * - **It names the table when it fails.** A silent failure here leaves a LIVE
16024
+ * table with its guard down, which is the exact residue this mechanism
16025
+ * exists to remove, so the message carries the physical id and the one
16026
+ * command that fixes it.
16027
+ */
16028
+ async function compensateRemovedDeletionProtection(opts) {
16029
+ if (!opts.flip.flippedOffByThisRun) return;
16030
+ if (opts.flip.deleteAccepted) return;
16031
+ if (!isTerminalDeleteFailure(opts.error)) return;
16032
+ try {
16033
+ await opts.reEnable();
16034
+ opts.logger.warn(`DynamoDB ${opts.typeLabel} ${opts.logicalId}: the delete failed after --remove-protection had turned DeletionProtectionEnabled off, so it was re-enabled on ${opts.physicalId}. The delete failure below is the outcome.`);
16035
+ } catch (reEnableError) {
16036
+ opts.logger.error(`DynamoDB ${opts.typeLabel} ${opts.logicalId}: could NOT re-enable DeletionProtectionEnabled on ${opts.physicalId} after the delete failed — that table is LIVE with its deletion protection still off. Restore it with: aws dynamodb update-table --table-name ${opts.physicalId} --deletion-protection-enabled. (${reEnableError instanceof Error ? reEnableError.message : String(reEnableError)})`);
16037
+ }
16038
+ }
15890
16039
 
15891
16040
  //#endregion
15892
16041
  //#region src/provisioning/providers/dynamodb-table-provider.ts
@@ -16488,6 +16637,17 @@ var DynamoDBTableProvider = class {
16488
16637
  * provider instance.
16489
16638
  */
16490
16639
  deleteBudgets = new ElapsedBudgetRegistry();
16640
+ /**
16641
+ * What each in-flight delete did to `DeletionProtectionEnabled` (issue
16642
+ * #1978), keyed exactly as the allowance above and for exactly the same
16643
+ * reason: `destroy-runner.ts` re-enters `delete()` after a retryable failure,
16644
+ * and the second attempt's pre-flip `DescribeTable` sees the guard already
16645
+ * off BECAUSE the first attempt turned it off. A record created per call
16646
+ * would therefore read "nothing to undo" on precisely the retry-then-fail
16647
+ * case the issue names. Keyed rather than a plain field for the budget's
16648
+ * reason too — one provider instance serves a whole level's tables at once.
16649
+ */
16650
+ protectionFlips = new ProtectionFlipRegistry();
16491
16651
  constructor() {
16492
16652
  const awsClients = getAwsClients();
16493
16653
  this.dynamoDBClient = awsClients.dynamoDB;
@@ -16807,15 +16967,60 @@ var DynamoDBTableProvider = class {
16807
16967
  }
16808
16968
  /**
16809
16969
  * Delete a DynamoDB table
16970
+ *
16971
+ * A thin wrapper around {@link deleteTableResource} whose only job is the
16972
+ * `--remove-protection` compensation (issue #1978): if this run turned
16973
+ * `DeletionProtectionEnabled` off and the delete then ended for good — a
16974
+ * terminal AWS refusal, or a Ctrl-C landing anywhere after the flip — the
16975
+ * guard goes back on before the failure is re-thrown. Without it the run ends
16976
+ * with a LIVE table whose protection cdkd silently stripped: the failure is
16977
+ * loud, the side effect is not.
16978
+ *
16979
+ * It is a WRAPPER rather than a `catch` inside the body because the window is
16980
+ * not one `try`. Between the flip and a successful delete this provider can
16981
+ * throw from the index-settled re-arm, from the retry loop, and from an
16982
+ * interrupted wait, and the same shape on the sibling `GlobalTable` provider
16983
+ * spans a replica teardown as well. Catching at the boundary covers every
16984
+ * exit; the {@link ProtectionFlipRecord} — not the position of the catch — is
16985
+ * what decides whether there is anything to undo.
16810
16986
  */
16811
16987
  async delete(logicalId, physicalId, resourceType, _properties, context) {
16988
+ const flip = this.protectionFlips.acquire(deleteBudgetKey(physicalId, context?.expectedRegion), DYNAMODB_DELETE_MIN_RESOURCE_TIMEOUT_MS, resolveDynamoDbDeleteBudgetClock());
16989
+ try {
16990
+ await this.deleteTableResource(logicalId, physicalId, resourceType, context, flip);
16991
+ } catch (error) {
16992
+ await compensateRemovedDeletionProtection({
16993
+ flip,
16994
+ error,
16995
+ logicalId,
16996
+ physicalId,
16997
+ typeLabel: "table",
16998
+ logger: this.logger,
16999
+ reEnable: async () => {
17000
+ await this.dynamoDBClient.send(new UpdateTableCommand({
17001
+ TableName: physicalId,
17002
+ DeletionProtectionEnabled: true
17003
+ }));
17004
+ }
17005
+ });
17006
+ throw error;
17007
+ }
17008
+ }
17009
+ async deleteTableResource(logicalId, physicalId, resourceType, context, flip) {
16812
17010
  this.logger.debug(`Deleting DynamoDB table ${logicalId}: ${physicalId}`);
16813
17011
  const budget = this.deleteBudgets.acquire(deleteBudgetKey(physicalId, context?.expectedRegion), resolveDynamoDbDeleteBudgetMs(), resolveDynamoDbDeleteBudgetClock(), DYNAMODB_DELETE_MIN_RESOURCE_TIMEOUT_MS);
16814
17012
  if (context?.removeProtection === true) try {
17013
+ let observedProtectionOn = false;
17014
+ try {
17015
+ observedProtectionOn = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }))).Table?.DeletionProtectionEnabled === true;
17016
+ } catch (observeError) {
17017
+ this.logger.debug(`Could not read DeletionProtectionEnabled on ${physicalId} before disabling it: ${observeError instanceof Error ? observeError.message : String(observeError)}`);
17018
+ }
16815
17019
  await this.dynamoDBClient.send(new UpdateTableCommand({
16816
17020
  TableName: physicalId,
16817
17021
  DeletionProtectionEnabled: false
16818
17022
  }));
17023
+ if (observedProtectionOn) flip.flippedOffByThisRun = true;
16819
17024
  this.logger.debug(`Disabled DeletionProtectionEnabled on DynamoDB table ${logicalId}, waiting for ACTIVE`);
16820
17025
  try {
16821
17026
  await this.waitForTableActiveAfterUpdate(physicalId, 60, budget);
@@ -16841,6 +17046,7 @@ var DynamoDBTableProvider = class {
16841
17046
  logger: this.logger,
16842
17047
  deleteTable: async () => {
16843
17048
  await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
17049
+ flip.deleteAccepted = true;
16844
17050
  },
16845
17051
  reArm: () => waitForIndexesSettled({
16846
17052
  tableName: physicalId,
@@ -16859,11 +17065,13 @@ var DynamoDBTableProvider = class {
16859
17065
  });
16860
17066
  this.logger.debug(`Successfully deleted DynamoDB table ${logicalId}`);
16861
17067
  this.deleteBudgets.release(deleteBudgetKey(physicalId, context?.expectedRegion));
17068
+ this.protectionFlips.release(deleteBudgetKey(physicalId, context?.expectedRegion));
16862
17069
  } catch (error) {
16863
17070
  if (error instanceof ResourceNotFoundException$1) {
16864
17071
  assertRegionMatch(await this.dynamoDBClient.config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
16865
17072
  this.logger.debug(`DynamoDB table ${physicalId} does not exist, skipping deletion`);
16866
17073
  this.deleteBudgets.release(deleteBudgetKey(physicalId, context?.expectedRegion));
17074
+ this.protectionFlips.release(deleteBudgetKey(physicalId, context?.expectedRegion));
16867
17075
  return;
16868
17076
  }
16869
17077
  const cause = error instanceof Error ? error : void 0;
@@ -18064,6 +18272,17 @@ var DynamoDBGlobalTableProvider = class {
18064
18272
  * terms it closes.
18065
18273
  */
18066
18274
  deleteBudgets = new ElapsedBudgetRegistry();
18275
+ /**
18276
+ * What each in-flight delete did to `DeletionProtectionEnabled` (issue
18277
+ * #1978), keyed exactly as the allowance above and for exactly the same
18278
+ * reason: `destroy-runner.ts` re-enters `delete()` after a retryable failure,
18279
+ * and the second attempt's pre-flip `DescribeTable` sees the guard already
18280
+ * off BECAUSE the first attempt turned it off. A record created per call
18281
+ * would therefore read "nothing to undo" on precisely the retry-then-fail
18282
+ * case the issue names. Keyed rather than a plain field for the budget's
18283
+ * reason too — one provider instance serves a whole level's tables at once.
18284
+ */
18285
+ protectionFlips = new ProtectionFlipRegistry();
18067
18286
  handledProperties = /* @__PURE__ */ new Map([["AWS::DynamoDB::GlobalTable", /* @__PURE__ */ new Set([
18068
18287
  "TableName",
18069
18288
  "KeySchema",
@@ -19332,13 +19551,42 @@ var DynamoDBGlobalTableProvider = class {
19332
19551
  * strip a still-existing resource from state.
19333
19552
  */
19334
19553
  async delete(logicalId, physicalId, resourceType, _properties, context) {
19554
+ const flip = this.protectionFlips.acquire(deleteBudgetKey(physicalId, context?.expectedRegion), DYNAMODB_DELETE_MIN_RESOURCE_TIMEOUT_MS, resolveDynamoDbDeleteBudgetClock());
19555
+ try {
19556
+ await this.deleteGlobalTableResource(logicalId, physicalId, resourceType, context, flip);
19557
+ } catch (error) {
19558
+ await compensateRemovedDeletionProtection({
19559
+ flip,
19560
+ error,
19561
+ logicalId,
19562
+ physicalId,
19563
+ typeLabel: "GlobalTable",
19564
+ logger: this.logger,
19565
+ reEnable: async () => {
19566
+ await this.dynamoDBClient.send(new UpdateTableCommand({
19567
+ TableName: physicalId,
19568
+ DeletionProtectionEnabled: true
19569
+ }));
19570
+ }
19571
+ });
19572
+ throw error;
19573
+ }
19574
+ }
19575
+ async deleteGlobalTableResource(logicalId, physicalId, resourceType, context, flip) {
19335
19576
  this.logger.debug(`Deleting DynamoDB GlobalTable ${logicalId}: ${physicalId}`);
19336
19577
  const budget = this.deleteBudgets.acquire(deleteBudgetKey(physicalId, context?.expectedRegion), resolveDynamoDbDeleteBudgetMs(), resolveDynamoDbDeleteBudgetClock(), DYNAMODB_DELETE_MIN_RESOURCE_TIMEOUT_MS);
19337
19578
  if (context?.removeProtection === true) try {
19579
+ let observedProtectionOn = false;
19580
+ try {
19581
+ observedProtectionOn = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }))).Table?.DeletionProtectionEnabled === true;
19582
+ } catch (observeError) {
19583
+ this.logger.debug(`Could not read DeletionProtectionEnabled on ${physicalId} before disabling it: ${observeError instanceof Error ? observeError.message : String(observeError)}`);
19584
+ }
19338
19585
  await this.dynamoDBClient.send(new UpdateTableCommand({
19339
19586
  TableName: physicalId,
19340
19587
  DeletionProtectionEnabled: false
19341
19588
  }));
19589
+ if (observedProtectionOn) flip.flippedOffByThisRun = true;
19342
19590
  this.logger.debug(`Disabled DeletionProtectionEnabled on ${logicalId}, waiting for ACTIVE`);
19343
19591
  try {
19344
19592
  await this.waitForTableActiveAfterUpdate(physicalId, logicalId, 600, budget);
@@ -19407,6 +19655,7 @@ var DynamoDBGlobalTableProvider = class {
19407
19655
  logger: this.logger,
19408
19656
  deleteTable: async () => {
19409
19657
  await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
19658
+ flip.deleteAccepted = true;
19410
19659
  },
19411
19660
  reArm: () => this.waitForIndexesActive(physicalId, logicalId, {
19412
19661
  maxAttempts: 60,
@@ -19423,11 +19672,13 @@ var DynamoDBGlobalTableProvider = class {
19423
19672
  await this.waitForTableGone(physicalId, logicalId, 600, budget);
19424
19673
  this.logger.debug(`Successfully deleted DynamoDB GlobalTable ${logicalId}`);
19425
19674
  this.deleteBudgets.release(deleteBudgetKey(physicalId, context?.expectedRegion));
19675
+ this.protectionFlips.release(deleteBudgetKey(physicalId, context?.expectedRegion));
19426
19676
  } catch (error) {
19427
19677
  if (error instanceof ResourceNotFoundException$1) {
19428
19678
  assertRegionMatch(await this.dynamoDBClient.config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
19429
19679
  this.logger.debug(`DynamoDB GlobalTable ${physicalId} does not exist, skipping`);
19430
19680
  this.deleteBudgets.release(deleteBudgetKey(physicalId, context?.expectedRegion));
19681
+ this.protectionFlips.release(deleteBudgetKey(physicalId, context?.expectedRegion));
19431
19682
  return;
19432
19683
  }
19433
19684
  const cause = error instanceof Error ? error : void 0;
@@ -65558,6 +65809,10 @@ var DriftDetectedError = class DriftDetectedError extends CdkdError {
65558
65809
  * a partially-failed `--revert` still exits 2 through `PartialFailureError`) --
65559
65810
  * those modes report per-resource refusals on their own paths, and changing them
65560
65811
  * would alter what a remediation run means, which is not what this closes.
65812
+ * Issue [#2208](https://github.com/go-to-k/cdkd/issues/2208) re-examined that
65813
+ * scoping and KEPT it: what those modes owed was not this exit code but an
65814
+ * honest SENTENCE, since their no-drift line claimed a clean bill of health for
65815
+ * a stack they had not read. See {@link incompleteRemediationMessage}.
65561
65816
  *
65562
65817
  * WHAT DOES NOT TRIGGER IT: a stack whose properties reference
65563
65818
  * `{{resolve:ssm-secure:...}}` still exits 0. Those properties genuinely are not
@@ -65652,6 +65907,171 @@ function outcomeExitSignal(outcome) {
65652
65907
  });
65653
65908
  }
65654
65909
  /**
65910
+ * What {@link incompleteRemediationMessage} says about each reason, and WHICH
65911
+ * CLAIM it is allowed to make about it.
65912
+ *
65913
+ * An exhaustive `Record`, for the same reason {@link notComparedReason} is one:
65914
+ * adding a cause must be a COMPILE ERROR here rather than falling into an
65915
+ * `else` that describes it as something it is not. The first cut of this
65916
+ * function had that `else`, and it was worse than a wrong default -- it
65917
+ * described any unknown cause as a dynamic-reference problem, while
65918
+ * {@link outcomeExitSignal} deliberately routes every NEW cause to the
65919
+ * incomplete side. The two together meant the next cause added would be
65920
+ * reported, at once, to the widest audience and under the wrong name. Carrying
65921
+ * `kind` in the SAME record rather than a second one beside it means a new
65922
+ * reason cannot be given a phrase while quietly inheriting a claim.
65923
+ *
65924
+ * `kind` exists because one blanket tail overclaimed. The line used to end
65925
+ * `..., so cdkd does not know whether they drifted`, which is true of a
65926
+ * throttled read and FALSE of a `Custom::*` resource -- for that one, issue
65927
+ * [#323](https://github.com/go-to-k/cdkd/issues/323)'s position is that drift is
65928
+ * not APPLICABLE, not that cdkd is uncertain. A stack with one throttle and five
65929
+ * `Custom::S3AutoDeleteObjects` would have claimed uncertainty about all six.
65930
+ *
65931
+ * The insertion ORDER is the order the phrases are emitted in, so the line is
65932
+ * deterministic without a second list to keep in sync: "not compared at all"
65933
+ * first, then the partial ones, then the two structural reasons.
65934
+ *
65935
+ * The wording splits on "was ANY of it compared", which is the same split the
65936
+ * human report's `NOT fully compared` heading makes -- a `readFailed` resource
65937
+ * is not "partially" anything.
65938
+ */
65939
+ const UNCOMPARED_REASONS = {
65940
+ readFailed: {
65941
+ kind: "unknown",
65942
+ phrase: "not compared AT ALL: the read or comparison failed"
65943
+ },
65944
+ refused: {
65945
+ kind: "unknown",
65946
+ phrase: "only PARTIALLY compared: cdkd refused to resolve a dynamic reference their state records"
65947
+ },
65948
+ unresolvedToken: {
65949
+ kind: "unknown",
65950
+ phrase: "only PARTIALLY compared: their state records a `{{resolve:...}}` spelling cdkd resolves for nobody, which no re-run can clear"
65951
+ },
65952
+ unsupported: {
65953
+ kind: "byDesign",
65954
+ phrase: "not compared AT ALL: their provider does not support drift detection yet"
65955
+ },
65956
+ skipped: {
65957
+ kind: "byDesign",
65958
+ phrase: "not compared AT ALL: cdkd does not drift-check the type"
65959
+ }
65960
+ };
65961
+ /**
65962
+ * The sentence lead each {@link UncomparedKind} group is reported under.
65963
+ *
65964
+ * Exhaustive for the same reason the record above is, and SEPARATE from it
65965
+ * because the lead is per GROUP while the phrase is per reason: writing the
65966
+ * claim into each phrase would put five copies of two sentences in the file,
65967
+ * and five copies is how two of them come to disagree.
65968
+ *
65969
+ * Insertion order is emit order, and `unknown` comes first deliberately: it is
65970
+ * the half the user can do something about.
65971
+ */
65972
+ const UNCOMPARED_KIND_LEADS = {
65973
+ unknown: "cdkd does not know whether these drifted",
65974
+ byDesign: "Not drift-checked by cdkd at all, which is a coverage limit rather than uncertainty"
65975
+ };
65976
+ /**
65977
+ * How many resources across every report ended UNCOMPARED, per reason.
65978
+ *
65979
+ * The `notCompared` population is read through {@link notComparedOutcomes} --
65980
+ * the single spelling both renderings already share -- and the other two come
65981
+ * from an exhaustive `matchOutcome`, so a new outcome variant cannot join the
65982
+ * "nothing to say about it" side by omission. That is the same guarantee
65983
+ * {@link outcomeExitSignal} carries at the exit code, applied to the count.
65984
+ */
65985
+ function uncomparedTally(reports) {
65986
+ const tally = /* @__PURE__ */ new Map();
65987
+ const bump = (reason) => {
65988
+ tally.set(reason, (tally.get(reason) ?? 0) + 1);
65989
+ };
65990
+ for (const report of reports) {
65991
+ for (const { cause } of notComparedOutcomes(report)) bump(cause);
65992
+ for (const outcome of report.outcomes) matchOutcome(outcome, {
65993
+ drifted: () => {},
65994
+ clean: () => {},
65995
+ notCompared: () => {},
65996
+ unsupported: () => bump("unsupported"),
65997
+ skipped: () => bump("skipped")
65998
+ });
65999
+ }
66000
+ return tally;
66001
+ }
66002
+ /**
66003
+ * The lines `--accept` / `--revert` print INSTEAD of `No drift detected --
66004
+ * nothing to accept.` when the run found no drift but did not manage to compare
66005
+ * everything ([issue #2208](https://github.com/go-to-k/cdkd/issues/2208)).
66006
+ *
66007
+ * MESSAGE-ONLY, and the EXIT CODE deliberately stays `0`. The remediation
66008
+ * modes' exit codes are a documented user contract -- #2108 scoped its `2` to
66009
+ * detection-only mode on purpose, and {@link DriftComparisonIncompleteError}'s
66010
+ * note says why: "changing them would alter what a remediation run means".
66011
+ * Nothing about that reasoning expired. The defect this closes is entirely in
66012
+ * what the run REPORTS: `--accept` / `--revert` already correctly leave an
66013
+ * uncompared resource alone (both iterate the drifted outcomes only, asserted
66014
+ * in `tests/unit/cli/drift-per-resource-failure.test.ts`), so the state and AWS
66015
+ * are right and only the sentence is wrong. Making the remediation path exit
66016
+ * non-zero would break the CI of everyone running `cdkd drift --accept` over a
66017
+ * stack that hits a throttle, to fix a wording problem.
66018
+ *
66019
+ * What the lines have to carry, since the exit code will not carry it: that the
66020
+ * comparison was INCOMPLETE (never that no drift was detected), HOW MANY
66021
+ * resources were not compared and WHY, WHICH of those cdkd is actually
66022
+ * uncertain about, and the pointer to the detection-only run, which is the mode
66023
+ * whose exit code does report it (`2`).
66024
+ *
66025
+ * THE TRIGGER AND THE COUNT ARE DIFFERENT POPULATIONS, deliberately, and
66026
+ * collapsing them is the defect review round 1 found here.
66027
+ *
66028
+ * - The TRIGGER is `anyIncomplete`, i.e. {@link outcomeExitSignal}'s
66029
+ * `incomplete`. It is narrow on purpose: a stack whose ONLY uncompared
66030
+ * resource holds a `{{resolve:ssm-secure:...}}` token must not start
66031
+ * shouting on every run about a comparison no action of the user's can ever
66032
+ * complete -- the same CI-forever hazard that cause is kept out of the exit
66033
+ * code on.
66034
+ * - The COUNT and the LABELS, once the line is triggered, cover EVERY
66035
+ * uncompared resource, each named by its own reason (see
66036
+ * {@link uncomparedTally}). A resource that was not compared was not
66037
+ * compared, whatever the reason, and counting only the clearable ones
66038
+ * printed `1 of 3` on a stack the report a few lines above called
66039
+ * `2 resource(s) NOT fully compared` -- two lines disagreeing about one
66040
+ * run, with the newer and quieter one being the one written to stop a
66041
+ * command from being quietly reassuring.
66042
+ *
66043
+ * WHAT `N` AND `M` ARE, stated because the alternatives are all defensible and
66044
+ * silence about the choice is what made the first cut wrong. `M` is the total
66045
+ * number of resource outcomes, and `N` is every one of them that was not
66046
+ * compared -- so `unsupported` and `skipped` land in `N`, WITH their own
66047
+ * phrase, rather than sitting only in the denominator where they would inflate
66048
+ * `M` and go unnamed. On a remediation path, erring LOUD is the correct
66049
+ * direction. Note this makes `N` deliberately WIDER than the report's
66050
+ * `NOT fully compared` heading, which counts the reference / read population
66051
+ * only and reports `drift unknown` separately: the question this line answers
66052
+ * is #2154's, "was everything actually compared", not "how many entries are in
66053
+ * that block". What being in `N` does NOT buy is a uniform claim about the
66054
+ * resource -- see {@link UNCOMPARED_REASONS}'s `kind`, and see
66055
+ * {@link UncomparedReason} for why counting a `skipped` resource here is not a
66056
+ * reversal of #323's silence.
66057
+ */
66058
+ function incompleteRemediationMessage(reports, mode) {
66059
+ const tally = uncomparedTally(reports);
66060
+ const total = reports.reduce((n, report) => n + report.outcomes.length, 0);
66061
+ const uncompared = [...tally.values()].reduce((a, b) => a + b, 0);
66062
+ const kinds = Object.keys(UNCOMPARED_KIND_LEADS);
66063
+ const reasons = Object.keys(UNCOMPARED_REASONS);
66064
+ const groups = kinds.flatMap((kind) => {
66065
+ const parts = reasons.filter((reason) => UNCOMPARED_REASONS[reason].kind === kind && (tally.get(reason) ?? 0) > 0).map((reason) => `${tally.get(reason)} ${UNCOMPARED_REASONS[reason].phrase}`);
66066
+ return parts.length === 0 ? [] : [`${UNCOMPARED_KIND_LEADS[kind]} — ${parts.join("; ")}.`];
66067
+ });
66068
+ return [
66069
+ `Comparison INCOMPLETE — nothing to ${mode}, and that is NOT a clean bill of health: ${uncompared} of ${total} resource(s) could not be compared.`,
66070
+ ...groups,
66071
+ `Re-run 'cdkd drift' without --${mode} to see which resources and why — a detection-only run exits 2 while a comparison is incomplete.`
66072
+ ];
66073
+ }
66074
+ /**
65655
66075
  * `cdkd drift [<stack>...]` command implementation.
65656
66076
  *
65657
66077
  * Three operating modes (mutually exclusive):
@@ -65728,6 +66148,10 @@ async function driftCommand(stacks, options) {
65728
66148
  return;
65729
66149
  }
65730
66150
  if (!drifted) {
66151
+ if (anyIncomplete) {
66152
+ for (const line of incompleteRemediationMessage(reports, options.accept ? "accept" : "revert")) logger.info(line);
66153
+ return;
66154
+ }
65731
66155
  logger.info(options.accept ? "No drift detected — nothing to accept." : "No drift detected — nothing to revert.");
65732
66156
  return;
65733
66157
  }
@@ -86808,4 +87232,4 @@ function buildProgram() {
86808
87232
 
86809
87233
  //#endregion
86810
87234
  export { buildProgram };
86811
- //# sourceMappingURL=program-Bd0gnMR0.js.map
87235
+ //# sourceMappingURL=program-DjH_cfVU.js.map