@go-to-k/cdkd 0.281.14 → 0.281.16

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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as replayWarn, $t as findLargeInlineResources, A as green, At as buildDockerImage, B as slowCcOperationTimeoutMs, Bt as getLegacyStateBucketName, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as ResourceTimeoutError, Ct as AssetModeResolver, D as bold, Dt as parseBootstrapMarker, E as formatResourceLine, En as StackTerminationProtectionError, Et as getBootstrapMarkerKey, F as clearOnUpdateRemoval, Ft as AssetManifestLoader, G as getAccountInfo, Gt as resolveStateBucketWithDefault, H as isTerminationProtectionPropagationError, Ht as resolveAutoAssetStorage, I as ProviderRegistry, It as getDockerImageBySourceHash, J as normalizeAwsTagsToCfn, Jt as stateBucketExistenceConfirmed, K as refStateLookupFromResource, Kt as resolveStateBucketWithDefaultAndSource, L as findActionableSilentDrops, Lt as Synthesizer, M as yellow, Mn as withErrorHandling, Mt as getDockerCmd, N as IAMRoleProvider, Nt as runDockerForeground, O as cyan, Ot as validateAssetBucketName, P as collectInlinePolicyNamesManagedBySiblings, Pt as runDockerStreaming, Q as readConfigString, Qt as MIGRATE_TMP_PREFIX, R as findSilentDropProperties, Rt as synthesisStatusMessage, S as extractDeploymentEventError, Sn as ProvisioningError, St as rewriteTemplateAssetReferences, T as renderStatefulReason, Tn as StackHasActiveImportsError, Tt as ensureAssetStorage, U as IntrinsicFunctionResolver, Ut as resolveCaptureObservedState, V as disableInstanceApiTermination, Vt as resolveApp, W as cfnRefValueFromPhysicalId, Wt as resolveSkipPrefix, X as assertRegionMatch, Xt as CFN_TEMPLATE_BODY_LIMIT, Y as resolveExplicitPhysicalId, Yt as warnDeprecatedNoPrefixCliFlag, Z as configStringRefusal, Zt as CFN_TEMPLATE_URL_LIMIT, _ as createPreDeleteFinalSnapshot, _n as LocalStartServiceError, _t as stringifyValue, a as DeploymentEventsStore, an as resolveBucketRegion, at as DiffCalculator, b as unsupportedFinalSnapshotError, bn as NestedStackChildDirectDestroyError, bt as createAssetRedirectResolver, c as replayFailedOperations, cn as resetAwsClients, ct as isRetryableTransientError, d as IMPLICIT_DELETE_DEPENDENCIES, dn as CdkdError, dt as TemplateParser, en as uploadCfnTemplate, et as requireConfigArray, f as computeImplicitDeleteEdges, ft as LockManager, g as ccRoutedFinalSnapshotError, gn as LocalMigrateError, gt as AssetPublisher, h as buildFinalSnapshotIdentifier, hn as LocalInvokeBuildError$1, ht as shouldRetainResource, i as DeploymentEventsReader, it as applyRoleArnIfSet, j as red, jn as normalizeAwsError, jt as formatDockerLoginError, k as gray, kt as validateContainerRepoName, l as replayRollback, ln as setAwsClients, lt as isThrottlingError, m as PRE_DELETE_SNAPSHOT_TYPES, mn as DeployCancelledError, mt as rebuildClientForBucketRegion, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as AssemblyReader, nt as requireConfigString, o as planFailedOps, on as AwsClients, ot as describeTypeWithThrottleRetry, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pt as S3StateBackend, q as WAFv2WebACLProvider, qt as resolveUseCdkBootstrapAssets, r as DeployEngine, rn as processStackMessages, rt as derivePartitionAndUrlSuffix, s as planRollback, sn as getAwsClients, st as withRetry, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as expectedOwnerParam, tt as requireConfigObject, u as withResourceDeadline, ut as DagBuilder, v as isFinalSnapshotError, vt as WorkGraph, w as isStatefulRecreateTargetSync, wn as ResourceUpdateNotSupportedError, wt as BOOTSTRAP_MARKER_PREFIX, x as makeCanonicalizePropertiesFn, xn as PartialFailureError, xt as loadPublishableAssetManifest, y as refusesFinalSnapshot, yn as MissingCdkCliError, yt as buildAssetRedirectMap, z as CloudControlProvider, zt as getDefaultStateBucketName } from "./deploy-engine-3BZtmJqZ.js";
2
+ import { $ as replayWarn, $t as findLargeInlineResources, A as green, At as buildDockerImage, B as slowCcOperationTimeoutMs, Bt as getLegacyStateBucketName, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as ResourceTimeoutError, Ct as AssetModeResolver, D as bold, Dt as parseBootstrapMarker, E as formatResourceLine, En as StackTerminationProtectionError, Et as getBootstrapMarkerKey, F as clearOnUpdateRemoval, Ft as AssetManifestLoader, G as getAccountInfo, Gt as resolveStateBucketWithDefault, H as isTerminationProtectionPropagationError, Ht as resolveAutoAssetStorage, I as ProviderRegistry, It as getDockerImageBySourceHash, J as normalizeAwsTagsToCfn, Jt as stateBucketExistenceConfirmed, K as refStateLookupFromResource, Kt as resolveStateBucketWithDefaultAndSource, L as findActionableSilentDrops, Lt as Synthesizer, M as yellow, Mn as withErrorHandling, Mt as getDockerCmd, N as IAMRoleProvider, Nt as runDockerForeground, O as cyan, Ot as validateAssetBucketName, P as collectInlinePolicyNamesManagedBySiblings, Pt as runDockerStreaming, Q as readConfigString, Qt as MIGRATE_TMP_PREFIX, R as findSilentDropProperties, Rt as synthesisStatusMessage, S as extractDeploymentEventError, Sn as ProvisioningError, St as rewriteTemplateAssetReferences, T as renderStatefulReason, Tn as StackHasActiveImportsError, Tt as ensureAssetStorage, U as IntrinsicFunctionResolver, Ut as resolveCaptureObservedState, V as disableInstanceApiTermination, Vt as resolveApp, W as cfnRefValueFromPhysicalId, Wt as resolveSkipPrefix, X as assertRegionMatch, Xt as CFN_TEMPLATE_BODY_LIMIT, Y as resolveExplicitPhysicalId, Yt as warnDeprecatedNoPrefixCliFlag, Z as configStringRefusal, Zt as CFN_TEMPLATE_URL_LIMIT, _ as createPreDeleteFinalSnapshot, _n as LocalStartServiceError, _t as stringifyValue, a as DeploymentEventsStore, an as resolveBucketRegion, at as DiffCalculator, b as unsupportedFinalSnapshotError, bn as NestedStackChildDirectDestroyError, bt as createAssetRedirectResolver, c as replayFailedOperations, cn as resetAwsClients, ct as isRetryableTransientError, d as IMPLICIT_DELETE_DEPENDENCIES, dn as CdkdError, dt as TemplateParser, en as uploadCfnTemplate, et as requireConfigArray, f as computeImplicitDeleteEdges, ft as LockManager, g as ccRoutedFinalSnapshotError, gn as LocalMigrateError, gt as AssetPublisher, h as buildFinalSnapshotIdentifier, hn as LocalInvokeBuildError$1, ht as shouldRetainResource, i as DeploymentEventsReader, it as applyRoleArnIfSet, j as red, jn as normalizeAwsError, jt as formatDockerLoginError, k as gray, kt as validateContainerRepoName, l as replayRollback, ln as setAwsClients, lt as isThrottlingError, m as PRE_DELETE_SNAPSHOT_TYPES, mn as DeployCancelledError, mt as rebuildClientForBucketRegion, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as AssemblyReader, nt as requireConfigString, o as planFailedOps, on as AwsClients, ot as describeTypeWithThrottleRetry, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pt as S3StateBackend, q as WAFv2WebACLProvider, qt as resolveUseCdkBootstrapAssets, r as DeployEngine, rn as processStackMessages, rt as derivePartitionAndUrlSuffix, s as planRollback, sn as getAwsClients, st as withRetry, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as expectedOwnerParam, tt as requireConfigObject, u as withResourceDeadline, ut as DagBuilder, v as isFinalSnapshotError, vt as WorkGraph, w as isStatefulRecreateTargetSync, wn as ResourceUpdateNotSupportedError, wt as BOOTSTRAP_MARKER_PREFIX, x as makeCanonicalizePropertiesFn, xn as PartialFailureError, xt as loadPublishableAssetManifest, y as refusesFinalSnapshot, yn as MissingCdkCliError, yt as buildAssetRedirectMap, z as CloudControlProvider, zt as getDefaultStateBucketName } from "./deploy-engine-CfnMSwyx.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, 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";
4
- import { t as ASGProvider } from "./asg-provider-C1WbHit9.js";
4
+ import { t as ASGProvider } from "./asg-provider-B1pHGT_X.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";
@@ -4892,18 +4892,270 @@ function s3BucketDestinationPath(dest, destinationPath) {
4892
4892
  return isPlainObject$1(dest) && isFlattenedDestination(dest) ? destinationPath : `${destinationPath}.S3BucketDestination`;
4893
4893
  }
