Skip to content

Commit e2753db

Browse files
authored
feat(audit): nested CFn->SDK key-divergence critic + the two live bugs its first run caught (#1377)
1 parent 64ab5aa commit e2753db

37 files changed

Lines changed: 4377 additions & 680 deletions

.claude/rules/code-layout.md

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

.claude/rules/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ hand-fed inline `Tags` and so agreed with the bug.
119119
1. Create new file in `src/provisioning/providers/`
120120
2. Implement `ResourceProvider` interface
121121
3. Register in `src/provisioning/register-providers.ts` within the `registerAllProviders()` function
122-
4. Refresh the CFn schema fixture for the new type: `node scripts/refresh-cfn-schemas.mjs --only-missing` (requires AWS credentials with `cloudformation:DescribeType`). Then classify every unaccounted property into `handledProperties` (if `create()`/`update()` wires the field) or `unhandledByDesign` (with a one-line rationale) so the new `property-coverage` test stays green — see [docs/provider-development.md](../../docs/provider-development.md) §3c.
122+
4. Refresh the CFn schema fixture for the new type: `node scripts/refresh-cfn-schemas.mjs --only-missing` (requires AWS credentials with `cloudformation:DescribeType`). Then classify every unaccounted property into `handledProperties` (if `create()`/`update()` wires the field) or `unhandledByDesign` (with a one-line rationale) so the new `property-coverage` test stays green — see [docs/provider-development.md](../../docs/provider-development.md) §3c. If the provider FORWARDS a nested config blob (a `handledProperties` entry whose value is a nested object/array the provider re-shapes for the SDK), ALSO add it to `NESTED_KEY_TARGETS` in `scripts/gen-nested-key-coverage.ts` — the critic's first run audits every nested key spelling against the SDK model (the #1370 silent-drop class, issue #1373).
123123
5. Write tests
124124
6. Add the resource type to [docs/supported-resources.md](../../docs/supported-resources.md) (deploy/manage capability table) AND to [docs/import.md](../../docs/import.md) (import-side coverage: auto-lookup vs override-only vs sub-resource)
125125
7. **If the provider gates a stabilization wait on `process.env['CDKD_NO_WAIT']`** (i.e. `--no-wait` skips a multi-minute poll for this type), add the resource type to the `--no-wait` docs in ALL of: the `--no-wait` table + intro in [docs/cli-reference.md](../../docs/cli-reference.md), the `--no-wait` feature bullet in [README.md](../../README.md), and the `noWaitOption` help string + JSDoc in [src/cli/options.ts](../../src/cli/options.ts). Enforced by `tests/unit/provisioning/no-wait-doc-coverage.test.ts` (fails CI if a `CDKD_NO_WAIT`-honoring provider has no handled type in the cli-reference table). The `AWS::Lambda::MicrovmImage` provider shipped honoring `--no-wait` but missed this list — the test is the backstop.

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,25 @@ jobs:
199199
fi
200200
- run: vp run audit:update-wrap-coverage:check
201201

202+
# Fail CI when docs/_generated/nested-key-coverage.{json,md} is stale
203+
# relative to the provider sources / schema fixtures / SDK typings. The
204+
# matrix diffs, per SDK provider forwarding a nested CFn config blob,
205+
# the blob's nested CFn property names against the SDK client model's
206+
# member names — a CFn key with no same-spelling SDK member and no
207+
# explicit mention in the provider is a write-side silent drop (the
208+
# #1370 CloudFront / #1304 MetricTimeZone class; issue #1373). The
209+
# second step is the CRITIC: it hard-fails on any non-allow-listed
210+
# divergence.
211+
- name: nested-key-coverage matrix is up-to-date
212+
run: |
213+
vp run gen:nested-key-coverage
214+
if ! git diff --quiet -- docs/_generated/nested-key-coverage.json docs/_generated/nested-key-coverage.md; then
215+
echo "::error::nested-key-coverage matrix is stale — run \`vp run gen:nested-key-coverage\` and commit the regenerated docs/_generated/nested-key-coverage.{json,md}"
216+
git --no-pager diff -- docs/_generated/nested-key-coverage.json docs/_generated/nested-key-coverage.md
217+
exit 1
218+
fi
219+
- run: vp run audit:nested-key-coverage:check
220+
202221
runtime-compat:
203222
runs-on: ubuntu-latest
204223
needs: check-build-test

docs/_generated/enrichment-coverage.json

Lines changed: 33 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
"schemaVersion": 1,
33
"summary": {
44
"classifiedCount": 134,
5-
"enriched": 10,
6-
"noComputedAttr": 14,
5+
"enriched": 14,
6+
"noComputedAttr": 15,
77
"unenrichedGap": 0,
8-
"sdkFallbackGap": 110,
9-
"allowListedTypes": 16
8+
"sdkFallbackGap": 105,
9+
"allowListedTypes": 23
1010
},
1111
"types": [
1212
{
@@ -115,17 +115,17 @@
115115
},
116116
{
117117
"name": "ApiId",
118-
"status": "gap"
118+
"status": "allow-listed",
119+
"rationale": "primaryIdentifier == physicalId; resolves via the resolver physicalId fallback"
119120
}
120121
],
121122
"gaps": [
122-
"ApiEndpoint",
123-
"ApiId"
123+
"ApiEndpoint"
124124
]
125125
},
126126
{
127127
"resourceType": "AWS::ApiGatewayV2::Authorizer",
128-
"bucket": "sdk-fallback-gap",
128+
"bucket": "enriched",
129129
"sdkBacked": true,
130130
"readOnlyProperties": [
131131
"AuthorizerId"
@@ -134,16 +134,15 @@
134134
"attributes": [
135135
{
136136
"name": "AuthorizerId",
137-
"status": "gap"
137+
"status": "allow-listed",
138+
"rationale": "primaryIdentifier == physicalId; resolves via the resolver physicalId fallback"
138139
}
139140
],
140-
"gaps": [
141-
"AuthorizerId"
142-
]
141+
"gaps": []
143142
},
144143
{
145144
"resourceType": "AWS::ApiGatewayV2::Integration",
146-
"bucket": "sdk-fallback-gap",
145+
"bucket": "enriched",
147146
"sdkBacked": true,
148147
"readOnlyProperties": [
149148
"IntegrationId"
@@ -152,16 +151,15 @@
152151
"attributes": [
153152
{
154153
"name": "IntegrationId",
155-
"status": "gap"
154+
"status": "allow-listed",
155+
"rationale": "primaryIdentifier == physicalId; resolves via the resolver physicalId fallback"
156156
}
157157
],
158-
"gaps": [
159-
"IntegrationId"
160-
]
158+
"gaps": []
161159
},
162160
{
163161
"resourceType": "AWS::ApiGatewayV2::Route",
164-
"bucket": "sdk-fallback-gap",
162+
"bucket": "enriched",
165163
"sdkBacked": true,
166164
"readOnlyProperties": [
167165
"RouteId"
@@ -170,30 +168,20 @@
170168
"attributes": [
171169
{
172170
"name": "RouteId",
173-
"status": "gap"
171+
"status": "allow-listed",
172+
"rationale": "primaryIdentifier == physicalId; resolves via the resolver physicalId fallback"
174173
}
175174
],
176-
"gaps": [
177-
"RouteId"
178-
]
175+
"gaps": []
179176
},
180177
{
181178
"resourceType": "AWS::ApiGatewayV2::Stage",
182-
"bucket": "sdk-fallback-gap",
179+
"bucket": "no-computed-attr",
183180
"sdkBacked": true,
184-
"readOnlyProperties": [
185-
"Id"
186-
],
181+
"readOnlyProperties": [],
187182
"enrichedKeys": [],
188-
"attributes": [
189-
{
190-
"name": "Id",
191-
"status": "gap"
192-
}
193-
],
194-
"gaps": [
195-
"Id"
196-
]
183+
"attributes": [],
184+
"gaps": []
197185
},
198186
{
199187
"resourceType": "AWS::AppSync::ApiKey",
@@ -672,12 +660,12 @@
672660
},
673661
{
674662
"name": "Id",
675-
"status": "gap"
663+
"status": "allow-listed",
664+
"rationale": "primaryIdentifier == physicalId; resolves via the resolver physicalId fallback"
676665
}
677666
],
678667
"gaps": [
679-
"DomainName",
680-
"Id"
668+
"DomainName"
681669
]
682670
},
683671
{
@@ -1464,17 +1452,17 @@
14641452
},
14651453
{
14661454
"name": "ServiceArn",
1467-
"status": "gap"
1455+
"status": "allow-listed",
1456+
"rationale": "primaryIdentifier == physicalId; resolves via the resolver physicalId fallback"
14681457
}
14691458
],
14701459
"gaps": [
1471-
"Name",
1472-
"ServiceArn"
1460+
"Name"
14731461
]
14741462
},
14751463
{
14761464
"resourceType": "AWS::ECS::TaskDefinition",
1477-
"bucket": "sdk-fallback-gap",
1465+
"bucket": "enriched",
14781466
"sdkBacked": true,
14791467
"readOnlyProperties": [
14801468
"TaskDefinitionArn"
@@ -1483,12 +1471,11 @@
14831471
"attributes": [
14841472
{
14851473
"name": "TaskDefinitionArn",
1486-
"status": "gap"
1474+
"status": "allow-listed",
1475+
"rationale": "primaryIdentifier == physicalId; resolves via the resolver physicalId fallback"
14871476
}
14881477
],
1489-
"gaps": [
1490-
"TaskDefinitionArn"
1491-
]
1478+
"gaps": []
14921479
},
14931480
{
14941481
"resourceType": "AWS::EFS::AccessPoint",

docs/_generated/enrichment-coverage.md

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ Gap severity depends on the tier. A gap on an **SDK-backed** type (`sdk-fallback
1212
## Summary
1313

1414
- Classified types (cached schema): **134**
15-
- Fully enriched: **10**
16-
- No computed attribute (Ref == physicalId is correct): **14**
15+
- Fully enriched: **14**
16+
- No computed attribute (Ref == physicalId is correct): **15**
1717
- **Pure-CC latent gaps (unenriched-computed, blocks CI): 0**
18-
- SDK-fallback gaps (informational, #614 path only): **110**
19-
- Types with allow-listed (not-a-gap) attributes: **16**
18+
- SDK-fallback gaps (informational, #614 path only): **105**
19+
- Types with allow-listed (not-a-gap) attributes: **23**
2020

2121
## Pure-CC latent gaps
2222

@@ -32,11 +32,7 @@ SDK-backed types whose computed attribute is unenriched: only exposed on the #61
3232
| `AWS::ApiGateway::Authorizer` | `AuthorizerId` |
3333
| `AWS::ApiGateway::Deployment` | `DeploymentId` |
3434
| `AWS::ApiGateway::Resource` | `ResourceId` |
35-
| `AWS::ApiGatewayV2::Api` | `ApiEndpoint`, `ApiId` |
36-
| `AWS::ApiGatewayV2::Authorizer` | `AuthorizerId` |
37-
| `AWS::ApiGatewayV2::Integration` | `IntegrationId` |
38-
| `AWS::ApiGatewayV2::Route` | `RouteId` |
39-
| `AWS::ApiGatewayV2::Stage` | `Id` |
35+
| `AWS::ApiGatewayV2::Api` | `ApiEndpoint` |
4036
| `AWS::AppSync::ApiKey` | `ApiKey`, `ApiKeyId`, `Arn` |
4137
| `AWS::AppSync::DataSource` | `DataSourceArn` |
4238
| `AWS::AppSync::GraphQLApi` | `ApiId`, `Arn`, `GraphQLDns`, `GraphQLEndpointArn`, `GraphQLUrl`, `RealtimeDns`, `RealtimeUrl` |
@@ -50,7 +46,7 @@ SDK-backed types whose computed attribute is unenriched: only exposed on the #61
5046
| `AWS::CertificateManager::Certificate` | `Id` |
5147
| `AWS::CloudFormation::Stack` | `ChangeSetId`, `CreationTime`, `LastUpdateTime`, `Outputs`, `ParentId`, `RootId`, `StackId`, `StackStatus` |
5248
| `AWS::CloudFront::CloudFrontOriginAccessIdentity` | `Id` |
53-
| `AWS::CloudFront::Distribution` | `DomainName`, `Id` |
49+
| `AWS::CloudFront::Distribution` | `DomainName` |
5450
| `AWS::CloudTrail::Trail` | `Arn`, `SnsTopicArn` |
5551
| `AWS::CloudWatch::Alarm` | `Arn` |
5652
| `AWS::CodeBuild::Project` | `Arn`, `Id` |
@@ -77,8 +73,7 @@ SDK-backed types whose computed attribute is unenriched: only exposed on the #61
7773
| `AWS::EC2::VPC` | `CidrBlockAssociations`, `DefaultNetworkAcl`, `DefaultSecurityGroup`, `Ipv6CidrBlocks`, `VpcId` |
7874
| `AWS::EC2::VPCGatewayAttachment` | `AttachmentType` |
7975
| `AWS::ECS::Cluster` | `Arn` |
80-
| `AWS::ECS::Service` | `Name`, `ServiceArn` |
81-
| `AWS::ECS::TaskDefinition` | `TaskDefinitionArn` |
76+
| `AWS::ECS::Service` | `Name` |
8277
| `AWS::EFS::AccessPoint` | `AccessPointId`, `Arn` |
8378
| `AWS::EFS::FileSystem` | `Arn`, `FileSystemId` |
8479
| `AWS::EFS::MountTarget` | `Id` |
@@ -149,11 +144,11 @@ SDK-backed types whose computed attribute is unenriched: only exposed on the #61
149144
| `AWS::ApiGateway::Method` | yes | no-computed-attr | _(none)_ |
150145
| `AWS::ApiGateway::Resource` | yes | sdk-fallback-gap | `ResourceId` (GAP) |
151146
| `AWS::ApiGateway::Stage` | yes | no-computed-attr | _(none)_ |
152-
| `AWS::ApiGatewayV2::Api` | yes | sdk-fallback-gap | `ApiEndpoint` (GAP), `ApiId` (GAP) |
153-
| `AWS::ApiGatewayV2::Authorizer` | yes | sdk-fallback-gap | `AuthorizerId` (GAP) |
154-
| `AWS::ApiGatewayV2::Integration` | yes | sdk-fallback-gap | `IntegrationId` (GAP) |
155-
| `AWS::ApiGatewayV2::Route` | yes | sdk-fallback-gap | `RouteId` (GAP) |
156-
| `AWS::ApiGatewayV2::Stage` | yes | sdk-fallback-gap | `Id` (GAP) |
147+
| `AWS::ApiGatewayV2::Api` | yes | sdk-fallback-gap | `ApiEndpoint` (GAP), `ApiId` (allow) |
148+
| `AWS::ApiGatewayV2::Authorizer` | yes | enriched | `AuthorizerId` (allow) |
149+
| `AWS::ApiGatewayV2::Integration` | yes | enriched | `IntegrationId` (allow) |
150+
| `AWS::ApiGatewayV2::Route` | yes | enriched | `RouteId` (allow) |
151+
| `AWS::ApiGatewayV2::Stage` | yes | no-computed-attr | _(none)_ |
157152
| `AWS::AppSync::ApiKey` | yes | sdk-fallback-gap | `ApiKey` (GAP), `ApiKeyId` (GAP), `Arn` (GAP) |
158153
| `AWS::AppSync::DataSource` | yes | sdk-fallback-gap | `DataSourceArn` (GAP) |
159154
| `AWS::AppSync::GraphQLApi` | yes | sdk-fallback-gap | `ApiId` (GAP), `Arn` (GAP), `GraphQLDns` (GAP), `GraphQLEndpointArn` (GAP), `GraphQLUrl` (GAP), `RealtimeDns` (GAP), `RealtimeUrl` (GAP) |
@@ -169,7 +164,7 @@ SDK-backed types whose computed attribute is unenriched: only exposed on the #61
169164
| `AWS::CloudFormation::Stack` | yes | sdk-fallback-gap | `ChangeSetId` (GAP), `CreationTime` (GAP), `LastUpdateTime` (GAP), `Outputs` (GAP), `ParentId` (GAP), `RootId` (GAP), `StackId` (GAP), `StackStatus` (GAP) |
170165
| `AWS::CloudFormation::WaitConditionHandle` | yes | enriched | `Id` (allow) |
171166
| `AWS::CloudFront::CloudFrontOriginAccessIdentity` | yes | sdk-fallback-gap | `Id` (GAP), `S3CanonicalUserId` (OK) |
172-
| `AWS::CloudFront::Distribution` | yes | sdk-fallback-gap | `DomainName` (GAP), `Id` (GAP) |
167+
| `AWS::CloudFront::Distribution` | yes | sdk-fallback-gap | `DomainName` (GAP), `Id` (allow) |
173168
| `AWS::CloudFront::OriginAccessControl` | yes | enriched | `Id` (OK) |
174169
| `AWS::CloudTrail::Trail` | yes | sdk-fallback-gap | `Arn` (GAP), `SnsTopicArn` (GAP) |
175170
| `AWS::CloudWatch::Alarm` | yes | sdk-fallback-gap | `Arn` (GAP) |
@@ -200,8 +195,8 @@ SDK-backed types whose computed attribute is unenriched: only exposed on the #61
200195
| `AWS::EC2::VPCGatewayAttachment` | yes | sdk-fallback-gap | `AttachmentType` (GAP) |
201196
| `AWS::ECR::Repository` | yes | enriched | `Arn` (OK), `RepositoryUri` (OK) |
202197
| `AWS::ECS::Cluster` | yes | sdk-fallback-gap | `Arn` (GAP) |
203-
| `AWS::ECS::Service` | yes | sdk-fallback-gap | `Name` (GAP), `ServiceArn` (GAP) |
204-
| `AWS::ECS::TaskDefinition` | yes | sdk-fallback-gap | `TaskDefinitionArn` (GAP) |
198+
| `AWS::ECS::Service` | yes | sdk-fallback-gap | `Name` (GAP), `ServiceArn` (allow) |
199+
| `AWS::ECS::TaskDefinition` | yes | enriched | `TaskDefinitionArn` (allow) |
205200
| `AWS::EFS::AccessPoint` | yes | sdk-fallback-gap | `AccessPointId` (GAP), `Arn` (GAP) |
206201
| `AWS::EFS::FileSystem` | yes | sdk-fallback-gap | `Arn` (GAP), `FileSystemId` (GAP) |
207202
| `AWS::EFS::MountTarget` | yes | sdk-fallback-gap | `Id` (GAP) |

docs/_generated/integ-last-run.tsv

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ conditions-and-if 2026-07-26T19:05:27Z PASS 66 verify.sh 0727b sweep-b8 stalenes
5555
conditions-update-2 2026-07-26T19:05:27Z PASS 83 verify.sh 0727b sweep-b8 staleness re-run (rc=0); account clean
5656
context-test 2026-07-26T18:43:33Z PASS 24 standard 0727b sweep-b5 staleness re-run (rc=0); account clean
5757
cross-region-state-bucket 2026-08-01T09:42:37Z PASS 29 verify.sh 0801 regression sweep; cross-region bucket round-trip ok, temp bucket removed
58-
cross-stack-references 2026-08-01T09:51:12Z PASS 23 standard 0801 regression sweep; 2-stack exporter/consumer deploy+destroy clean
58+
cross-stack-references 2026-08-08T17:48:34Z PASS 25 standard marker re-flip after review coercion fix; deploy+destroy clean
5959
custom-resource-getatt-data 2026-07-20T14:59:39Z PASS 63 verify.sh CR Data GetAtt->dependent prop, 6 res clean
6060
custom-resource-provider 2026-07-24T08:31:58Z PASS 480 standard cross-region us-west-2 vs us-east-1 bucket (issue #1195 fix + hardening live-verified); 17 res, destroy 0 err 0 orphan
6161
data-analytics 2026-07-21T18:30:46Z PASS 50 verify.sh rc ok, orph clean
@@ -224,7 +224,7 @@ rollback-failure-injection 2026-08-08T17:42:23Z PASS 389 verify.sh issue #1375 r
224224
rollback-sqs-cooldown 2026-07-25T05:24:26Z PASS 180 verify.sh #1220 echo-before-assert on steps 4/9/10, destroy clean
225225
route53 2026-07-22T07:59:10Z PASS verify.sh TTL-refresh sweep; HostedZone/GeoProximity/CidrRouting backfills, 6 del 0 err, hosted zone + state gone
226226
s3-asset-deploy 2026-07-26T19:45:58Z PASS 58 verify.sh 0727b sweep-b12 staleness re-run (rc=0); account clean
227-
s3-cloudfront 2026-08-08T16:49:03Z PASS 223 verify.sh PR1372 post-rebase+lint-fix run; destroy clean 0 orphans
227+
s3-cloudfront 2026-08-08T17:42:05Z PASS 228 verify.sh 1373 OriginCustomHeaders create+update-survive asserts green; destroy 0 errors 0 orphans
228228
s3-directory-bucket 2026-08-02T15:38:47Z PASS 50 verify.sh re-run for #1347 wontdo-comment PR; guard + clean destroy, 0 orphans
229229
s3-event-notification 2026-07-21T05:28:05Z PASS 79 verify.sh rc ok, orph clean
230230
s3-lifecycle 2026-07-21T14:28:43Z PASS 58 verify.sh rc ok, orph clean

0 commit comments

Comments
 (0)