@go-to-k/cdkd 0.281.13 → 0.281.15

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 uploadCfnTemplate, A as green, An as normalizeAwsError, At as formatDockerLoginError, B as slowCcOperationTimeoutMs, Bt as resolveApp, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as ResourceUpdateNotSupportedError, Ct as BOOTSTRAP_MARKER_PREFIX, D as bold, Dt as validateAssetBucketName, E as formatResourceLine, Et as parseBootstrapMarker, F as clearOnUpdateRemoval, Ft as getDockerImageBySourceHash, G as getAccountInfo, Gt as resolveStateBucketWithDefaultAndSource, H as isTerminationProtectionPropagationError, Ht as resolveCaptureObservedState, I as ProviderRegistry, It as Synthesizer, J as normalizeAwsTagsToCfn, Jt as warnDeprecatedNoPrefixCliFlag, K as refStateLookupFromResource, Kt as resolveUseCdkBootstrapAssets, L as findActionableSilentDrops, Lt as synthesisStatusMessage, M as yellow, Mt as runDockerForeground, N as IAMRoleProvider, Nt as runDockerStreaming, O as cyan, Ot as validateContainerRepoName, P as collectInlinePolicyNamesManagedBySiblings, Pt as AssetManifestLoader, Q as readConfigString, Qt as findLargeInlineResources, R as findSilentDropProperties, Rt as getDefaultStateBucketName, S as extractDeploymentEventError, Sn as ResourceTimeoutError, St as AssetModeResolver, T as renderStatefulReason, Tn as StackTerminationProtectionError, Tt as getBootstrapMarkerKey, U as IntrinsicFunctionResolver, Ut as resolveSkipPrefix, V as disableInstanceApiTermination, Vt as resolveAutoAssetStorage, W as cfnRefValueFromPhysicalId, Wt as resolveStateBucketWithDefault, X as assertRegionMatch, Xt as CFN_TEMPLATE_URL_LIMIT, Y as resolveExplicitPhysicalId, Yt as CFN_TEMPLATE_BODY_LIMIT, Z as configStringRefusal, Zt as MIGRATE_TMP_PREFIX, _ as createPreDeleteFinalSnapshot, _t as WorkGraph, a as DeploymentEventsStore, an as AwsClients, at as describeTypeWithThrottleRetry, b as unsupportedFinalSnapshotError, bn as PartialFailureError, bt as loadPublishableAssetManifest, c as replayFailedOperations, cn as setAwsClients, ct as isThrottlingError, d as IMPLICIT_DELETE_DEPENDENCIES, dt as LockManager, en as expectedOwnerParam, et as requireConfigArray, f as computeImplicitDeleteEdges, ft as S3StateBackend, g as ccRoutedFinalSnapshotError, gn as LocalStartServiceError, gt as stringifyValue, h as buildFinalSnapshotIdentifier, hn as LocalMigrateError, ht as AssetPublisher, i as DeploymentEventsReader, in as resolveBucketRegion, it as DiffCalculator, j as red, jn as withErrorHandling, jt as getDockerCmd, k as gray, kt as buildDockerImage, l as replayRollback, lt as DagBuilder, m as PRE_DELETE_SNAPSHOT_TYPES, mn as LocalInvokeBuildError$1, mt as shouldRetainResource, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as processStackMessages, nt as requireConfigString, o as planFailedOps, on as getAwsClients, ot as withRetry, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as DeployCancelledError, pt as rebuildClientForBucketRegion, q as WAFv2WebACLProvider, qt as stateBucketExistenceConfirmed, r as DeployEngine, rt as applyRoleArnIfSet, s as planRollback, sn as resetAwsClients, st as isRetryableTransientError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as AssemblyReader, tt as requireConfigObject, u as withResourceDeadline, un as CdkdError, ut as TemplateParser, v as isFinalSnapshotError, vn as MissingCdkCliError, vt as buildAssetRedirectMap, w as isStatefulRecreateTargetSync, wn as StackHasActiveImportsError, wt as ensureAssetStorage, x as makeCanonicalizePropertiesFn, xn as ProvisioningError, xt as rewriteTemplateAssetReferences, y as refusesFinalSnapshot, yn as NestedStackChildDirectDestroyError, yt as createAssetRedirectResolver, z as CloudControlProvider, zt as getLegacyStateBucketName } from "./deploy-engine-BWM9RxAW.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-B2MDP-jK.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-CsY1ahKM.js";
4
+ import { t as ASGProvider } from "./asg-provider-CgQFJdPo.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
@@ -36867,51 +37179,6 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
36867
37179
  }
36868
37180
  };
36869
37181
 
36870
- //#endregion
36871
- //#region src/utils/aws-partition.ts
36872
- /**
36873
- * AWS partition / URL-suffix derivation, shared across layers.
36874
- *
36875
- * Lives in `src/utils/` because it has consumers in two different layers: the
36876
- * `cdkd local *` command family (which passes a region in once to keep the STS
36877
- * hop minimal) and the provisioning layer's `AppSyncProvider`, which rebuilds a
36878
- * child resource's ARN when AWS did not report one.
36879
- *
36880
- * It was originally defined in `src/local/ecs-task-resolver.ts`, which
36881
- * re-exports it so every existing call site is unchanged; a provisioning
36882
- * provider importing from `src/local/**` would invert the layering.
36883
- *
36884
- * NOTE this is the mapping to prefer over `getAccountInfo().partition`
36885
- * (`src/deployment/intrinsic-function-resolver.ts`), which is hardcoded to
36886
- * `'aws'` and says so in its own comment.
36887
- */
36888
- /**
36889
- * Derive the AWS partition / URL suffix for an AWS region. Same mapping
36890
- * CloudFormation applies to `${AWS::Partition}` / `${AWS::URLSuffix}`.
36891
- */
36892
- function derivePartitionAndUrlSuffix(region) {
36893
- if (region.startsWith("cn-")) return {
36894
- partition: "aws-cn",
36895
- urlSuffix: "amazonaws.com.cn"
36896
- };
36897
- if (region.startsWith("us-gov-")) return {
36898
- partition: "aws-us-gov",
36899
- urlSuffix: "amazonaws.com"
36900
- };
36901
- if (region.startsWith("us-iso-")) return {
36902
- partition: "aws-iso",
36903
- urlSuffix: "c2s.ic.gov"
36904
- };
36905
- if (region.startsWith("us-isob-")) return {
36906
- partition: "aws-iso-b",
36907
- urlSuffix: "sc2s.sgov.gov"
36908
- };
36909
- return {
36910
- partition: "aws",
36911
- urlSuffix: "amazonaws.com"
36912
- };
36913
- }
36914
-
36915
37182
  //#endregion
36916
37183
  //#region src/provisioning/providers/appsync-provider.ts
36917
37184
  const MUTABLE_GRAPHQL_API_CONFIG_PROPERTIES = [
@@ -73590,7 +73857,7 @@ function createMigrateCommand() {
73590
73857
  */
73591
73858
  function buildProgram() {
73592
73859
  const program = new Command();
73593
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.13");
73860
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.15");
73594
73861
  program.hook("preAction", (_thisCommand, actionCommand) => {
73595
73862
  const { profile } = actionCommand.optsWithGlobals();
73596
73863
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;