4894
4894
  /**
4895
- * The structural path segments relative to the enclosing `Destination` block —
4896
- * of the bag `resolveS3BucketDestination` PICKED, so a substituted value can be
4897
- * written back at the key the template actually declared (issue #1670).
4895
+ * The FLATTENED CFn `Destination` block cdkd actually SENDS for `declared`
4896
+ * (issue #1707) — the shared fold behind BOTH halves of the #1633 pair: the
4897
+ * per-item `effectiveProperties` record and
4898
+ * {@link S3BucketProvider.canonicalizeDesiredProperties}.
4899
+ *
4900
+ * The desired side accepts TWO spellings (`resolveS3BucketDestination` picks
4901
+ * either the flattened CFn block or the nested SDK `S3BucketDestination` one)
4902
+ * plus a `Bucket` / `BucketArn` alias inside either, while the readback
4903
+ * (`inventorySdkToCfn` / `analyticsSdkToCfn`) emits ONLY the flattened form with
4904
+ * `BucketArn`. So a record written in any other spelling can never match what
4905
+ * `readCurrentState` returns: `cdkd drift` re-reports it forever and `--revert`
4906
+ * re-issues the same Put — the never-emitted-KEY class of issue #1686, reached
4907
+ * through the SHAPE rather than through a value. The live CFn registry schema
4908
+ * agrees the flattened form is the only CFn spelling (`Destination` is
4909
+ * `{BucketArn, BucketAccountId, Format, Prefix}` with `additionalProperties:
4910
+ * false`), so `S3BucketDestination` and `Bucket` are cdkd-only tolerances.
4911
+ *
4912
+ * This NORMALIZES rather than retracting the tolerance, per the #1686 rule:
4913
+ * refusing the SDK spelling would break templates that deploy today. It also
4914
+ * supersedes #1670's write-back-at-the-DECLARED-branch decision, whose whole
4915
+ * reason was that writing at a hardcoded branch would leave the malformed value
4916
+ * alive at the other key and add a stray one — there is no other key left once
4917
+ * the block is normalized wholesale.
4918
+ *
4919
+ * `Format` is the one member a caller can override: the applier passes the
4920
+ * value it SENT, which differs from the declared one when the warn-and-
4921
+ * SUBSTITUTE read replaced it (#1670). Absent on BOTH sides it defaults to the
4922
+ * `'CSV'` the appliers send, so the record carries what AWS will report back
4923
+ * rather than one key fewer.
4924
+ *
4925
+ * @returns the flattened block, or `declared` BY IDENTITY when it already is
4926
+ * one (so an ordinary template records byte-for-byte what it declared) or is
4927
+ * not a shape this can fold at all (absent / malformed — left for the
4928
+ * resolver's own refusal to report).
4929
+ */
4930
+ function effectiveS3BucketDestination(declared, sentFormat) {
4931
+ if (!isPlainObject$1(declared)) return declared;
4932
+ const bag = isFlattenedDestination(declared) ? declared : declared["S3BucketDestination"];
4933
+ if (!isPlainObject$1(bag)) return declared;
4934
+ if (!bag["BucketArn"] && !bag["Bucket"]) return declared;
4935
+ const out = {};
4936
+ const bucketArn = bag["BucketArn"] ?? bag["Bucket"];
4937
+ if (bucketArn !== void 0) out["BucketArn"] = bucketArn;
4938
+ if (bag["BucketAccountId"] !== void 0) out["BucketAccountId"] = bag["BucketAccountId"];
4939
+ out["Format"] = sentFormat ?? declaredOrDefault(bag, "Format", INVENTORY_DESTINATION_DEFAULT_FORMAT);
4940
+ if (bag["Prefix"] !== void 0) out["Prefix"] = bag["Prefix"];
4941
+ return sameScalarRecord(out, declared) ? declared : out;
4942
+ }
4943
+ /**
4944
+ * `container[key]` when the key is DECLARED, else `fallback` — presence-based,
4945
+ * NEVER `??`, and that distinction is the whole guard against issue #1670's
4946
+ * finding 3.
4947
+ *
4948
+ * The ONE licensed `??` in these folds is the ALIAS read
4949
+ * `bag['BucketArn'] ?? bag['Bucket']`, and it is not an exception to this rule —
4950
+ * it supplies no DEFAULT. It picks between two spellings of the same declared
4951
+ * value, exactly as the applier's own read does, so a nullish `BucketArn`
4952
+ * correctly falls through to the alias the wire would have used. When neither is
4953
+ * declared the key is simply absent from the fold's output, which is what makes
4954
+ * a bucket-less block identity-return rather than gain a phantom key.
4955
+ *
4956
+ * The default belongs to a key the template OMITTED: there the provider sends
4957
+ * it, the readback reports it, and folding both sides is what makes them agree.
4958
+ * A key DECLARED with a malformed value is a different case entirely — the
4959
+ * provider warns and substitutes, records what it SENT, and `cdkd diff` must go
4960
+ * on reporting the difference until the template is corrected, because that
4961
+ * report plus the warning are the user's only signal. A `??` chain cannot tell
4962
+ * the two apart: it treats a declared `null` as absent and quietly folds the
4963
+ * template side onto the substituted value, so the comparison comes out equal,
4964
+ * the provider is never called, and the warning STOPS — the exact concealment
4965
+ * #1670 refused a twin over. Measured on `Format: null` while reviewing this
4966
+ * change; a malformed value that is not nullish (a blank string, an array, an
4967
+ * unresolved intrinsic) never hit it, so the unit rows written first all passed.
4968
+ */
4969
+ function declaredOrDefault(container, key, fallback) {
4970
+ return key in container ? container[key] : fallback;
4971
+ }
4972
+ /**
4973
+ * The default `Destination.Format` both per-item appliers send when the block
4974
+ * declares none. Named rather than repeated so the wire value and the fold above
4975
+ * cannot drift apart.
4976
+ */
4977
+ const INVENTORY_DESTINATION_DEFAULT_FORMAT = "CSV";
4978
+ /**
4979
+ * The default `Enabled` / `IncludedObjectVersions` an inventory item is SENT
4980
+ * with when it declares none — the same drift-apart concern as
4981
+ * {@link INVENTORY_DESTINATION_DEFAULT_FORMAT}.
4982
+ */
4983
+ const INVENTORY_DEFAULT_ENABLED = true;
4984
+ const INVENTORY_DEFAULT_INCLUDED_OBJECT_VERSIONS = "All";
4985
+ /** The `OutputSchemaVersion` an analytics data export is SENT with by default. */
4986
+ const ANALYTICS_DEFAULT_OUTPUT_SCHEMA_VERSION = "V_1";
4987
+ /**
4988
+ * Are two records equal as SCALAR-valued bags? Used only to identity-return an
4989
+ * already-canonical block, so a fold that changes nothing cannot manufacture a
4990
+ * fresh object (and, through it, a fresh diff line on a template nobody edited).
4991
+ *
4992
+ * Scalar-only is sufficient here: every member of the CFn `Destination`
4993
+ * definition is a scalar (`{BucketArn, BucketAccountId, Format, Prefix}`).
4994
+ *
4995
+ * Precisely (review of #1707 corrected an over-claim): a nested value is
4996
+ * compared by REFERENCE like any other. On the flattened branch the fold copies
4997
+ * the caller's own references, so a nested value is `===` itself and identity IS
4998
+ * returned — which is correct, since nothing changed. It is only a
4999
+ * REBUILT nested value that compares different, and that falls back to recording
5000
+ * the folded block — the safe direction.
5001
+ */
5002
+ function sameScalarRecord(a, b) {
5003
+ const aKeys = Object.keys(a);
5004
+ if (aKeys.length !== Object.keys(b).length) return false;
5005
+ return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && Object.is(a[k], b[k]));
5006
+ }
5007
+ /** The `ScheduleFrequency` an inventory item is SENT with when it declares none. */
5008
+ const INVENTORY_DEFAULT_SCHEDULE_FREQUENCY = "Weekly";
5009
+ /**
5010
+ * The `InventoryConfigurations[]` ITEM cdkd actually SENDS for `item` — the ONE
5011
+ * helper behind both halves of the #1633 pair, per
5012
+ * `.claude/rules/providers.md`'s "share ONE helper with the provisioning path
5013
+ * rather than re-deriving the rule".
5014
+ *
5015
+ * Called from `applyInventoryConfigurations` with the values that went on the
5016
+ * wire (`sent`), and from
5017
+ * {@link S3BucketProvider.canonicalizeDesiredProperties} with none — where the
5018
+ * declared value IS what would be sent for every item the appliers do not warn
5019
+ * about, which is the whole population the twin exists for. On a MALFORMED value
5020
+ * the two deliberately disagree (the applier records the substituted value, the
5021
+ * fold keeps the declared one), so `cdkd diff` keeps reporting the field until
5022
+ * the template is corrected — the #1670 finding-3 outcome, preserved rather than
5023
+ * concealed.
5024
+ *
5025
+ * Three folds, all of them the never-emitted-KEY class (issue #1686) rather than
5026
+ * a narrowing:
5027
+ *
5028
+ * - `Schedule: { Frequency }` -> `ScheduleFrequency`. cdkd accepts the SDK
5029
+ * spelling (the #1605 fall-through) while `inventorySdkToCfn` emits only the
5030
+ * CFn one, which the registry schema declares REQUIRED and whose `Schedule`
5031
+ * sibling it does not declare at all.
5032
+ * - `Destination` -> the flattened CFn block (see
5033
+ * {@link effectiveS3BucketDestination}).
5034
+ * - `Enabled` / `IncludedObjectVersions` DEFAULTS. Both are always sent
5035
+ * (`IsEnabled: … ?? true`, `readConfigString(…, 'All')`) and always read back,
5036
+ * so an item omitting them recorded one key FEWER than the readback produces
5037
+ * and the comparator saw a key-count mismatch on the whole array — the
5038
+ * defaulted-but-SENT class of issue #1670, one property over (issue #1718).
5039
+ *
5040
+ * COPYING rather than mutating is load-bearing, not hygiene: `item` is an
5041
+ * element of the caller's DESIRED property bag, which `DiffCalculator` and the
5042
+ * engine still read afterwards — and the diff is now one of the two callers, so
5043
+ * mutating in place would rewrite the template side of a comparison the other
5044
+ * caller is in the middle of.
5045
+ *
5046
+ * @returns `item` BY IDENTITY when every fold is a no-op, so an ordinary
5047
+ * fully-declared CFn template records and diffs byte-for-byte as before.
5048
+ */
5049
+ function effectiveInventoryItem(item, sent) {
5050
+ const patch = {};
5051
+ if (item["Enabled"] === void 0) patch["Enabled"] = INVENTORY_DEFAULT_ENABLED;
5052
+ if (item["IncludedObjectVersions"] === void 0) patch["IncludedObjectVersions"] = INVENTORY_DEFAULT_INCLUDED_OBJECT_VERSIONS;
5053
+ const declaredSchedule = item["Schedule"];
5054
+ const declaresScheduleFrequency = item["ScheduleFrequency"] !== void 0;
5055
+ const scheduleFrequencyUsable = declaresScheduleFrequency && configStringRefusal(item, "ScheduleFrequency", INVENTORY_DEFAULT_SCHEDULE_FREQUENCY, "AWS::S3::Bucket InventoryConfigurations[]") === void 0;
5056
+ const scheduleContainerUsable = configStringRefusal(declaredSchedule, "Frequency", INVENTORY_DEFAULT_SCHEDULE_FREQUENCY, "AWS::S3::Bucket InventoryConfigurations[].Schedule") === void 0;
5057
+ const canFoldSchedule = sent?.frequency !== void 0 || scheduleFrequencyUsable || !declaresScheduleFrequency && scheduleContainerUsable;
5058
+ if (canFoldSchedule) {
5059
+ const declaredFrequency = declaresScheduleFrequency ? item["ScheduleFrequency"] : isPlainObject$1(declaredSchedule) && declaredSchedule["Frequency"] !== void 0 ? declaredSchedule["Frequency"] : INVENTORY_DEFAULT_SCHEDULE_FREQUENCY;
5060
+ const frequency = sent?.frequency ?? declaredFrequency;
5061
+ if (!Object.is(item["ScheduleFrequency"], frequency)) patch["ScheduleFrequency"] = frequency;
5062
+ }
5063
+ const destination = effectiveS3BucketDestination(item["Destination"], sent?.format);
5064
+ if (destination !== item["Destination"]) patch["Destination"] = destination;
5065
+ const dropsSchedule = declaredSchedule !== void 0 && canFoldSchedule;
5066
+ if (!dropsSchedule && Object.keys(patch).length === 0) return item;
5067
+ const out = {
5068
+ ...item,
5069
+ ...patch
5070
+ };
5071
+ if (dropsSchedule) delete out["Schedule"];
5072
+ return out;
5073
+ }
5074
+ /**
5075
+ * The `AnalyticsConfigurations[]` ITEM twin of {@link effectiveInventoryItem}.
5076
+ *
5077
+ * Only the data-export block diverges: the destination takes the same flattening
5078
+ * (`analyticsSdkToCfn` emits the flattened CFn spelling too — note it spells the
5079
+ * account `BucketAccountId` on BOTH sides here, unlike the inventory reverse
5080
+ * mapper), and `OutputSchemaVersion` is a defaulted-but-SENT member of the same
5081
+ * class as inventory's `Enabled`.
5082
+ *
5083
+ * A non-object `StorageClassAnalysis` / `DataExport` is left ALONE rather than
5084
+ * rebuilt: those are exactly the shapes `requireConfigObject` refuses, so the
5085
+ * item is SKIPPED on the wire and folding it here would describe a call that
5086
+ * never went out.
5087
+ */
5088
+ function effectiveAnalyticsItem(item, sent) {
5089
+ const storageClassAnalysis = item["StorageClassAnalysis"];
5090
+ if (!isPlainObject$1(storageClassAnalysis)) return item;
5091
+ const dataExport = storageClassAnalysis["DataExport"];
5092
+ if (!isPlainObject$1(dataExport)) return item;
5093
+ const patch = {};
5094
+ const outputSchemaVersion = sent?.outputSchemaVersion ?? declaredOrDefault(dataExport, "OutputSchemaVersion", ANALYTICS_DEFAULT_OUTPUT_SCHEMA_VERSION);
5095
+ if (!Object.is(dataExport["OutputSchemaVersion"], outputSchemaVersion)) patch["OutputSchemaVersion"] = outputSchemaVersion;
5096
+ const destination = effectiveS3BucketDestination(dataExport["Destination"], sent?.format);
5097
+ if (destination !== dataExport["Destination"]) patch["Destination"] = destination;
5098
+ if (Object.keys(patch).length === 0) return item;
5099
+ return {
5100
+ ...item,
5101
+ StorageClassAnalysis: {
5102
+ ...storageClassAnalysis,
5103
+ DataExport: {
5104
+ ...dataExport,
5105
+ ...patch
5106
+ }
5107
+ }
5108
+ };
5109
+ }
5110
+ /** The `Status` an intelligent-tiering item is SENT with when it declares none. */
5111
+ const INTELLIGENT_TIERING_DEFAULT_STATUS = "Enabled";
5112
+ /**
5113
+ * The `IntelligentTieringConfigurations[]` ITEM twin of
5114
+ * {@link effectiveInventoryItem} — the third applier of the #1718 audit.
5115
+ *
5116
+ * It has no never-emitted KEY or SHAPE (its destination-less block is spelled
5117
+ * identically on both sides), so the ONLY fold is the defaulted-but-SENT
5118
+ * `Status`: the Put always carries one and `intelligentTieringSdkToCfn` always
5119
+ * reads it back, so an item omitting it recorded one key fewer than the readback
5120
+ * produces. There is no `sent` parameter because this applier has no
5121
+ * warn-and-SUBSTITUTE arm at all — a malformed `Status` SKIPS the item (issue
5122
+ * #1595: defaulting to `Enabled` would start moving objects into archive
5123
+ * tiers), so the only value the fold can ever supply is the create-path default.
5124
+ *
5125
+ * The fourth applier, metrics, needs no fold: it sends `Id` plus a `Filter`
5126
+ * derived from the declared predicates and defaults no scalar member.
5127
+ */
5128
+ function effectiveIntelligentTieringItem(item) {
5129
+ if (item["Status"] !== void 0) return item;
5130
+ return {
5131
+ ...item,
5132
+ Status: INTELLIGENT_TIERING_DEFAULT_STATUS
5133
+ };
5134
+ }
5135
+ /**
5136
+ * Apply a per-ITEM fold to one of the `Id`-keyed array properties on BOTH sides
5137
+ * of a diff — the `canonicalizeDesiredProperties` plumbing shared by the
5138
+ * inventory and analytics folds.
4898
5139
  *
4899
- * The value-side twin of {@link s3BucketDestinationPath}, which names the same
4900
- * branch for MESSAGES. Derived from the picked bag's IDENTITY rather than by
4901
- * re-running {@link isFlattenedDestination}: a third copy of that branch
4902
- * condition is exactly what the predicate's own header warns about, and identity
4903
- * cannot drift from the resolver at all.
5140
+ * Identity-returns at every level (untouched item -> untouched array ->
5141
+ * untouched bag), and leaves a non-array property or a non-object element ALONE
5142
+ * so a malformed template still reaches the provider's own refusal rather than
5143
+ * being reshaped into something that looks valid.
4904
5144
  */
4905
- function s3BucketDestinationSegments(declared, picked) {
4906
- return picked === declared ? [] : ["S3BucketDestination"];
5145
+ function canonicalizeItemList(properties, key, fold) {
5146
+ const list = properties[key];
5147
+ if (!Array.isArray(list)) return properties;
5148
+ let changed = false;
5149
+ const mapped = list.map((element) => {
5150
+ if (!isPlainObject$1(element)) return element;
5151
+ const next = fold(element);
5152
+ if (next !== element) changed = true;
5153
+ return next;
5154
+ });
5155
+ return changed ? {
5156
+ ...properties,
5157
+ [key]: mapped
5158
+ } : properties;
4907
5159
  }
4908
5160
  /**
4909
5161
  * Name a malformed value in a refusal message, the way `config-shape.ts`'s
@@ -5169,24 +5421,37 @@ var S3BucketProvider = class S3BucketProvider {
5169
5421
  * the declared value describes a configuration AWS does not hold, and
5170
5422
  * `readCurrentState` reads all of these back — the permanent phantom drift
5171
5423
  * `.claude/rules/providers.md` records for the #1633 warn-and-default arm.
5172
- * That file also carries the full rationale, including why these sites take
5173
- * NO `canonicalizeDesiredProperties` twin; do not re-derive it here.
5174
- *
5175
- * Three details are load-bearing:
5176
- *
5177
- * - `onSubstituted` receives the value this call RETURNS, not the `fallback`
5178
- * argument, so "what is recorded" and "what is sent" stay one expression.
5179
- * - `options` is forwarded to BOTH the probe and the read, so a site that
5180
- * ever needs `coerceNumber` cannot make the two disagree.
5181
- * - Without `options.onUnusable` there is no probe: the read still THROWS,
5182
- * and recording a value the provider merely failed to send is what the
5183
- * "already ANNOUNCED" condition on `effectiveProperties` forbids.
5184
- */
5185
- readSubstitutedConfigString(container, key, fallback, containerPath, options, onSubstituted) {
5186
- const refused = options.onUnusable !== void 0 && configStringRefusal(container, key, fallback, containerPath, options) !== void 0;
5187
- const value = readConfigString(container, key, fallback, containerPath, options);
5188
- if (refused) onSubstituted(value);
5189
- return value;
5424
+ *
5425
+ * These sites DO take a `canonicalizeDesiredProperties` twin as of issues
5426
+ * #1707 / #1717, and this paragraph said the opposite until then — it is
5427
+ * restated rather than deleted because the earlier answer was not wrong, it
5428
+ * was answering a different question. #1670 asked it about the SUBSTITUTED
5429
+ * VALUE and said no, because canonicalizing there would conceal a malformed
5430
+ * value whose warning is the user's only signal. The twin that shipped folds
5431
+ * the never-emitted KEY and SHAPE at the same sites, which have no fault to
5432
+ * conceal and emit no warning at all, and it is keyed off the DECLARED shape
5433
+ * so the substituted value stays visible exactly as before. See
5434
+ * {@link S3BucketProvider.canonicalizeDesiredProperties} and
5435
+ * {@link declaredOrDefault}.
5436
+ *
5437
+ * **It is now a verbatim alias of `readConfigString`**, and the bullets that
5438
+ * used to sit here described a probe that no longer exists the same stale
5439
+ * -rationale class this round fixed one paragraph up, so it is named rather
5440
+ * than quietly rewritten. The `onSubstituted` callback it used to take is GONE
5441
+ * (review of #1707): recording is the per-item fold's job now, and all three
5442
+ * call sites had been left passing a comment-only no-op, so the probe ran a
5443
+ * second `configStringRefusal` per read purely to invoke nothing.
5444
+ *
5445
+ * It is kept rather than inlined for ONE reason, which is worth stating so the
5446
+ * next reader can weigh it: the name marks these three reads as the
5447
+ * warn-and-SUBSTITUTE sites, distinguishing them at a glance from the
5448
+ * SKIP-guarded ones (`skipOnUnusableConfigString`) and the plain strict reads.
5449
+ * The behavior is `readConfigString`'s, unchanged — `options.onUnusable`
5450
+ * carries the WARNING the "already ANNOUNCED" condition on
5451
+ * `effectiveProperties` needs, and without it the read still THROWS.
5452
+ */
5453
+ readSubstitutedConfigString(container, key, fallback, containerPath, options) {
5454
+ return readConfigString(container, key, fallback, containerPath, options);
5190
5455
  }
5191
5456
  /**
5192
5457
  * Apply lifecycle configuration
@@ -5627,10 +5892,8 @@ var S3BucketProvider = class S3BucketProvider {
5627
5892
  for (const config of configs) {
5628
5893
  index += 1;
5629
5894
  const id = config["Id"];
5630
- let sentItem;
5631
- const recordSubstitution = (path, sent) => {
5632
- sentItem = S3BucketProvider.withDeepValue(sentItem ?? config, path, sent);
5633
- };
5895
+ let sentOutputSchemaVersion;
5896
+ let sentFormat;
5634
5897
  const rawStorageClassAnalysis = config["StorageClassAnalysis"];
5635
5898
  if (rawStorageClassAnalysis != null && requireConfigObject(rawStorageClassAnalysis, "AWS::S3::Bucket AnalyticsConfigurations[].StorageClassAnalysis", { ...onUnusable ? { onUnusable } : {} }) === void 0) {
5636
5899
  skipped.push(index);
@@ -5669,23 +5932,12 @@ var S3BucketProvider = class S3BucketProvider {
5669
5932
  continue;
5670
5933
  }
5671
5934
  const destPath = s3BucketDestinationPath(rawDest, analyticsDestPath);
5672
- const destSegments = s3Dest ? s3BucketDestinationSegments(rawDest, s3Dest) : [];
5673
5935
  analyticsConfig.StorageClassAnalysis = { DataExport: {
5674
- OutputSchemaVersion: this.readSubstitutedConfigString(dataExport, "OutputSchemaVersion", "V_1", "AWS::S3::Bucket AnalyticsConfigurations[].StorageClassAnalysis.DataExport", { ...onUnusable ? { onUnusable } : {} }, (sent) => recordSubstitution([
5675
- "StorageClassAnalysis",
5676
- "DataExport",
5677
- "OutputSchemaVersion"
5678
- ], sent)),
5936
+ OutputSchemaVersion: sentOutputSchemaVersion = this.readSubstitutedConfigString(dataExport, "OutputSchemaVersion", ANALYTICS_DEFAULT_OUTPUT_SCHEMA_VERSION, "AWS::S3::Bucket AnalyticsConfigurations[].StorageClassAnalysis.DataExport", { ...onUnusable ? { onUnusable } : {} }),
5679
5937
  Destination: s3Dest ? { S3BucketDestination: {
5680
5938
  Bucket: s3Dest["BucketArn"] ?? s3Dest["Bucket"],
5681
5939
  BucketAccountId: s3Dest["BucketAccountId"],
5682
- Format: this.readSubstitutedConfigString(s3Dest, "Format", "CSV", destPath, { ...onUnusable ? { onUnusable } : {} }, (sent) => recordSubstitution([
5683
- "StorageClassAnalysis",
5684
- "DataExport",
5685
- "Destination",
5686
- ...destSegments,
5687
- "Format"
5688
- ], sent)),
5940
+ Format: sentFormat = this.readSubstitutedConfigString(s3Dest, "Format", INVENTORY_DESTINATION_DEFAULT_FORMAT, destPath, { ...onUnusable ? { onUnusable } : {} }),
5689
5941
  Prefix: s3Dest["Prefix"]
5690
5942
  } } : void 0
5691
5943
  } };
@@ -5695,7 +5947,11 @@ var S3BucketProvider = class S3BucketProvider {
5695
5947
  Id: id,
5696
5948
  AnalyticsConfiguration: analyticsConfig
5697
5949
  }));
5698
- if (sentItem !== void 0) substituted.set(index, sentItem);
5950
+ const sentItem = effectiveAnalyticsItem(config, {
5951
+ ...sentOutputSchemaVersion !== void 0 ? { outputSchemaVersion: sentOutputSchemaVersion } : {},
5952
+ ...sentFormat !== void 0 ? { format: sentFormat } : {}
5953
+ });
5954
+ if (sentItem !== config) substituted.set(index, sentItem);
5699
5955
  }
5700
5956
  this.logger.debug(`Applied ${configs.length - skipped.length} analytics configuration(s) to bucket ${bucketName}`);
5701
5957
  return {
@@ -5712,11 +5968,18 @@ var S3BucketProvider = class S3BucketProvider {
5712
5968
  * @returns what happened to each item, as {@link PerItemApplyOutcome} — the
5713
5969
  * per-item unit of `effectiveProperties` (issue #1612). Its per-item
5714
5970
  * `Status` read takes the SKIP downgrade rather than a substitution (issue
5715
- * #1595 — defaulting to `Enabled` would start archiving objects), so the
5716
- * `substituted` map is always empty here.
5971
+ * #1595 — defaulting to `Enabled` would start archiving objects), so no
5972
+ * MALFORMED value is ever recorded here. The `substituted` map is still
5973
+ * populated for the defaulted-but-SENT arm (issue #1718): an item that
5974
+ * simply OMITS `Status` is sent `Enabled` and read back with it, so a record
5975
+ * without the key compares unequal to the readback for the whole array.
5976
+ * That is the audit #1718 asks for on the sibling appliers — `metrics` has
5977
+ * no defaulted scalar member at all (it sends only `Id` plus a `Filter`
5978
+ * derived from the declared predicates), so it needs no fold.
5717
5979
  */
5718
5980
  async applyIntelligentTieringConfigurations(bucketName, configs, onUnusable) {
5719
5981
  const skipped = [];
5982
+ const substituted = /* @__PURE__ */ new Map();
5720
5983
  let index = -1;
5721
5984
  for (const config of configs) {
5722
5985
  index += 1;
@@ -5752,11 +6015,13 @@ var S3BucketProvider = class S3BucketProvider {
5752
6015
  Id: id,
5753
6016
  IntelligentTieringConfiguration: itConfig
5754
6017
  }));
6018
+ const sentItem = effectiveIntelligentTieringItem(config);
6019
+ if (sentItem !== config) substituted.set(index, sentItem);
5755
6020
  }
5756
6021
  this.logger.debug(`Applied ${configs.length - skipped.length} intelligent tiering configuration(s) to bucket ${bucketName}`);
5757
6022
  return {
5758
6023
  skipped,
5759
- substituted: /* @__PURE__ */ new Map()
6024
+ substituted
5760
6025
  };
5761
6026
  }
5762
6027
  /**
@@ -5790,11 +6055,7 @@ var S3BucketProvider = class S3BucketProvider {
5790
6055
  continue;
5791
6056
  }
5792
6057
  const destPath = s3BucketDestinationPath(rawDest, inventoryDestPath);
5793
- const destSegments = s3Dest ? s3BucketDestinationSegments(rawDest, s3Dest) : [];
5794
- let sentItem;
5795
- const recordSubstitution = (path, sent) => {
5796
- sentItem = S3BucketProvider.withDeepValue(sentItem ?? config, path, sent);
5797
- };
6058
+ let sentFormat;
5798
6059
  const scheduleFrequencyRefusal = onUnusable ? configStringRefusal(config, "ScheduleFrequency", "Weekly", "AWS::S3::Bucket InventoryConfigurations[]") : void 0;
5799
6060
  if (scheduleFrequencyRefusal !== void 0 && config["Schedule"] == null) {
5800
6061
  onUnusable?.(`${scheduleFrequencyRefusal}. Skipping this inventory configuration: there is no Schedule.Frequency to fall back to, and defaulting to Weekly would silently re-cadence a live inventory report; the same value is REFUSED on a template-path create`);
@@ -5808,22 +6069,15 @@ var S3BucketProvider = class S3BucketProvider {
5808
6069
  continue;
5809
6070
  }
5810
6071
  const frequency = useScheduleFrequency ? requireConfigString(config["ScheduleFrequency"], "Weekly", "AWS::S3::Bucket InventoryConfigurations[].ScheduleFrequency") : readConfigString(config["Schedule"], "Frequency", "Weekly", "AWS::S3::Bucket InventoryConfigurations[].Schedule");
5811
- const declaresSdkSchedule = config["Schedule"] !== void 0;
5812
- if (scheduleFrequencyRefusal !== void 0 || declaresSdkSchedule) recordSubstitution(["ScheduleFrequency"], frequency);
5813
- if (declaresSdkSchedule) sentItem = S3BucketProvider.withoutKey(sentItem ?? config, "Schedule");
5814
6072
  const inventoryConfig = {
5815
6073
  Id: id,
5816
- IsEnabled: config["Enabled"] ?? true,
5817
- IncludedObjectVersions: readConfigString(config, "IncludedObjectVersions", "All", "AWS::S3::Bucket InventoryConfigurations[]"),
6074
+ IsEnabled: config["Enabled"] ?? INVENTORY_DEFAULT_ENABLED,
6075
+ IncludedObjectVersions: readConfigString(config, "IncludedObjectVersions", INVENTORY_DEFAULT_INCLUDED_OBJECT_VERSIONS, "AWS::S3::Bucket InventoryConfigurations[]"),
5818
6076
  Schedule: { Frequency: frequency },
5819
6077
  Destination: { S3BucketDestination: s3Dest ? {
5820
6078
  Bucket: s3Dest["BucketArn"] ?? s3Dest["Bucket"],
5821
6079
  AccountId: s3Dest["BucketAccountId"],
5822
- Format: this.readSubstitutedConfigString(s3Dest, "Format", "CSV", destPath, { ...onUnusable ? { onUnusable } : {} }, (sent) => recordSubstitution([
5823
- "Destination",
5824
- ...destSegments,
5825
- "Format"
5826
- ], sent)),
6080
+ Format: sentFormat = this.readSubstitutedConfigString(s3Dest, "Format", INVENTORY_DESTINATION_DEFAULT_FORMAT, destPath, { ...onUnusable ? { onUnusable } : {} }),
5827
6081
  Prefix: s3Dest["Prefix"]
5828
6082
  } : void 0 },
5829
6083
  OptionalFields: config["OptionalFields"],
@@ -5834,7 +6088,11 @@ var S3BucketProvider = class S3BucketProvider {
5834
6088
  Id: id,
5835
6089
  InventoryConfiguration: inventoryConfig
5836
6090
  }));
5837
- if (sentItem !== void 0) substituted.set(index, sentItem);
6091
+ const sentItem = effectiveInventoryItem(config, {
6092
+ frequency,
6093
+ ...sentFormat !== void 0 ? { format: sentFormat } : {}
6094
+ });
6095
+ if (sentItem !== config) substituted.set(index, sentItem);
5838
6096
  }
5839
6097
  this.logger.debug(`Applied ${configs.length - skipped.length} inventory configuration(s) to bucket ${bucketName}`);
5840
6098
  return {
@@ -6209,53 +6467,6 @@ var S3BucketProvider = class S3BucketProvider {
6209
6467
  return effective;
6210
6468
  }
6211
6469
  /**
6212
- * A COPY of `container` with `path` set to `value` (issue #1670).
6213
- *
6214
- * Copying rather than mutating is load-bearing, not hygiene: the container is
6215
- * an item of the caller's DESIRED property bag, which `DiffCalculator` and the
6216
- * engine still read afterwards — writing the substituted value into it would
6217
- * silently rewrite the template side of the next comparison, which is exactly
6218
- * the `canonicalizeDesiredProperties` behavior this fix must NOT have. Only
6219
- * the objects ALONG the path are cloned; every sibling subtree is shared,
6220
- * which is safe because nothing here mutates them.
6221
- *
6222
- * A non-object intermediate is rebuilt as an EMPTY block, which is not a
6223
- * defensive shrug but the accurate record: it is what `readConfigString`
6224
- * itself did on the way in — its container arm says "Treating the block as
6225
- * empty, so <path>.<key> takes its default" — so `{}` plus the substituted
6226
- * leaf IS what was sent. No call site can reach it today (every intermediate
6227
- * has already been validated by `requireConfigObject` /
6228
- * `resolveS3BucketDestination`, which SKIP the item on a malformed one).
6229
- */
6230
- static withDeepValue(container, path, value) {
6231
- const [head, ...rest] = path;
6232
- if (head === void 0) return container;
6233
- const child = container[head];
6234
- return {
6235
- ...container,
6236
- [head]: rest.length === 0 ? value : S3BucketProvider.withDeepValue(isPlainObject$1(child) ? child : {}, rest, value)
6237
- };
6238
- }
6239
- /**
6240
- * The REMOVAL twin of {@link withDeepValue}: a copy of `container` without
6241
- * `key`, sharing every surviving subtree and never mutating the input (same
6242
- * reason — the input is an item of the caller's DESIRED bag).
6243
- *
6244
- * A substitution answers "the value we sent differs from the declared one";
6245
- * this answers "the KEY we sent does not exist on the readback at all", which
6246
- * is the shape-mismatch half of the same phantom-drift class (issue #1686).
6247
- * Setting the key to `undefined` instead would NOT do: `deepEqual` is
6248
- * `JSON.stringify`, which drops an `undefined` member — but the state record
6249
- * is written from this object and a present-but-`undefined` key survives a
6250
- * `structuredClone`, so the two sides would disagree on key COUNT for any
6251
- * consumer that walks `Object.keys` (the `unionWalkObjects` drift path does).
6252
- */
6253
- static withoutKey(container, key) {
6254
- if (!(key in container)) return container;
6255
- const { [key]: _dropped, ...rest } = container;
6256
- return rest;
6257
- }
6258
- /**
6259
6470
  * The empty-collection placeholder arm of {@link applySubConfigDiffs}: the
6260
6471
  * Put is SKIPPED, so AWS still holds the previously-applied configuration and
6261
6472
  * the record must say so (issue #1671).
@@ -6316,6 +6527,51 @@ var S3BucketProvider = class S3BucketProvider {
6316
6527
  retainPrevious(key);
6317
6528
  }
6318
6529
  /**
6530
+ * The CREATE-path sibling of {@link emptyCollectionSkip} (issue #1718).
6531
+ *
6532
+ * `applyAllSubConfigsForCreate` carries the same empty-collection guard the
6533
+ * update path does and skipped in SILENCE, so a fresh bucket came up with a
6534
+ * declared lifecycle / CORS configuration that was never applied and nothing
6535
+ * said so. The template shape that motivated #1671 — a condition-pruned or
6536
+ * intrinsic-collapsed empty array — is equally reachable on a create, and the
6537
+ * live A/B recorded on {@link emptyCollectionSkip} measured CloudFormation
6538
+ * REFUSING it there too, so the skip stands and only the silence was the
6539
+ * defect.
6540
+ *
6541
+ * WHAT IT RECORDS is where the two paths differ, and the answer here is
6542
+ * NOTHING — deliberately, not by omission. The update path retains the
6543
+ * PREVIOUS value because AWS still holds it; on a create there is no previous
6544
+ * value, and the rule's replay-CREATE line ("DROP the key") would be the
6545
+ * obvious import. It is the wrong one at this site: `readLifecycle` /
6546
+ * `readCors` ALWAYS emit the empty placeholder for an unconfigured bucket
6547
+ * (`{Rules: []}` on `NoSuchLifecycleConfiguration`, `{CorsRules: []}` on an
6548
+ * absent CORS config), so the declared empty collection is ALREADY exactly
6549
+ * what `readCurrentState` returns — the two agree with no override at all,
6550
+ * while dropping the key would leave the template declaring one the record
6551
+ * does not and churn a no-op UPDATE on every deploy. The rule's drop answer is
6552
+ * scoped to a skip whose declared value AWS cannot report; this one it can.
6553
+ *
6554
+ * Known bound, stated because the guard is wider than the shape that was
6555
+ * measured. It fires for every DECLARED collection the apply gate rejects, not
6556
+ * only the `{Rules: []}` the live A/B covered:
6557
+ *
6558
+ * - a MALFORMED non-array collection (an unresolved intrinsic), where the
6559
+ * record keeps a value the readback can never match — narrowing the record
6560
+ * there is the #1612 malformed-value class rather than this one;
6561
+ * - an EMPTY block (`{}`), where the message is literally true (it declares no
6562
+ * rules) and nothing is lost.
6563
+ *
6564
+ * The wording says "declares no rules" rather than asserting an empty ARRAY
6565
+ * for exactly that reason — the same hedge {@link emptyCollectionSkip} carries
6566
+ * on the update path, whose guard is equally wide. The gate is `!= null` and
6567
+ * not `!== undefined` (review of #1718): a property explicitly set to `null`
6568
+ * declares no configuration to lose, so warning about it would be noise, and
6569
+ * `!= null` is the convention the rest of this file's container gates use.
6570
+ */
6571
+ announceCreateEmptyCollectionSkip(key, collectionPath) {
6572
+ this.logger.warn(`AWS::S3::Bucket ${collectionPath} declares no rules, which CloudFormation rejects outright (the collection is a required property, and an empty one is not a removal intent). Creating the bucket WITHOUT the declared ${key}; remove the whole ${key} property if that is what you meant, or check for a condition-pruned or intrinsic-collapsed rules array.`);
6573
+ }
6574
+ /**
6319
6575
  * Diff CFn-shape sub-config values between previous and new state.
6320
6576
  *
6321
6577
  * Four transitions:
@@ -6605,10 +6861,11 @@ var S3BucketProvider = class S3BucketProvider {
6605
6861
  if (notifConfig) await this.applyNotificationConfiguration(bucketName, notifConfig);
6606
6862
  const corsConfig = properties["CorsConfiguration"];
6607
6863
  if (corsConfig?.CorsRules && Array.isArray(corsConfig.CorsRules) && corsConfig.CorsRules.length > 0) await this.applyCorsConfiguration(bucketName, corsConfig);
6864
+ else if (corsConfig != null) this.announceCreateEmptyCollectionSkip("CorsConfiguration", "CorsConfiguration.CorsRules");
6608
6865
  const lifecycleConfig = properties["LifecycleConfiguration"];
6609
6866
  if (lifecycleConfig?.Rules && Array.isArray(lifecycleConfig.Rules) && lifecycleConfig.Rules.length > 0) {
6610
6867
  if (!await this.applyLifecycleConfiguration(bucketName, lifecycleConfig, replayOnUnusable)) overrides.set("LifecycleConfiguration", void 0);
6611
- }
6868
+ } else if (lifecycleConfig != null) this.announceCreateEmptyCollectionSkip("LifecycleConfiguration", "LifecycleConfiguration.Rules");
6612
6869
  const loggingConfig = properties["LoggingConfiguration"];
6613
6870
  const loggingRefusal = replayOnUnusable ? configStringRefusal(loggingConfig, "DestinationBucketName", "", "AWS::S3::Bucket LoggingConfiguration") : void 0;
6614
6871
  if (loggingRefusal !== void 0) {
@@ -6742,6 +6999,61 @@ var S3BucketProvider = class S3BucketProvider {
6742
6999
  }
6743
7000
  }
6744
7001
  /**
7002
+ * The `effectiveProperties` TWIN required by `.claude/rules/providers.md`
7003
+ * ("`effectiveProperties` alone BREAKS the next deploy") for the per-item
7004
+ * folds the inventory and analytics appliers record — issue #1717 for the
7005
+ * inventory `Schedule` -> `ScheduleFrequency` half (#1686), issue #1707 for
7006
+ * the destination-shape half, issue #1718 for the defaulted-but-SENT members.
7007
+ *
7008
+ * Applied by `DiffCalculator` to BOTH comparison sides through
7009
+ * `makeCanonicalizePropertiesFn`, so the record's folded spelling and the
7010
+ * template's declared one compare EQUAL and an unchanged template stops
7011
+ * redeploying as `1 to update` forever. Folding both sides is also what heals
7012
+ * the already-deployed population: an item whose value never changes is never
7013
+ * re-Put, so the recording side alone can never reach a record written before
7014
+ * the fold.
7015
+ *
7016
+ * The rule's OTHER requirement — share ONE helper with the provisioning path
7017
+ * rather than re-deriving the rule — is met by
7018
+ * {@link effectiveInventoryItem} / {@link effectiveAnalyticsItem}, which the
7019
+ * appliers call with the values they actually sent and this calls with none.
7020
+ *
7021
+ * Note the twin question is re-asked per site rather than inherited: the #1670
7022
+ * sites next door answered NO because canonicalizing there would CONCEAL a
7023
+ * malformed value whose warning tells the user what to fix. These folds have
7024
+ * no fault to conceal — an item declaring only the SDK spelling is perfectly
7025
+ * valid input that emits no warning at all — so the twin's absence bought
7026
+ * nothing and cost a permanent `cdkd diff` line plus a redundant per-`Id` Put
7027
+ * on every deploy.
7028
+ *
7029
+ * Identity-returns an untouched bag, so every other `AWS::S3::Bucket` template
7030
+ * and every other resource type is byte-for-byte unaffected.
7031
+ *
7032
+ * THE #1717 OBSTACLE DID NOT REPRODUCE, and that is worth stating rather than
7033
+ * quietly omitting: the issue measured a `canonicalizeDesiredProperties`
7034
+ * folding this key making `gen-nested-key-coverage` report the still-correct
7035
+ * plural->singular `segmentRenames` entry for `InventoryConfigurations` STALE,
7036
+ * and warned that each piece is inert alone so only the combination trips it.
7037
+ * With the fold implemented as a MODULE-level pure function over an item
7038
+ * parameter, the shipped critic is green end to end — `vp run
7039
+ * audit:nested-key-coverage:check` reports 0 divergences in all three passes,
7040
+ * the committed matrix does not drift by a byte, and
7041
+ * `InventoryConfigurations` is still listed in the target's
7042
+ * `usedSegmentRenames`, i.e. the entry is doing work and the fence is not
7043
+ * masking anything. So no `segmentRenames` entry was removed and no staleness
7044
+ * check was widened; the issue's "resolve the interaction WITHOUT deleting the
7045
+ * entry" requirement is met by there being no interaction to resolve. What is
7046
+ * NOT claimed is a mechanism for why the issue's attempt tripped it — that
7047
+ * attempt is not in the tree to bisect against.
7048
+ */
7049
+ canonicalizeDesiredProperties(resourceType, properties) {
7050
+ if (resourceType !== "AWS::S3::Bucket") return properties;
7051
+ let out = canonicalizeItemList(properties, "InventoryConfigurations", (item) => effectiveInventoryItem(item));
7052
+ out = canonicalizeItemList(out, "AnalyticsConfigurations", (item) => effectiveAnalyticsItem(item));
7053
+ out = canonicalizeItemList(out, "IntelligentTieringConfigurations", effectiveIntelligentTieringItem);
7054
+ return out;
7055
+ }
7056
+ /**
6745
7057
  * Read the AWS-current S3 bucket configuration in CFn-property shape.
6746
7058
  *
6747
7059
  * Issues a small handful of independent S3 GET calls and stitches them
@@ -11882,6 +12194,43 @@ function packCompositeId(resourceType, logicalId, segments, options) {
11882
12194
  else throw new ProvisioningError(refusal, resourceType, logicalId);
11883
12195
  return segments.map((segment) => String(segment.value)).join("|");
11884
12196
  }
12197
+ /** Render the expected id shape, e.g. `<databaseName>|<tableName>`. */
12198
+ function formatShape(segments) {
12199
+ return segments.map((segment) => `<${segment}>`).join("|");
12200
+ }
12201
+ /**
12202
+ * The message EVERY malformed-composite-id decode site emits (issue
12203
+ * [#1657](https://github.com/go-to-k/cdkd/issues/1657)).
12204
+ *
12205
+ * ## Why one builder
12206
+ *
12207
+ * Roughly half the decode sites stated the expected shape and half only echoed
12208
+ * the value back, and the format is documented nowhere else — so on the silent
12209
+ * half the message was the user's only possible route to the answer and it did
12210
+ * not carry it. Routing the sites that already got it right through the same
12211
+ * builder is what stops them drifting back.
12212
+ *
12213
+ * ## This is a WORDING fix, not an acceptance change
12214
+ *
12215
+ * Each caller keeps its own split-and-guard exactly as it was. The arities
12216
+ * genuinely differ (some sites accept AT LEAST N segments, some destructure the
12217
+ * first N and tolerate extras — the #1672 ambiguity, tracked separately), and
12218
+ * folding them into one predicate here would silently change which ids deploy.
12219
+ *
12220
+ * ## The `skipping` variant
12221
+ *
12222
+ * On the DELETE path the established policy is warn-and-continue, so a
12223
+ * malformed record leaves the AWS resource ALIVE while `cdkd destroy` reports
12224
+ * success. That policy is not changed here — but the warning now says what the
12225
+ * skip costs, because an orphan the user is never told about is one they cannot
12226
+ * go and delete.
12227
+ */
12228
+ function compositeIdFormatMessage(format, logicalId, physicalId, options) {
12229
+ const accepted = format.alsoAccepts ? `"${formatShape(format.segments)}" or ${format.alsoAccepts}` : `"${formatShape(format.segments)}"`;
12230
+ const head = `Invalid physicalId format for ${format.label} ${logicalId}: expected ${accepted}, got "${physicalId}"`;
12231
+ if (!options?.skipping) return head;
12232
+ return `${head}. Skipping the delete — the AWS resource is LEFT IN PLACE and cdkd will report this delete as successful, so delete it by hand (or repair the id in state.json and re-run) if it still exists. NOTE this arm is reached from cdkd destroy AND from the deploy engine's replacement / rollback deletes.`;
12233
+ }
11885
12234
 
11886
12235
  //#endregion
11887
12236
  //#region src/provisioning/providers/lambda-event-invoke-config-provider.ts
@@ -18888,6 +19237,32 @@ function canonicalizeIpProtocolValue(value) {
18888
19237
 
18889
19238
  //#endregion
18890
19239
  //#region src/provisioning/providers/ec2-provider.ts
19240
+ /** Shapes of the four `AWS::EC2::*` composite physicalIds (issue #1657). */
19241
+ const EC2_VPC_GATEWAY_ATTACHMENT_ID_FORMAT = {
19242
+ label: "VPCGatewayAttachment",
19243
+ segments: ["internetGatewayId", "vpcId"]
19244
+ };
19245
+ const EC2_ROUTE_ID_FORMAT = {
19246
+ label: "Route",
19247
+ segments: ["routeTableId", "destination"]
19248
+ };
19249
+ const EC2_SG_INGRESS_ID_FORMAT = {
19250
+ label: "SecurityGroupIngress",
19251
+ segments: [
19252
+ "groupId",
19253
+ "ipProtocol",
19254
+ "fromPort",
19255
+ "toPort"
19256
+ ]
19257
+ };
19258
+ const EC2_NETWORK_ACL_ENTRY_ID_FORMAT = {
19259
+ label: "NetworkAclEntry",
19260
+ segments: [
19261
+ "networkAclId",
19262
+ "ruleNumber",
19263
+ "egress"
19264
+ ]
19265
+ };
18891
19266
  /**
18892
19267
  * The `AWS::EC2::Route` destination keys, in CloudFormation's own precedence
18893
19268
  * order — which is also the order `createRoute`'s
@@ -19780,7 +20155,7 @@ var EC2Provider = class {
19780
20155
  async deleteVpcGatewayAttachment(logicalId, physicalId, resourceType, context) {
19781
20156
  this.logger.debug(`Deleting VPCGatewayAttachment ${logicalId}: ${physicalId}`);
19782
20157
  const parts = physicalId.split("|");
19783
- if (parts.length !== 2) throw new ProvisioningError(`Invalid physicalId format for VPCGatewayAttachment ${logicalId}: expected "IGW|VpcId", got "${physicalId}"`, resourceType, logicalId, physicalId);
20158
+ if (parts.length !== 2) throw new ProvisioningError(compositeIdFormatMessage(EC2_VPC_GATEWAY_ATTACHMENT_ID_FORMAT, logicalId, physicalId), resourceType, logicalId, physicalId);
19784
20159
  const [internetGatewayId, vpcId] = parts;
19785
20160
  try {
19786
20161
  await this.withDependencyViolationRetry(() => this.ec2Client.send(new DetachInternetGatewayCommand({
@@ -19973,7 +20348,7 @@ var EC2Provider = class {
19973
20348
  async deleteRoute(logicalId, physicalId, resourceType, context) {
19974
20349
  this.logger.debug(`Deleting Route ${logicalId}: ${physicalId}`);
19975
20350
  const parts = physicalId.split("|");
19976
- if (parts.length !== 2) throw new ProvisioningError(`Invalid physicalId format for Route ${logicalId}: expected "RouteTableId|Destination", got "${physicalId}"`, resourceType, logicalId, physicalId);
20351
+ if (parts.length !== 2) throw new ProvisioningError(compositeIdFormatMessage(EC2_ROUTE_ID_FORMAT, logicalId, physicalId), resourceType, logicalId, physicalId);
19977
20352
  const [routeTableId, destination] = parts;
19978
20353
  try {
19979
20354
  const destinationParam = destination?.includes(":") ? { DestinationIpv6CidrBlock: destination } : destination?.startsWith("pl-") ? { DestinationPrefixListId: destination } : { DestinationCidrBlock: destination };
@@ -20317,7 +20692,7 @@ var EC2Provider = class {
20317
20692
  async deleteSecurityGroupIngress(logicalId, physicalId, resourceType, properties, context) {
20318
20693
  this.logger.debug(`Deleting SecurityGroupIngress ${logicalId}: ${physicalId}`);
20319
20694
  const parts = physicalId.split("|");
20320
- if (parts.length !== 4) throw new ProvisioningError(`Invalid physicalId format for SecurityGroupIngress ${logicalId}: expected "GroupId|Protocol|FromPort|ToPort", got "${physicalId}"`, resourceType, logicalId, physicalId);
20695
+ if (parts.length !== 4) throw new ProvisioningError(compositeIdFormatMessage(EC2_SG_INGRESS_ID_FORMAT, logicalId, physicalId), resourceType, logicalId, physicalId);
20321
20696
  const [groupId, ipProtocol, fromPortStr, toPortStr] = parts;
20322
20697
  const ipPermission = properties ? this.buildIpPermission(properties) : {
20323
20698
  IpProtocol: ipProtocol,
@@ -21007,7 +21382,7 @@ var EC2Provider = class {
21007
21382
  this.logger.debug(`Deleting NetworkAclEntry ${logicalId}: ${physicalId}`);
21008
21383
  const parts = physicalId.split("|");
21009
21384
  if (parts.length < 3) {
21010
- this.logger.warn(`Invalid NetworkAclEntry physical ID format: ${physicalId}, skipping`);
21385
+ this.logger.warn(compositeIdFormatMessage(EC2_NETWORK_ACL_ENTRY_ID_FORMAT, logicalId, physicalId, { skipping: true }));
21011
21386
  return;
21012
21387
  }
21013
21388
  const networkAclId = parts[0];
@@ -21317,7 +21692,7 @@ var EC2Provider = class {
21317
21692
  * Skipped (return `undefined`, falls through to the comparator's
21318
21693
  * "unsupported" outcome):
21319
21694
  * - **AWS::EC2::VPCGatewayAttachment**: physical id is
21320
- * `IGW|VpcId`. The two ids are immutable inputs to the SDK call;
21695
+ * `<internetGatewayId>|<vpcId>`. The two ids are immutable inputs to the SDK call;
21321
21696
  * drift detection on this resource has no useful signal beyond
21322
21697
  * existence verification (which the user can do via the parent IGW
21323
21698
  * / VPC drift report).
@@ -22009,6 +22384,15 @@ function isDefaultEgressRule(rule) {
22009
22384
 
22010
22385
  //#endregion
22011
22386
  //#region src/provisioning/providers/apigateway-provider.ts
22387
+ /** Shape of an `AWS::ApiGateway::Method` physicalId (issue #1657). */
22388
+ const APIGW_METHOD_ID_FORMAT = {
22389
+ label: "API Gateway Method",
22390
+ segments: [
22391
+ "restApiId",
22392
+ "resourceId",
22393
+ "httpMethod"
22394
+ ]
22395
+ };
22012
22396
  /**
22013
22397
  * AWS API Gateway Provider
22014
22398
  *
@@ -22994,7 +23378,7 @@ var ApiGatewayProvider = class ApiGatewayProvider {
22994
23378
  async updateMethod(logicalId, physicalId, resourceType, properties, previousProperties) {
22995
23379
  this.logger.debug(`Updating API Gateway Method ${logicalId}: ${physicalId}`);
22996
23380
  const parts = physicalId.split("|");
22997
- if (parts.length !== 3) throw new ProvisioningError(`Invalid physicalId format for API Gateway Method ${logicalId}: expected "restApiId|resourceId|httpMethod", got "${physicalId}"`, resourceType, logicalId, physicalId);
23381
+ if (parts.length !== 3) throw new ProvisioningError(compositeIdFormatMessage(APIGW_METHOD_ID_FORMAT, logicalId, physicalId), resourceType, logicalId, physicalId);
22998
23382
  const [restApiId, resourceId, httpMethod] = parts;
22999
23383
  const patchOperations = [];
23000
23384
  for (const { key, path } of [
@@ -23068,7 +23452,7 @@ var ApiGatewayProvider = class ApiGatewayProvider {
23068
23452
  async deleteMethod(logicalId, physicalId, resourceType, context) {
23069
23453
  this.logger.debug(`Deleting API Gateway Method ${logicalId}: ${physicalId}`);
23070
23454
  const parts = physicalId.split("|");
23071
- if (parts.length !== 3) throw new ProvisioningError(`Invalid physicalId format for API Gateway Method ${logicalId}: expected "restApiId|resourceId|httpMethod", got "${physicalId}"`, resourceType, logicalId, physicalId);
23455
+ if (parts.length !== 3) throw new ProvisioningError(compositeIdFormatMessage(APIGW_METHOD_ID_FORMAT, logicalId, physicalId), resourceType, logicalId, physicalId);
23072
23456
  const [restApiId, resourceId, httpMethod] = parts;
23073
23457
  try {
23074
23458
  await this.apiGatewayClient.send(new DeleteMethodCommand({
@@ -36869,6 +37253,23 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
36869
37253
 
36870
37254
  //#endregion
36871
37255
  //#region src/provisioning/providers/appsync-provider.ts
37256
+ /** Shapes of the three `AWS::AppSync::*` child composite physicalIds (issue #1657). */
37257
+ const APPSYNC_DATASOURCE_ID_FORMAT = {
37258
+ label: "AppSync DataSource",
37259
+ segments: ["apiId", "name"]
37260
+ };
37261
+ const APPSYNC_RESOLVER_ID_FORMAT = {
37262
+ label: "AppSync Resolver",
37263
+ segments: [
37264
+ "apiId",
37265
+ "typeName",
37266
+ "fieldName"
37267
+ ]
37268
+ };
37269
+ const APPSYNC_APIKEY_ID_FORMAT = {
37270
+ label: "AppSync ApiKey",
37271
+ segments: ["apiId", "apiKeyId"]
37272
+ };
36872
37273
  const MUTABLE_GRAPHQL_API_CONFIG_PROPERTIES = [
36873
37274
  "UserPoolConfig",
36874
37275
  "OpenIDConnectConfig",
@@ -37339,7 +37740,7 @@ var AppSyncProvider = class {
37339
37740
  if (next !== void 0 && prev !== void 0 && next !== prev) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS AppSync DataSource.${field} is immutable — destroy + redeploy to change`);
37340
37741
  }
37341
37742
  const [apiId, name] = physicalId.split("|");
37342
- if (!apiId || !name) throw new ProvisioningError(`Invalid DataSource physical ID format: ${physicalId}`, resourceType, logicalId, physicalId);
37743
+ if (!apiId || !name) throw new ProvisioningError(compositeIdFormatMessage(APPSYNC_DATASOURCE_ID_FORMAT, logicalId, physicalId), resourceType, logicalId, physicalId);
37343
37744
  const type = properties["Type"] ?? previousProperties["Type"];
37344
37745
  if (![
37345
37746
  "Description",
@@ -37377,7 +37778,7 @@ var AppSyncProvider = class {
37377
37778
  if (next !== void 0 && prev !== void 0 && next !== prev) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS AppSync Resolver.${field} is immutable — destroy + redeploy to change`);
37378
37779
  }
37379
37780
  const parts = physicalId.split("|");
37380
- if (parts.length < 3) throw new ProvisioningError(`Invalid Resolver physical ID format: ${physicalId}`, resourceType, logicalId, physicalId);
37781
+ if (parts.length < 3) throw new ProvisioningError(compositeIdFormatMessage(APPSYNC_RESOLVER_ID_FORMAT, logicalId, physicalId), resourceType, logicalId, physicalId);
37381
37782
  const [apiId, typeName, fieldName] = parts;
37382
37783
  if (![
37383
37784
  "DataSourceName",
@@ -37411,7 +37812,7 @@ var AppSyncProvider = class {
37411
37812
  async updateApiKey(logicalId, physicalId, resourceType, properties, previousProperties) {
37412
37813
  if (properties["ApiId"] !== void 0 && previousProperties["ApiId"] !== void 0 && properties["ApiId"] !== previousProperties["ApiId"]) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, "AWS AppSync ApiKey.ApiId is immutable — destroy + redeploy to change");
37413
37814
  const [apiId, apiKeyId] = physicalId.split("|");
37414
- if (!apiId || !apiKeyId) throw new ProvisioningError(`Invalid ApiKey physical ID format: ${physicalId}`, resourceType, logicalId, physicalId);
37815
+ if (!apiId || !apiKeyId) throw new ProvisioningError(compositeIdFormatMessage(APPSYNC_APIKEY_ID_FORMAT, logicalId, physicalId), resourceType, logicalId, physicalId);
37415
37816
  const newDesc = properties["Description"];
37416
37817
  const oldDesc = previousProperties["Description"];
37417
37818
  const newExp = properties["Expires"];
@@ -38117,7 +38518,7 @@ var AppSyncProvider = class {
38117
38518
  this.logger.debug(`Deleting DataSource ${logicalId}: ${physicalId}`);
38118
38519
  const [apiId, name] = physicalId.split("|");
38119
38520
  if (!apiId || !name) {
38120
- this.logger.warn(`Invalid DataSource physical ID format: ${physicalId}, skipping`);
38521
+ this.logger.warn(compositeIdFormatMessage(APPSYNC_DATASOURCE_ID_FORMAT, logicalId, physicalId, { skipping: true }));
38121
38522
  return;
38122
38523
  }
38123
38524
  try {
@@ -38180,7 +38581,7 @@ var AppSyncProvider = class {
38180
38581
  this.logger.debug(`Deleting Resolver ${logicalId}: ${physicalId}`);
38181
38582
  const parts = physicalId.split("|");
38182
38583
  if (parts.length < 3) {
38183
- this.logger.warn(`Invalid Resolver physical ID format: ${physicalId}, skipping`);
38584
+ this.logger.warn(compositeIdFormatMessage(APPSYNC_RESOLVER_ID_FORMAT, logicalId, physicalId, { skipping: true }));
38184
38585
  return;
38185
38586
  }
38186
38587
  const [apiId, typeName, fieldName] = parts;
@@ -38236,7 +38637,7 @@ var AppSyncProvider = class {
38236
38637
  this.logger.debug(`Deleting ApiKey ${logicalId}: ${physicalId}`);
38237
38638
  const [apiId, apiKeyId] = physicalId.split("|");
38238
38639
  if (!apiId || !apiKeyId) {
38239
- this.logger.warn(`Invalid ApiKey physical ID format: ${physicalId}, skipping`);
38640
+ this.logger.warn(compositeIdFormatMessage(APPSYNC_APIKEY_ID_FORMAT, logicalId, physicalId, { skipping: true }));
38240
38641
  return;
38241
38642
  }
38242
38643
  try {
@@ -38623,6 +39024,11 @@ var AppSyncProvider = class {
38623
39024
 
38624
39025
  //#endregion
38625
39026
  //#region src/provisioning/providers/glue-provider.ts
39027
+ /** Shape of an `AWS::Glue::Table` physicalId, for every decode site (issue #1657). */
39028
+ const GLUE_TABLE_ID_FORMAT = {
39029
+ label: "Glue Table",
39030
+ segments: ["databaseName", "tableName"]
39031
+ };
38626
39032
  /**
38627
39033
  * Read a template-borne value that is about to be forwarded to a Glue read
38628
39034
  * API as a string.
@@ -38920,7 +39326,7 @@ var GlueProvider = class {
38920
39326
  async updateTable(logicalId, physicalId, resourceType, properties, previousProperties) {
38921
39327
  this.logger.debug(`Updating Glue Table ${logicalId}: ${physicalId}`);
38922
39328
  const [databaseName, tableName] = physicalId.split("|");
38923
- if (!databaseName || !tableName) throw new ProvisioningError(`Invalid Glue Table physical ID format: ${physicalId}`, resourceType, logicalId, physicalId);
39329
+ if (!databaseName || !tableName) throw new ProvisioningError(compositeIdFormatMessage(GLUE_TABLE_ID_FORMAT, logicalId, physicalId), resourceType, logicalId, physicalId);
38924
39330
  const tableInput = properties["TableInput"];
38925
39331
  if (!tableInput) throw new ProvisioningError(`TableInput is required for Glue Table update ${logicalId}`, resourceType, logicalId, physicalId);
38926
39332
  const catalogId = properties["CatalogId"];
@@ -38957,7 +39363,7 @@ var GlueProvider = class {
38957
39363
  this.logger.debug(`Deleting Glue Table ${logicalId}: ${physicalId}`);
38958
39364
  const [databaseName, tableName] = physicalId.split("|");
38959
39365
  if (!databaseName || !tableName) {
38960
- this.logger.warn(`Invalid Glue Table physical ID format: ${physicalId}, skipping`);
39366
+ this.logger.warn(compositeIdFormatMessage(GLUE_TABLE_ID_FORMAT, logicalId, physicalId, { skipping: true }));
38961
39367
  return;
38962
39368
  }
38963
39369
  const target = deleteCatalogId(properties);
@@ -49920,6 +50326,20 @@ var S3DirectoryBucketProvider = class {
49920
50326
 
49921
50327
  //#endregion
49922
50328
  //#region src/provisioning/providers/s3-tables-provider.ts
50329
+ /** Shapes of the two `AWS::S3Tables::*` composite physicalIds (issue #1657). */
50330
+ const S3_TABLES_NAMESPACE_ID_FORMAT = {
50331
+ label: "S3 Tables Namespace",
50332
+ segments: ["tableBucketARN", "namespace"]
50333
+ };
50334
+ const S3_TABLES_TABLE_ID_FORMAT = {
50335
+ label: "S3 Tables Table",
50336
+ segments: [
50337
+ "tableBucketARN",
50338
+ "namespace",
50339
+ "name"
50340
+ ],
50341
+ alsoAccepts: "a table ARN"
50342
+ };
49923
50343
  /**
49924
50344
  * Parse cdkd's composite namespace physicalId `<tableBucketARN>|<namespace>`.
49925
50345
  *
@@ -50166,7 +50586,7 @@ var S3TablesProvider = class {
50166
50586
  async deleteNamespace(logicalId, physicalId, resourceType, context) {
50167
50587
  this.logger.debug(`Deleting S3 Tables Namespace ${logicalId}: ${physicalId}`);
50168
50588
  const [tableBucketARN, namespaceName] = physicalId.split("|");
50169
- if (!tableBucketARN || !namespaceName) throw new ProvisioningError(`Invalid physical ID format for S3 Tables Namespace ${logicalId}: ${physicalId}`, resourceType, logicalId, physicalId);
50589
+ if (!tableBucketARN || !namespaceName) throw new ProvisioningError(compositeIdFormatMessage(S3_TABLES_NAMESPACE_ID_FORMAT, logicalId, physicalId), resourceType, logicalId, physicalId);
50170
50590
  try {
50171
50591
  await this.getClient().send(new DeleteNamespaceCommand$1({
50172
50592
  tableBucketARN,
@@ -50580,7 +51000,7 @@ var S3TablesProvider = class {
50580
51000
  this.logger.debug(`S3 Tables Table ${physicalId} does not exist, skipping deletion`);
50581
51001
  return;
50582
51002
  }
50583
- if (resolved === "unparseable") throw new ProvisioningError(`Invalid physical ID format for S3 Tables Table ${logicalId}: ${physicalId} (expected '<tableBucketARN>|<namespace>|<name>' or a table ARN)`, resourceType, logicalId, physicalId);
51003
+ if (resolved === "unparseable") throw new ProvisioningError(compositeIdFormatMessage(S3_TABLES_TABLE_ID_FORMAT, logicalId, physicalId), resourceType, logicalId, physicalId);
50584
51004
  const { tableBucketARN, namespace, name } = resolved;
50585
51005
  try {
50586
51006
  await this.getClient().send(new DeleteTableCommand$2({
@@ -50701,7 +51121,7 @@ var S3TablesProvider = class {
50701
51121
  if (removedKeys.length === 0 && Object.keys(upserts).length === 0) return;
50702
51122
  const resolvedParts = await this.resolveTableParts(physicalId);
50703
51123
  if (resolvedParts === "not-found") throw new ProvisioningError(`applyTableTagsDiff: table '${physicalId}' no longer exists (state is out of sync) — refusing to silently drop the tag update`, resourceType, logicalId, physicalId);
50704
- if (resolvedParts === "unparseable") throw new ProvisioningError(`applyTableTagsDiff: cannot derive table ARN from physicalId '${physicalId}' (expected '<bucketArn>|<namespace>|<name>' or a table ARN) refusing to silently drop the tag update`, resourceType, logicalId, physicalId);
51124
+ if (resolvedParts === "unparseable") throw new ProvisioningError(`applyTableTagsDiff: ${compositeIdFormatMessage(S3_TABLES_TABLE_ID_FORMAT, logicalId, physicalId)}. Cannot derive the table ARN for the tag update, and refusing to silently drop it.`, resourceType, logicalId, physicalId);
50705
51125
  const { tableBucketARN, namespace, name } = resolvedParts;
50706
51126
  const resourceArn = await this.lookupTableArn(tableBucketARN, namespace, name);
50707
51127
  if (!resourceArn) throw new ProvisioningError(`applyTableTagsDiff: GetTable returned no tableARN for ${physicalId} — table is gone or state is out-of-sync. Refusing to silently drop the tag update (run 'cdkd state orphan ${logicalId}' to clean up if the table was deleted out-of-band).`, resourceType, logicalId, physicalId);
@@ -73545,7 +73965,7 @@ function createMigrateCommand() {
73545
73965
  */
73546
73966
  function buildProgram() {
73547
73967
  const program = new Command();
73548
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.14");
73968
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.16");
73549
73969
  program.hook("preAction", (_thisCommand, actionCommand) => {
73550
73970
  const { profile } = actionCommand.optsWithGlobals();
73551
73971
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;